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