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