OpenSecurity/bin/opensecurity_client_restful_server.py
author Oliver Maurhart <oliver.maurhart@ait.ac.at>
Thu, 22 May 2014 11:38:21 +0200
changeset 168 76267df09d71
parent 167 1e1811fa44bc
child 176 32c895509a2a
permissions -rwxr-xr-x
add default log server
     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 
    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         driveHandler.join(None)
   313         return 'Ok'
   314 
   315          
   316         
   317 # handles netumount request                    
   318 class UmountNetworkDriveHandler(threading.Thread): 
   319     drive = None
   320     running = True
   321     
   322     def __init__(self, drv):
   323         threading.Thread.__init__(self)
   324         self.drive = drv
   325 
   326     def run(self):
   327         while self.running:
   328             result = Cygwin.checkResult(Cygwin.execute('C:\\Windows\\system32\\net.exe', 'USE'))
   329             mappedDrives = list()
   330             for line in result[1].splitlines():
   331                 if 'USB' in line or 'Download' in line:
   332                     parts = line.split()
   333                     mappedDrives.append(parts[1])
   334             
   335             logger.info(mappedDrives)
   336             logger.info(self.drive)
   337             if self.drive not in mappedDrives:
   338                 self.running = False
   339             else:
   340                 command = 'USE ' + self.drive + ' /DELETE /YES' 
   341                 result = Cygwin.checkResult(Cygwin.execute('C:\\Windows\\system32\\net.exe', command)) 
   342                 if string.find(str(result[1]), 'successfully',) == -1:
   343                     logger.error(result[2])
   344                     continue
   345                         
   346 
   347 class os_netumount:
   348     
   349     """OpenSecurity '/netumount' handler"""
   350     
   351     def GET(self):
   352         # pick the arguments
   353         args = web.input()
   354         
   355         # we _need_ a drive_letter
   356         if not "drive_letter" in args:
   357             raise web.badrequest('no drive_letter given')
   358         
   359         driveHandler = UmountNetworkDriveHandler(args['drive_letter'])
   360         driveHandler.start()
   361         driveHandler.join(None)
   362         return 'Ok'
   363     
   364 
   365 class os_root:
   366 
   367     """OpenSecurity '/' handler"""
   368     
   369     def GET(self):
   370     
   371         res = "OpenSecurity-Client RESTFul Server { \"version\": \"%s\" }" % opensecurity.__version__
   372         
   373         # add some sample links
   374         res = res + """
   375         
   376 USAGE EXAMPLES:
   377         
   378 Request a password: 
   379     (copy paste this into your browser's address field after the host:port)
   380     
   381     /password?text=Give+me+a+password+for+device+%22My+USB+Drive%22+(ID%3A+32090-AAA-X0)
   382     
   383     (eg.: http://127.0.0.1:8090/password?text=Give+me+a+password+for+device+%22My+USB+Drive%22+(ID%3A+32090-AAA-X0))
   384     NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
   385     
   386     
   387 Request a combination of user and password:
   388     (copy paste this into your browser's address field after the host:port)
   389     
   390     /credentials?text=Tell+the+NSA+which+credentials+to+use+in+order+to+avoid+hacking+noise+on+wire.
   391     
   392     (eg.: http://127.0.0.1:8090/credentials?text=Tell+the+NSA+which+credentials+to+use+in+order+to+avoid+hacking+noise+on+wire.)
   393     NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
   394     
   395 
   396 Request a combination of password and keyfile:
   397     (copy paste this into your browser's address field after the host:port)
   398     
   399     /keyfile?text=Your%20private%20RSA%20Keyfile%3A
   400     
   401     (eg.: http://127.0.0.1:8090//keyfile?text=Your%20private%20RSA%20Keyfile%3A)
   402     NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
   403     
   404 
   405 Start a Browser:
   406     (copy paste this into your browser's address field after the host:port)
   407 
   408     /application?vm=Debian+7&app=Browser
   409 
   410     (e.g. http://127.0.0.1:8090/application?vm=Debian+7&app=Browser)
   411         """
   412     
   413         return res
   414 
   415 
   416 class ProcessResultBouncer(threading.Thread):
   417 
   418     """A class to post the result of a given process - assuming it to be in JSON - to a REST Api."""
   419 
   420     def __init__(self, process, remote_ip, resource): 
   421 
   422         """ctor"""
   423 
   424         threading.Thread.__init__(self)
   425         self._process = process
   426         self._remote_ip = remote_ip
   427         self._resource = resource
   428  
   429     
   430     def stop(self):
   431 
   432         """stop thread"""
   433         self.running = False
   434         
   435     
   436     def run(self):
   437 
   438         """run the thread"""
   439 
   440         # invoke the user dialog as a subprocess
   441         result = self._process.communicate()[0]
   442         if self._process.returncode != 0:
   443             print 'user request has been aborted.'
   444             return
   445         
   446         # all ok, tell send request back appropriate destination
   447         try:
   448             j = json.loads(result)
   449         except:
   450             print 'error in password parsing'
   451             return
   452         
   453         # by provided a 'data' we turn this into a POST statement
   454         url_addr = 'http://' + self._remote_ip + ':58080' + self._resource
   455         req = urllib2.Request(url_addr, urllib.urlencode(j))
   456         try:
   457             res = urllib2.urlopen(req)
   458         except:
   459             print 'failed to contact: ' + url_addr
   460             return 
   461 
   462 
   463 class RESTServerThread(threading.Thread):
   464 
   465     """Thread for serving the REST API."""
   466 
   467     def __init__(self, port): 
   468 
   469         """ctor"""
   470         threading.Thread.__init__(self)
   471         self._port = port 
   472     
   473     def stop(self):
   474 
   475         """stop thread"""
   476         self.running = False
   477         
   478     
   479     def run(self):
   480 
   481         """run the thread"""
   482         _serve(self._port)
   483 
   484 
   485 
   486 def is_already_running(port = 8090):
   487 
   488     """check if this is started twice"""
   489 
   490     try:
   491         s = socket.create_connection(('127.0.0.1', port), 0.5)
   492     except:
   493         return False
   494 
   495     return True
   496 
   497 
   498 def _bounce_vm_logs():
   499 
   500     """grab all logs from the VMs and push them to the log servers"""
   501 
   502     global log_file_lock
   503 
   504     # pick the highest current number
   505     cur = 0
   506     for f in glob.iglob(os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.*')):
   507         try:
   508             n = f.split('.')[-1:][0]
   509             if cur < int(n):
   510                 cur = int(n)
   511         except:
   512             pass
   513 
   514     cur = cur + 1
   515 
   516     # first add new vm logs to our existing one: rename the log file
   517     log_file_name_new = os.path.join(Environment('OpenSecurity').log_path, 'vm_new.log')
   518     log_file_name_cur = os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.' + str(cur))
   519     log_file_lock.acquire()
   520     try:
   521         os.rename(log_file_name_new, log_file_name_cur)
   522         print('new log file: ' + log_file_name_cur)
   523     except:
   524         pass
   525     log_file_lock.release()
   526 
   527     # now we have a list of next log files to dump
   528     log_files = glob.glob(os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.*'))
   529     log_files.sort()
   530     for log_file in log_files:
   531 
   532         try:
   533             f = open(log_file, 'rb')
   534             while True:
   535                 l = pickle.load(f)
   536                 _push_log(l)
   537 
   538         except EOFError:
   539 
   540             try:
   541                 os.remove(log_file)
   542             except:
   543                 logger.warning('tried to delete log file (pushed to EOF) "' + log_file + '" but failed')
   544 
   545         except:
   546             logger.warning('encountered error while pushing log file "' + log_file + '"')
   547             break
   548 
   549     # start bouncer again ...
   550     global log_file_bouncer
   551     log_file_bouncer = threading.Timer(5.0, _bounce_vm_logs)
   552     log_file_bouncer.start()
   553 
   554 
   555 def _push_log(log):
   556     """POST a single log to log server
   557 
   558     @param  log     the log POST param
   559     """
   560 
   561     log_server_url = "http://extern.x-net.at/opensecurity/log"
   562     try:
   563         key = win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE, 'SOFTWARE\OpenSecurity')
   564         log_server_url = str(win32api.RegQueryValueEx(key, 'LogServerURL')[0])
   565         win32api.RegCloseKey(key)
   566     except:
   567         logger.warning('Cannot open Registry HKEY_LOCAL_MACHINE\SOFTWARE\OpenSecurity and get "LogServerURL" value, using default instead')
   568 
   569     # by provided a 'data' we turn this into a POST statement
   570     d = urllib.urlencode(log)
   571     req = urllib2.Request(log_server_url, d)
   572     urllib2.urlopen(req)
   573     logger.debug('pushed log to server: ' + str(log_server_url))
   574 
   575 
   576 def _serve(port):
   577 
   578     """Start the REST server"""
   579 
   580     global server
   581 
   582     # start the VM-log bouncer timer
   583     global log_file_bouncer
   584     log_file_bouncer = threading.Timer(5.0, _bounce_vm_logs)
   585     log_file_bouncer.start()
   586 
   587     # trick the web.py server 
   588     sys.argv = [__file__, str(port)]
   589     server = web.application(opensecurity_urls, globals())
   590     server.run()
   591 
   592 
   593 def serve(port = 8090, background = False):
   594 
   595     """Start serving the REST Api
   596     port ... port number to listen on
   597     background ... cease into background (spawn thread) and return immediately"""
   598 
   599     # start threaded or direct version
   600     if background == True:
   601         t = RESTServerThread(port)
   602         t.start()
   603     else:
   604         _serve(port)
   605 
   606 def stop():
   607 
   608     """Stop serving the REST Api"""
   609 
   610     global server
   611     if server is None:
   612         return
   613 
   614     global log_file_bouncer
   615     if log_file_bouncer is not None:
   616         log_file_bouncer.cancel()
   617 
   618     server.stop()
   619 
   620 # start
   621 if __name__ == "__main__":
   622     serve()
   623