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