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