OpenSecurity/bin/opensecurity_client_restful_server.py
author BarthaM@N3SIM1218.D03.arc.local
Thu, 05 Jun 2014 15:04:13 +0100
changeset 181 3875d43bacd2
parent 179 04f1f06faaf0
child 182 6e9b8f105de1
permissions -rwxr-xr-x
fixed vmmanager update
     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     print networkPath
   300     if (os.path.exists(networkPath)):
   301         print networkPath, " is found..."
   302         print "Trying to map ", networkPath, " on to ", drive, " ....."
   303         try:
   304             win32wnet.WNetAddConnection2(win32netcon.RESOURCETYPE_DISK, drive, networkPath, None, user, password)
   305         except:
   306             print "Unexpected error..."
   307             return 1
   308         print "Mapping successful"
   309         return 0
   310     else:
   311         print "Network path unreachable..."
   312         return 1    
   313 		
   314 # handles netumount request                    
   315 class MountNetworkDriveHandler(threading.Thread): 
   316     networkPath = None
   317     def __init__(self, net_path):
   318         threading.Thread.__init__(self)
   319         self.networkPath = net_path
   320     
   321     def run(self):
   322         drive = genNetworkDrive()
   323         if not drive:
   324             logger.error("Failed to assign drive letter for: " + self.networkPath)
   325             return 1
   326         else:
   327             logger.info("Assigned drive " + drive + " to " + self.networkPath)
   328         
   329         #Check for drive availability
   330         drive = drive+':'
   331         if os.path.exists(drive):
   332             logger.error("Drive letter is already in use: " + drive)
   333             return 1
   334         
   335         #Check for network resource availability
   336         retry = 20
   337         while not os.path.exists(self.networkPath):
   338             if retry == 0:
   339                 break
   340             logger.info("Path not accessible: " + self.networkPath + " retrying")
   341             time.sleep(1)
   342             retry-=1
   343 
   344         return mapDrive(drive, self.networkPath, "", "")
   345 
   346 class os_netmount:
   347     
   348     """OpenSecurity '/netmount' handler"""
   349     
   350     def GET(self):
   351         # pick the arguments
   352         args = web.input()
   353         
   354         # we _need_ a net_resource
   355         if not "net_resource" in args:
   356             raise web.badrequest('no net_resource given')
   357         
   358         driveHandler = MountNetworkDriveHandler(args['net_resource'])
   359         driveHandler.start()
   360         driveHandler.join(None)
   361         return 'Ok'
   362 
   363 def unmapDrive(drive, force=0):
   364     print "drive in use, trying to unmap..."
   365     if force == 0:
   366         print "Executing un-forced call..."
   367     
   368     try:
   369         win32wnet.WNetCancelConnection2(drive, 1, force)
   370         print drive, "successfully unmapped..."
   371         return 0
   372     except:
   373         print "Unmap failed, try again..."
   374         return 1
   375 
   376 # handles netumount request                    
   377 class UmountNetworkDriveHandler(threading.Thread): 
   378     networkPath = None
   379     running = True
   380     
   381     def __init__(self, path):
   382         threading.Thread.__init__(self)
   383         self.networkPath = path
   384 
   385     def run(self):
   386         while self.running:
   387             drive = getNetworkDrive(self.networkPath)
   388             if not drive:
   389                 logger.info("Failed to retrieve drive letter for: " + self.networkPath + ". Successfully deleted or missing.")
   390                 self.running = False
   391             else:
   392                 drive = drive+':'
   393                 logger.info("Unmounting drive " + drive + " for " + self.networkPath)
   394                 result = unmapDrive(drive, force=1) 
   395                 if result != 0:
   396                     continue
   397                         
   398 
   399 class os_netumount:
   400     
   401     """OpenSecurity '/netumount' handler"""
   402     
   403     def GET(self):
   404         # pick the arguments
   405         args = web.input()
   406         
   407         # we _need_ a net_resource
   408         if not "net_resource" in args:
   409             raise web.badrequest('no net_resource given')
   410         
   411         driveHandler = UmountNetworkDriveHandler(args['net_resource'])
   412         driveHandler.start()
   413         driveHandler.join(None)
   414         return 'Ok'
   415 
   416 class os_netcleanup:
   417     
   418     """OpenSecurity '/netcleanup' handler"""
   419     
   420     def GET(self):
   421         # pick the arguments
   422         args = web.input()
   423         
   424         # we _need_ a net_resource
   425         if not "hostonly_ip" in args:
   426             raise web.badrequest('no hostonly_ip given')
   427         
   428         ip = args['hostonly_ip']
   429         ip = ip[:ip.rindex('.')]
   430         drives = getLogicalDrives()
   431         for drive in drives:
   432             # found network drive
   433             if getDriveType(drive) == 4:
   434                 path = getNetworkPath(drive)
   435                 if ip in path:
   436                     driveHandler = UmountNetworkDriveHandler(path)
   437                     driveHandler.start()
   438                     driveHandler.join(None)
   439 
   440 class os_root:
   441 
   442     """OpenSecurity '/' handler"""
   443     
   444     def GET(self):
   445     
   446         res = "OpenSecurity-Client RESTFul Server { \"version\": \"%s\" }" % opensecurity.__version__
   447         
   448         # add some sample links
   449         res = res + """
   450         
   451 USAGE EXAMPLES:
   452         
   453 Request a password: 
   454     (copy paste this into your browser's address field after the host:port)
   455     
   456     /password?text=Give+me+a+password+for+device+%22My+USB+Drive%22+(ID%3A+32090-AAA-X0)
   457     
   458     (eg.: http://127.0.0.1:8090/password?text=Give+me+a+password+for+device+%22My+USB+Drive%22+(ID%3A+32090-AAA-X0))
   459     NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
   460     
   461     
   462 Request a combination of user and password:
   463     (copy paste this into your browser's address field after the host:port)
   464     
   465     /credentials?text=Tell+the+NSA+which+credentials+to+use+in+order+to+avoid+hacking+noise+on+wire.
   466     
   467     (eg.: http://127.0.0.1:8090/credentials?text=Tell+the+NSA+which+credentials+to+use+in+order+to+avoid+hacking+noise+on+wire.)
   468     NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
   469     
   470 
   471 Request a combination of password and keyfile:
   472     (copy paste this into your browser's address field after the host:port)
   473     
   474     /keyfile?text=Your%20private%20RSA%20Keyfile%3A
   475     
   476     (eg.: http://127.0.0.1:8090//keyfile?text=Your%20private%20RSA%20Keyfile%3A)
   477     NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
   478     
   479 
   480 Start a Browser:
   481     (copy paste this into your browser's address field after the host:port)
   482 
   483     /application?vm=Debian+7&app=Browser
   484 
   485     (e.g. http://127.0.0.1:8090/application?vm=Debian+7&app=Browser)
   486         """
   487     
   488         return res
   489 
   490 
   491 class ProcessResultBouncer(threading.Thread):
   492 
   493     """A class to post the result of a given process - assuming it to be in JSON - to a REST Api."""
   494 
   495     def __init__(self, process, remote_ip, resource): 
   496 
   497         """ctor"""
   498 
   499         threading.Thread.__init__(self)
   500         self._process = process
   501         self._remote_ip = remote_ip
   502         self._resource = resource
   503  
   504     
   505     def stop(self):
   506 
   507         """stop thread"""
   508         self.running = False
   509         
   510     
   511     def run(self):
   512 
   513         """run the thread"""
   514 
   515         # invoke the user dialog as a subprocess
   516         result = self._process.communicate()[0]
   517         if self._process.returncode != 0:
   518             print 'user request has been aborted.'
   519             return
   520         
   521         # all ok, tell send request back appropriate destination
   522         try:
   523             j = json.loads(result)
   524         except:
   525             print 'error in password parsing'
   526             return
   527         
   528         # by provided a 'data' we turn this into a POST statement
   529         url_addr = 'http://' + self._remote_ip + ':58080' + self._resource
   530         req = urllib2.Request(url_addr, urllib.urlencode(j))
   531         try:
   532             res = urllib2.urlopen(req)
   533         except:
   534             print 'failed to contact: ' + url_addr
   535             return 
   536 
   537 
   538 class RESTServerThread(threading.Thread):
   539 
   540     """Thread for serving the REST API."""
   541 
   542     def __init__(self, port): 
   543 
   544         """ctor"""
   545         threading.Thread.__init__(self)
   546         self._port = port 
   547     
   548     def stop(self):
   549 
   550         """stop thread"""
   551         self.running = False
   552         
   553     
   554     def run(self):
   555 
   556         """run the thread"""
   557         _serve(self._port)
   558 
   559 
   560 
   561 def is_already_running(port = 8090):
   562 
   563     """check if this is started twice"""
   564 
   565     try:
   566         s = socket.create_connection(('127.0.0.1', port), 0.5)
   567     except:
   568         return False
   569 
   570     return True
   571 
   572 
   573 def _bounce_vm_logs():
   574 
   575     """grab all logs from the VMs and push them to the log servers"""
   576 
   577     global log_file_lock
   578 
   579     # pick the highest current number
   580     cur = 0
   581     for f in glob.iglob(os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.*')):
   582         try:
   583             n = f.split('.')[-1:][0]
   584             if cur < int(n):
   585                 cur = int(n)
   586         except:
   587             pass
   588 
   589     cur = cur + 1
   590 
   591     # first add new vm logs to our existing one: rename the log file
   592     log_file_name_new = os.path.join(Environment('OpenSecurity').log_path, 'vm_new.log')
   593     log_file_name_cur = os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.' + str(cur))
   594     log_file_lock.acquire()
   595     try:
   596         os.rename(log_file_name_new, log_file_name_cur)
   597         print('new log file: ' + log_file_name_cur)
   598     except:
   599         pass
   600     log_file_lock.release()
   601 
   602     # now we have a list of next log files to dump
   603     log_files = glob.glob(os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.*'))
   604     log_files.sort()
   605     for log_file in log_files:
   606 
   607         try:
   608             f = open(log_file, 'rb')
   609             while True:
   610                 l = pickle.load(f)
   611                 _push_log(l)
   612 
   613         except EOFError:
   614 
   615             try:
   616                 os.remove(log_file)
   617             except:
   618                 logger.warning('tried to delete log file (pushed to EOF) "' + log_file + '" but failed')
   619 
   620         except:
   621             logger.warning('encountered error while pushing log file "' + log_file + '"')
   622             break
   623 
   624     # start bouncer again ...
   625     global log_file_bouncer
   626     log_file_bouncer = threading.Timer(5.0, _bounce_vm_logs)
   627     log_file_bouncer.start()
   628 
   629 
   630 def _push_log(log):
   631     """POST a single log to log server
   632 
   633     @param  log     the log POST param
   634     """
   635 
   636     log_server_url = "http://extern.x-net.at/opensecurity/log"
   637     try:
   638         key = win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE, 'SOFTWARE\OpenSecurity')
   639         log_server_url = str(win32api.RegQueryValueEx(key, 'LogServerURL')[0])
   640         win32api.RegCloseKey(key)
   641     except:
   642         logger.warning('Cannot open Registry HKEY_LOCAL_MACHINE\SOFTWARE\OpenSecurity and get "LogServerURL" value, using default instead')
   643 
   644     # by provided a 'data' we turn this into a POST statement
   645     d = urllib.urlencode(log)
   646     req = urllib2.Request(log_server_url, d)
   647     urllib2.urlopen(req)
   648     logger.debug('pushed log to server: ' + str(log_server_url))
   649 
   650 
   651 def _serve(port):
   652 
   653     """Start the REST server"""
   654 
   655     global server
   656 
   657     # start the VM-log bouncer timer
   658     global log_file_bouncer
   659     log_file_bouncer = threading.Timer(5.0, _bounce_vm_logs)
   660     log_file_bouncer.start()
   661 
   662     # trick the web.py server 
   663     sys.argv = [__file__, str(port)]
   664     server = web.application(opensecurity_urls, globals())
   665     server.run()
   666 
   667 
   668 def serve(port = 8090, background = False):
   669 
   670     """Start serving the REST Api
   671     port ... port number to listen on
   672     background ... cease into background (spawn thread) and return immediately"""
   673 
   674     # start threaded or direct version
   675     if background == True:
   676         t = RESTServerThread(port)
   677         t.start()
   678     else:
   679         _serve(port)
   680 
   681 def stop():
   682 
   683     """Stop serving the REST Api"""
   684 
   685     global server
   686     if server is None:
   687         return
   688 
   689     global log_file_bouncer
   690     if log_file_bouncer is not None:
   691         log_file_bouncer.cancel()
   692 
   693     server.stop()
   694 
   695 # start
   696 if __name__ == "__main__":
   697     serve()
   698