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