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