OpenSecurity/bin/opensecurity_client_restful_server.py
author BarthaM@N3SIM1218.D03.arc.local
Thu, 12 Jun 2014 10:34:46 +0100
changeset 185 fed80e5183b4
parent 184 416e097660da
child 193 8d5b7c9ff783
permissions -rwxr-xr-x
fixed log remove bug
     1 #!/usr/bin/env python
     2 # -*- coding: utf-8 -*-
     3 
     4 # ------------------------------------------------------------
     5 # opensecurity_client_restful_server
     6 # 
     7 # the OpenSecurity client RESTful server
     8 #
     9 # Autor: Oliver Maurhart, <oliver.maurhart@ait.ac.at>
    10 #
    11 # Copyright (C) 2013 AIT Austrian Institute of Technology
    12 # AIT Austrian Institute of Technology GmbH
    13 # Donau-City-Strasse 1 | 1220 Vienna | Austria
    14 # http://www.ait.ac.at
    15 #
    16 # This program is free software; you can redistribute it and/or
    17 # modify it under the terms of the GNU General Public License
    18 # as published by the Free Software Foundation version 2.
    19 # 
    20 # This program is distributed in the hope that it will be useful,
    21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
    22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    23 # GNU General Public License for more details.
    24 # 
    25 # You should have received a copy of the GNU General Public License
    26 # along with this program; if not, write to the Free Software
    27 # Foundation, Inc., 51 Franklin Street, Fifth Floor, 
    28 # Boston, MA  02110-1301, USA.
    29 # ------------------------------------------------------------
    30 
    31 
    32 # ------------------------------------------------------------
    33 # imports
    34 
    35 import getpass
    36 import glob
    37 import json
    38 import os
    39 import os.path
    40 import pickle
    41 import platform
    42 import socket
    43 import subprocess
    44 import sys
    45 import threading
    46 import time
    47 import urllib
    48 import urllib2
    49 import web
    50 import threading
    51 import time
    52 import string
    53 import win32api
    54 import win32con
    55 import win32wnet
    56 import win32netcon
    57 import itertools
    58 import ctypes
    59 
    60 from opensecurity_util import logger, setupLogger, OpenSecurityException
    61 if sys.platform == 'win32' or sys.platform == 'cygwin':
    62     from cygwin import Cygwin
    63 
    64 # local
    65 import __init__ as opensecurity
    66 from environment import Environment
    67 
    68 
    69 # ------------------------------------------------------------
    70 # const
    71 
    72 
    73 """All the URLs we know mapping to class handler"""
    74 opensecurity_urls = (
    75     '/credentials',             'os_credentials',
    76     '/keyfile',                 'os_keyfile',
    77     '/log',                     'os_log',
    78     '/notification',            'os_notification',
    79     '/password',                'os_password',
    80     '/netmount',                'os_netmount',
    81     '/netumount',               'os_netumount',
    82     '/netcleanup',              'os_netcleanup',
    83     '/',                        'os_root'
    84 )
    85 
    86 
    87 # ------------------------------------------------------------
    88 # vars
    89 
    90 
    91 """lock for read/write log file"""
    92 log_file_lock = threading.Lock()
    93 
    94 """timer for the log file bouncer"""
    95 log_file_bouncer = None
    96 
    97 
    98 """The REST server object"""
    99 server = None
   100 
   101 
   102 # ------------------------------------------------------------
   103 # code
   104 
   105 
   106 class os_credentials:
   107 
   108     """OpenSecurity '/credentials' handler.
   109     
   110     This is called on GET /credentials?text=TEXT.
   111     Ideally this should pop up a user dialog to insert his
   112     credentials based the given TEXT.
   113     """
   114     
   115     def GET(self):
   116         
   117         # pick the arguments
   118         args = web.input()
   119         
   120         # we _need_ a text
   121         if not "text" in args:
   122             raise web.badrequest('no text given')
   123         
   124         # remember remote ip
   125         remote_ip = web.ctx.environ['REMOTE_ADDR']
   126 
   127         # create the process which queries the user
   128         dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
   129         process_command = [sys.executable, dlg_image, 'credentials', args.text]
   130         process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)        
   131         
   132         # run process result handling in seprate thread (not to block main one)
   133         bouncer = ProcessResultBouncer(process, remote_ip, '/credentials')
   134         bouncer.start()
   135          
   136         return 'user queried for credentials'
   137 
   138 
   139 class os_keyfile:
   140 
   141     """OpenSecurity '/keyfile' handler.
   142     
   143     This is called on GET /keyfile?text=TEXT.
   144     Ideally this should pop up a user dialog to insert his
   145     password along with a keyfile.
   146     """
   147     
   148     def GET(self):
   149         
   150         # pick the arguments
   151         args = web.input()
   152         
   153         # we _need_ a text
   154         if not "text" in args:
   155             raise web.badrequest('no text given')
   156             
   157         # remember remote ip
   158         remote_ip = web.ctx.environ['REMOTE_ADDR']
   159         
   160         # create the process which queries the user
   161         dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
   162         process_command = [sys.executable, dlg_image, 'keyfile', args.text]
   163         process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)        
   164         
   165         # run process result handling in seprate thread (not to block main one)
   166         bouncer = ProcessResultBouncer(process, remote_ip, '/keyfile')
   167         bouncer.start()
   168          
   169         return 'user queried for password and keyfile'
   170 
   171 
   172 class os_log:
   173 
   174     """OpenSecurity '/log' handler.
   175     
   176     This is called on GET or POST on the log function /log
   177     """
   178     
   179     def GET(self):
   180         
   181         # pick the arguments
   182         self.POST()
   183 
   184 
   185     def POST(self):
   186         
   187         # pick the arguments
   188         args = web.input()
   189         args['user'] = getpass.getuser()
   190         args['system'] = platform.node() + " " + platform.system() + " " + platform.release()
   191 
   192         # add these to new data to log
   193         global log_file_lock
   194         log_file_name = os.path.join(Environment('OpenSecurity').log_path, 'vm_new.log')
   195         log_file_lock.acquire()
   196         pickle.dump(args,  open(log_file_name, 'ab'))
   197         log_file_lock.release()
   198 
   199         return "Ok"
   200 
   201 
   202 class os_notification:
   203 
   204     """OpenSecurity '/notification' handler.
   205     
   206     This is called on GET /notification?msgtype=TYPE&text=TEXT.
   207     This will pop up an OpenSecurity notifcation window
   208     """
   209 
   210     def POST(self):
   211         return self.GET()
   212     
   213     def GET(self):
   214         
   215         # pick the arguments
   216         args = web.input()
   217         
   218         # we _need_ a type
   219         if not "msgtype" in args:
   220             raise web.badrequest('no msgtype given')
   221             
   222         if not args.msgtype in ['information', 'warning', 'critical']:
   223             raise web.badrequest('Unknown value for msgtype')
   224             
   225         # we _need_ a text
   226         if not "text" in args:
   227             raise web.badrequest('no text given')
   228             
   229         # invoke the user dialog as a subprocess
   230         dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.py')
   231         process_command = [sys.executable, dlg_image, 'notification-' + args.msgtype, args.text]
   232         process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)
   233 
   234         return "Ok"
   235 
   236 
   237 class os_password:
   238 
   239     """OpenSecurity '/password' handler.
   240     
   241     This is called on GET /password?text=TEXT.
   242     Ideally this should pop up a user dialog to insert his
   243     password based device name.
   244     """
   245     
   246     def GET(self):
   247         
   248         # pick the arguments
   249         args = web.input()
   250         
   251         # we _need_ a text
   252         if not "text" in args:
   253             raise web.badrequest('no text given')
   254             
   255         # remember remote ip
   256         remote_ip = web.ctx.environ['REMOTE_ADDR']
   257         
   258         # create the process which queries the user
   259         dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
   260         process_command = [sys.executable, dlg_image, 'password', args.text]
   261         process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)        
   262         
   263         # run process result handling in seprate thread (not to block main one)
   264         bouncer = ProcessResultBouncer(process, remote_ip, '/password')
   265         bouncer.start()
   266         
   267         return 'user queried for password'
   268 
   269 def genNetworkDrive():
   270     logical_drives = getLogicalDrives()
   271     logger.info("Used logical drive letters: "+ str(logical_drives).strip('[]') )
   272     drives = list(map(chr, range(68, 91)))  
   273     for drive in drives:
   274         if drive not in logical_drives:
   275             return drive
   276     return None
   277             
   278 def getLogicalDrives():
   279     drive_bitmask = ctypes.cdll.kernel32.GetLogicalDrives()
   280     drives = list(itertools.compress(string.ascii_uppercase,  map(lambda x:ord(x) - ord('0'), bin(drive_bitmask)[:1:-1])))
   281     return drives
   282 
   283 def getNetworkPath(drive):
   284     return win32wnet.WNetGetConnection(drive+':')
   285 
   286 def getDriveType(drive):
   287     return ctypes.cdll.kernel32.GetDriveTypeW(u"%s:\\"%drive)
   288         
   289 def getNetworkDrive(path):
   290     for drive in getLogicalDrives():
   291         #if is a network drive
   292         if getDriveType(drive) == 4:
   293             network_path = getNetworkPath(drive)
   294             if path in network_path:
   295                 return drive
   296     return None
   297 	
   298 def mapDrive(drive, networkPath, user, password):
   299     if (os.path.exists(networkPath)):
   300         logger.debug(networkPath + " is found...")
   301         logger.debug("Trying to map " + networkPath + " on to " + drive + " .....")
   302         try:
   303             win32wnet.WNetAddConnection2(win32netcon.RESOURCETYPE_DISK, drive, networkPath, None, user, password)
   304         except:
   305             logger.error("Unexpected error...")
   306             return 1
   307         logger.info("Mapping successful")
   308         return 0
   309     else:
   310         logger.error("Network path unreachable...")
   311         return 1    
   312 
   313 mount_lock = threading.Lock()
   314 		
   315 # handles netumount request                    
   316 class MountNetworkDriveHandler(threading.Thread): 
   317     networkPath = None
   318     def __init__(self, net_path):
   319         threading.Thread.__init__(self)
   320         self.networkPath = net_path
   321     
   322     def run(self):
   323         #Check for network resource availability
   324         retry = 20
   325         while not os.path.exists(self.networkPath):
   326             if retry == 0:
   327                 break
   328             logger.info("Path not accessible: " + self.networkPath + " retrying")
   329             time.sleep(1)
   330             retry-=1
   331         with mount_lock:
   332             drive = genNetworkDrive()
   333             if not drive:
   334                 logger.error("Failed to assign drive letter for: " + self.networkPath)
   335                 return 1
   336             else:
   337                 logger.info("Assigned drive " + drive + " to " + self.networkPath)
   338             
   339             #Check for drive availability
   340             drive = drive+':'
   341             if os.path.exists(drive):
   342                 logger.error("Drive letter is already in use: " + drive)
   343                 return 1
   344             
   345             return mapDrive(drive, self.networkPath, "", "")
   346 
   347 class os_netmount:
   348     
   349     """OpenSecurity '/netmount' handler"""
   350     
   351     def GET(self):
   352         # pick the arguments
   353         args = web.input()
   354         
   355         # we _need_ a net_resource
   356         if not "net_resource" in args:
   357             raise web.badrequest('no net_resource given')
   358         
   359         driveHandler = MountNetworkDriveHandler(args['net_resource'])
   360         driveHandler.start()
   361         driveHandler.join(None)
   362         return 'Ok'
   363 
   364 def unmapDrive(drive, force=0):
   365     logger.debug("drive in use, trying to unmap...")
   366     if force == 0:
   367         logger.debug("Executing un-forced call...")
   368     
   369     try:
   370         win32wnet.WNetCancelConnection2(drive, 1, force)
   371         logger,info(drive + "successfully unmapped...")
   372         return 0
   373     except:
   374         logger.error("Unmap failed, try again...")
   375         return 1
   376 
   377 # handles netumount request                    
   378 class UmountNetworkDriveHandler(threading.Thread): 
   379     networkPath = None
   380     running = True
   381     
   382     def __init__(self, path):
   383         threading.Thread.__init__(self)
   384         self.networkPath = path
   385 
   386     def run(self):
   387         while self.running:
   388             drive = getNetworkDrive(self.networkPath)
   389             if not drive:
   390                 logger.info("Failed to retrieve drive letter for: " + self.networkPath + ". Successfully deleted or missing.")
   391                 self.running = False
   392             else:
   393                 drive = drive+':'
   394                 logger.info("Unmounting drive " + drive + " for " + self.networkPath)
   395                 result = unmapDrive(drive, force=1) 
   396                 if result != 0:
   397                     continue
   398                         
   399 
   400 class os_netumount:
   401     
   402     """OpenSecurity '/netumount' handler"""
   403     
   404     def GET(self):
   405         # pick the arguments
   406         args = web.input()
   407         
   408         # we _need_ a net_resource
   409         if not "net_resource" in args:
   410             raise web.badrequest('no net_resource given')
   411         
   412         driveHandler = UmountNetworkDriveHandler(args['net_resource'])
   413         driveHandler.start()
   414         driveHandler.join(None)
   415         return 'Ok'
   416 
   417 class os_netcleanup:
   418     
   419     """OpenSecurity '/netcleanup' handler"""
   420     
   421     def GET(self):
   422         # pick the arguments
   423         args = web.input()
   424         
   425         # we _need_ a net_resource
   426         if not "hostonly_ip" in args:
   427             raise web.badrequest('no hostonly_ip given')
   428         
   429         ip = args['hostonly_ip']
   430         ip = ip[:ip.rindex('.')]
   431         drives = getLogicalDrives()
   432         for drive in drives:
   433             # found network drive
   434             if getDriveType(drive) == 4:
   435                 path = getNetworkPath(drive)
   436                 if ip in path:
   437                     driveHandler = UmountNetworkDriveHandler(path)
   438                     driveHandler.start()
   439                     driveHandler.join(None)
   440 
   441 class os_root:
   442 
   443     """OpenSecurity '/' handler"""
   444     
   445     def GET(self):
   446     
   447         res = "OpenSecurity-Client RESTFul Server { \"version\": \"%s\" }" % opensecurity.__version__
   448         
   449         # add some sample links
   450         res = res + """
   451         
   452 USAGE EXAMPLES:
   453         
   454 Request a password: 
   455     (copy paste this into your browser's address field after the host:port)
   456     
   457     /password?text=Give+me+a+password+for+device+%22My+USB+Drive%22+(ID%3A+32090-AAA-X0)
   458     
   459     (eg.: http://127.0.0.1:8090/password?text=Give+me+a+password+for+device+%22My+USB+Drive%22+(ID%3A+32090-AAA-X0))
   460     NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
   461     
   462     
   463 Request a combination of user and password:
   464     (copy paste this into your browser's address field after the host:port)
   465     
   466     /credentials?text=Tell+the+NSA+which+credentials+to+use+in+order+to+avoid+hacking+noise+on+wire.
   467     
   468     (eg.: http://127.0.0.1:8090/credentials?text=Tell+the+NSA+which+credentials+to+use+in+order+to+avoid+hacking+noise+on+wire.)
   469     NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
   470     
   471 
   472 Request a combination of password and keyfile:
   473     (copy paste this into your browser's address field after the host:port)
   474     
   475     /keyfile?text=Your%20private%20RSA%20Keyfile%3A
   476     
   477     (eg.: http://127.0.0.1:8090//keyfile?text=Your%20private%20RSA%20Keyfile%3A)
   478     NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
   479     
   480 
   481 Start a Browser:
   482     (copy paste this into your browser's address field after the host:port)
   483 
   484     /application?vm=Debian+7&app=Browser
   485 
   486     (e.g. http://127.0.0.1:8090/application?vm=Debian+7&app=Browser)
   487         """
   488     
   489         return res
   490 
   491 
   492 class ProcessResultBouncer(threading.Thread):
   493 
   494     """A class to post the result of a given process - assuming it to be in JSON - to a REST Api."""
   495 
   496     def __init__(self, process, remote_ip, resource): 
   497 
   498         """ctor"""
   499 
   500         threading.Thread.__init__(self)
   501         self._process = process
   502         self._remote_ip = remote_ip
   503         self._resource = resource
   504  
   505     
   506     def stop(self):
   507 
   508         """stop thread"""
   509         self.running = False
   510         
   511     
   512     def run(self):
   513 
   514         """run the thread"""
   515 
   516         # invoke the user dialog as a subprocess
   517         result = self._process.communicate()[0]
   518         if self._process.returncode != 0:
   519             print 'user request has been aborted.'
   520             return
   521         
   522         # all ok, tell send request back appropriate destination
   523         try:
   524             j = json.loads(result)
   525         except:
   526             print 'error in password parsing'
   527             return
   528         
   529         # by provided a 'data' we turn this into a POST statement
   530         url_addr = 'http://' + self._remote_ip + ':58080' + self._resource
   531         req = urllib2.Request(url_addr, urllib.urlencode(j))
   532         try:
   533             res = urllib2.urlopen(req)
   534         except:
   535             print 'failed to contact: ' + url_addr
   536             return 
   537 
   538 
   539 class RESTServerThread(threading.Thread):
   540 
   541     """Thread for serving the REST API."""
   542 
   543     def __init__(self, port): 
   544 
   545         """ctor"""
   546         threading.Thread.__init__(self)
   547         self._port = port 
   548     
   549     def stop(self):
   550 
   551         """stop thread"""
   552         self.running = False
   553         
   554     
   555     def run(self):
   556 
   557         """run the thread"""
   558         _serve(self._port)
   559 
   560 
   561 
   562 def is_already_running(port = 8090):
   563 
   564     """check if this is started twice"""
   565 
   566     try:
   567         s = socket.create_connection(('127.0.0.1', port), 0.5)
   568     except:
   569         return False
   570 
   571     return True
   572 
   573 
   574 def _bounce_vm_logs():
   575 
   576     """grab all logs from the VMs and push them to the log servers"""
   577 
   578     global log_file_lock
   579 
   580     # pick the highest current number
   581     cur = 0
   582     for f in glob.iglob(os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.*')):
   583         try:
   584             n = f.split('.')[-1:][0]
   585             if cur < int(n):
   586                 cur = int(n)
   587         except:
   588             pass
   589 
   590     cur = cur + 1
   591 
   592     # first add new vm logs to our existing one: rename the log file
   593     log_file_name_new = os.path.join(Environment('OpenSecurity').log_path, 'vm_new.log')
   594     log_file_name_cur = os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.' + str(cur))
   595     log_file_lock.acquire()
   596     try:
   597         os.rename(log_file_name_new, log_file_name_cur)
   598         print('new log file: ' + log_file_name_cur)
   599     except:
   600         pass
   601     log_file_lock.release()
   602 
   603     # now we have a list of next log files to dump
   604     log_files = glob.glob(os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.*'))
   605     log_files.sort()
   606     for log_file in log_files:
   607 
   608         try:
   609             f = open(log_file, 'rb')
   610             while True:
   611                 l = pickle.load(f)
   612                 _push_log(l)
   613 
   614         except EOFError:
   615             try:
   616                 f.close()
   617                 os.remove(log_file)
   618             except:
   619                 logger.warning('tried to delete log file (pushed to EOF) "' + log_file + '" but failed')
   620 
   621         except:
   622             logger.warning('encountered error while pushing log file "' + log_file + '"')
   623             break
   624 
   625     # start bouncer again ...
   626     global log_file_bouncer
   627     log_file_bouncer = threading.Timer(5.0, _bounce_vm_logs)
   628     log_file_bouncer.start()
   629 
   630 
   631 def _push_log(log):
   632     """POST a single log to log server
   633 
   634     @param  log     the log POST param
   635     """
   636 
   637     log_server_url = "http://extern.x-net.at/opensecurity/log"
   638     try:
   639         key = win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE, 'SOFTWARE\OpenSecurity')
   640         log_server_url = str(win32api.RegQueryValueEx(key, 'LogServerURL')[0])
   641         win32api.RegCloseKey(key)
   642     except:
   643         logger.warning('Cannot open Registry HKEY_LOCAL_MACHINE\SOFTWARE\OpenSecurity and get "LogServerURL" value, using default instead')
   644 
   645     # by provided a 'data' we turn this into a POST statement
   646     d = urllib.urlencode(log)
   647     req = urllib2.Request(log_server_url, d)
   648     urllib2.urlopen(req)
   649     logger.debug('pushed log to server: ' + str(log_server_url))
   650 
   651 
   652 def _serve(port):
   653 
   654     """Start the REST server"""
   655 
   656     global server
   657 
   658     # start the VM-log bouncer timer
   659     global log_file_bouncer
   660     log_file_bouncer = threading.Timer(5.0, _bounce_vm_logs)
   661     log_file_bouncer.start()
   662 
   663     # trick the web.py server 
   664     sys.argv = [__file__, str(port)]
   665     server = web.application(opensecurity_urls, globals())
   666     server.run()
   667 
   668 
   669 def serve(port = 8090, background = False):
   670 
   671     """Start serving the REST Api
   672     port ... port number to listen on
   673     background ... cease into background (spawn thread) and return immediately"""
   674 
   675     # start threaded or direct version
   676     if background == True:
   677         t = RESTServerThread(port)
   678         t.start()
   679     else:
   680         _serve(port)
   681 
   682 def stop():
   683 
   684     """Stop serving the REST Api"""
   685 
   686     global server
   687     if server is None:
   688         return
   689 
   690     global log_file_bouncer
   691     if log_file_bouncer is not None:
   692         log_file_bouncer.cancel()
   693 
   694     server.stop()
   695 
   696 # start
   697 if __name__ == "__main__":
   698     serve()
   699