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