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