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