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