1.1 --- a/OpenSecurity/bin/opensecurity_client_restful_server.py Tue May 20 11:21:48 2014 +0200
1.2 +++ b/OpenSecurity/bin/opensecurity_client_restful_server.py Thu May 22 11:00:33 2014 +0200
1.3 @@ -1,621 +1,623 @@
1.4 -#!/bin/env python
1.5 -# -*- coding: utf-8 -*-
1.6 -
1.7 -# ------------------------------------------------------------
1.8 -# opensecurity_client_restful_server
1.9 -#
1.10 -# the OpenSecurity client RESTful server
1.11 -#
1.12 -# Autor: Oliver Maurhart, <oliver.maurhart@ait.ac.at>
1.13 -#
1.14 -# Copyright (C) 2013 AIT Austrian Institute of Technology
1.15 -# AIT Austrian Institute of Technology GmbH
1.16 -# Donau-City-Strasse 1 | 1220 Vienna | Austria
1.17 -# http://www.ait.ac.at
1.18 -#
1.19 -# This program is free software; you can redistribute it and/or
1.20 -# modify it under the terms of the GNU General Public License
1.21 -# as published by the Free Software Foundation version 2.
1.22 -#
1.23 -# This program is distributed in the hope that it will be useful,
1.24 -# but WITHOUT ANY WARRANTY; without even the implied warranty of
1.25 -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1.26 -# GNU General Public License for more details.
1.27 -#
1.28 -# You should have received a copy of the GNU General Public License
1.29 -# along with this program; if not, write to the Free Software
1.30 -# Foundation, Inc., 51 Franklin Street, Fifth Floor,
1.31 -# Boston, MA 02110-1301, USA.
1.32 -# ------------------------------------------------------------
1.33 -
1.34 -
1.35 -# ------------------------------------------------------------
1.36 -# imports
1.37 -
1.38 -import getpass
1.39 -import glob
1.40 -import json
1.41 -import os
1.42 -import os.path
1.43 -import pickle
1.44 -import platform
1.45 -import socket
1.46 -import subprocess
1.47 -import sys
1.48 -import threading
1.49 -import time
1.50 -import urllib
1.51 -import urllib2
1.52 -import web
1.53 -import threading
1.54 -import time
1.55 -import string
1.56 -import win32api
1.57 -import win32con
1.58 -
1.59 -from opensecurity_util import logger, setupLogger, OpenSecurityException
1.60 -if sys.platform == 'win32' or sys.platform == 'cygwin':
1.61 - from cygwin import Cygwin
1.62 -
1.63 -# local
1.64 -import __init__ as opensecurity
1.65 -from environment import Environment
1.66 -
1.67 -
1.68 -# ------------------------------------------------------------
1.69 -# const
1.70 -
1.71 -
1.72 -"""All the URLs we know mapping to class handler"""
1.73 -opensecurity_urls = (
1.74 - '/credentials', 'os_credentials',
1.75 - '/keyfile', 'os_keyfile',
1.76 - '/log', 'os_log',
1.77 - '/notification', 'os_notification',
1.78 - '/password', 'os_password',
1.79 - '/netmount', 'os_netmount',
1.80 - '/netumount', 'os_netumount',
1.81 - '/', 'os_root'
1.82 -)
1.83 -
1.84 -
1.85 -# ------------------------------------------------------------
1.86 -# vars
1.87 -
1.88 -
1.89 -"""lock for read/write log file"""
1.90 -log_file_lock = threading.Lock()
1.91 -
1.92 -"""timer for the log file bouncer"""
1.93 -log_file_bouncer = None
1.94 -
1.95 -
1.96 -"""The REST server object"""
1.97 -server = None
1.98 -
1.99 -
1.100 -# ------------------------------------------------------------
1.101 -# code
1.102 -
1.103 -
1.104 -class os_credentials:
1.105 -
1.106 - """OpenSecurity '/credentials' handler.
1.107 -
1.108 - This is called on GET /credentials?text=TEXT.
1.109 - Ideally this should pop up a user dialog to insert his
1.110 - credentials based the given TEXT.
1.111 - """
1.112 -
1.113 - def GET(self):
1.114 -
1.115 - # pick the arguments
1.116 - args = web.input()
1.117 -
1.118 - # we _need_ a text
1.119 - if not "text" in args:
1.120 - raise web.badrequest('no text given')
1.121 -
1.122 - # remember remote ip
1.123 - remote_ip = web.ctx.environ['REMOTE_ADDR']
1.124 -
1.125 - # create the process which queries the user
1.126 - dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
1.127 - process_command = [sys.executable, dlg_image, 'credentials', args.text]
1.128 - process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)
1.129 -
1.130 - # run process result handling in seprate thread (not to block main one)
1.131 - bouncer = ProcessResultBouncer(process, remote_ip, '/credentials')
1.132 - bouncer.start()
1.133 -
1.134 - return 'user queried for credentials'
1.135 -
1.136 -
1.137 -class os_keyfile:
1.138 -
1.139 - """OpenSecurity '/keyfile' handler.
1.140 -
1.141 - This is called on GET /keyfile?text=TEXT.
1.142 - Ideally this should pop up a user dialog to insert his
1.143 - password along with a keyfile.
1.144 - """
1.145 -
1.146 - def GET(self):
1.147 -
1.148 - # pick the arguments
1.149 - args = web.input()
1.150 -
1.151 - # we _need_ a text
1.152 - if not "text" in args:
1.153 - raise web.badrequest('no text given')
1.154 -
1.155 - # remember remote ip
1.156 - remote_ip = web.ctx.environ['REMOTE_ADDR']
1.157 -
1.158 - # create the process which queries the user
1.159 - dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
1.160 - process_command = [sys.executable, dlg_image, 'keyfile', args.text]
1.161 - process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)
1.162 -
1.163 - # run process result handling in seprate thread (not to block main one)
1.164 - bouncer = ProcessResultBouncer(process, remote_ip, '/keyfile')
1.165 - bouncer.start()
1.166 -
1.167 - return 'user queried for password and keyfile'
1.168 -
1.169 -
1.170 -class os_log:
1.171 -
1.172 - """OpenSecurity '/log' handler.
1.173 -
1.174 - This is called on GET or POST on the log function /log
1.175 - """
1.176 -
1.177 - def GET(self):
1.178 -
1.179 - # pick the arguments
1.180 - self.POST()
1.181 -
1.182 -
1.183 - def POST(self):
1.184 -
1.185 - # pick the arguments
1.186 - args = web.input()
1.187 - args['user'] = getpass.getuser()
1.188 - args['system'] = platform.node() + " " + platform.system() + " " + platform.release()
1.189 -
1.190 - # add these to new data to log
1.191 - global log_file_lock
1.192 - log_file_name = os.path.join(Environment('OpenSecurity').log_path, 'vm_new.log')
1.193 - log_file_lock.acquire()
1.194 - pickle.dump(args, open(log_file_name, 'ab'))
1.195 - log_file_lock.release()
1.196 -
1.197 - return "Ok"
1.198 -
1.199 -
1.200 -class os_notification:
1.201 -
1.202 - """OpenSecurity '/notification' handler.
1.203 -
1.204 - This is called on GET /notification?msgtype=TYPE&text=TEXT.
1.205 - This will pop up an OpenSecurity notifcation window
1.206 - """
1.207 -
1.208 - def GET(self):
1.209 -
1.210 - # pick the arguments
1.211 - args = web.input()
1.212 -
1.213 - # we _need_ a type
1.214 - if not "msgtype" in args:
1.215 - raise web.badrequest('no msgtype given')
1.216 -
1.217 - if not args.msgtype in ['information', 'warning', 'critical']:
1.218 - raise web.badrequest('Unknown value for msgtype')
1.219 -
1.220 - # we _need_ a text
1.221 - if not "text" in args:
1.222 - raise web.badrequest('no text given')
1.223 -
1.224 - # invoke the user dialog as a subprocess
1.225 - dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.py')
1.226 - process_command = [sys.executable, dlg_image, 'notification-' + args.msgtype, args.text]
1.227 - process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)
1.228 -
1.229 - return "Ok"
1.230 -
1.231 -
1.232 -class os_password:
1.233 -
1.234 - """OpenSecurity '/password' handler.
1.235 -
1.236 - This is called on GET /password?text=TEXT.
1.237 - Ideally this should pop up a user dialog to insert his
1.238 - password based device name.
1.239 - """
1.240 -
1.241 - def GET(self):
1.242 -
1.243 - # pick the arguments
1.244 - args = web.input()
1.245 -
1.246 - # we _need_ a text
1.247 - if not "text" in args:
1.248 - raise web.badrequest('no text given')
1.249 -
1.250 - # remember remote ip
1.251 - remote_ip = web.ctx.environ['REMOTE_ADDR']
1.252 -
1.253 - # create the process which queries the user
1.254 - dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
1.255 - process_command = [sys.executable, dlg_image, 'password', args.text]
1.256 - process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)
1.257 -
1.258 - # run process result handling in seprate thread (not to block main one)
1.259 - bouncer = ProcessResultBouncer(process, remote_ip, '/password')
1.260 - bouncer.start()
1.261 -
1.262 - return 'user queried for password'
1.263 -
1.264 -# handles netumount request
1.265 -class MountNetworkDriveHandler(threading.Thread):
1.266 - drive = None
1.267 - resource = None
1.268 -
1.269 - def __init__(self, drv, net_path):
1.270 - threading.Thread.__init__(self)
1.271 - self.drive = drv
1.272 - self.networkPath = net_path
1.273 -
1.274 - def run(self):
1.275 - #Check for drive availability
1.276 - if os.path.exists(self.drive):
1.277 - logger.error("Drive letter is already in use: " + self.drive)
1.278 - return 1
1.279 -
1.280 - #Check for network resource availability
1.281 - retry = 5
1.282 - while not os.path.exists(self.networkPath):
1.283 - time.sleep(1)
1.284 - if retry == 0:
1.285 - return 1
1.286 - logger.info("Path not accessible: " + self.networkPath + " retrying")
1.287 - retry-=1
1.288 -
1.289 - command = 'USE ' + self.drive + ' ' + self.networkPath + ' /PERSISTENT:NO'
1.290 -
1.291 - result = Cygwin.checkResult(Cygwin.execute('C:\\Windows\\system32\\NET', command))
1.292 - if string.find(result[1], 'successfully',) == -1:
1.293 - logger.error("Failed: NET " + command)
1.294 - return 1
1.295 - return 0
1.296 -
1.297 -class os_netmount:
1.298 -
1.299 - """OpenSecurity '/netmount' handler"""
1.300 -
1.301 - def GET(self):
1.302 - # pick the arguments
1.303 - args = web.input()
1.304 -
1.305 - # we _need_ a net_resource
1.306 - if not "net_resource" in args:
1.307 - raise web.badrequest('no net_resource given')
1.308 -
1.309 - # we _need_ a drive_letter
1.310 - if not "drive_letter" in args:
1.311 - raise web.badrequest('no drive_letter given')
1.312 -
1.313 - driveHandler = MountNetworkDriveHandler(args['drive_letter'], args['net_resource'])
1.314 - driveHandler.start()
1.315 - return 'Ok'
1.316 -
1.317 -
1.318 -
1.319 -# handles netumount request
1.320 -class UmountNetworkDriveHandler(threading.Thread):
1.321 - drive = None
1.322 - running = True
1.323 -
1.324 - def __init__(self, drv):
1.325 - threading.Thread.__init__(self)
1.326 - self.drive = drv
1.327 -
1.328 - def run(self):
1.329 - while self.running:
1.330 - result = Cygwin.checkResult(Cygwin.execute('C:\\Windows\\system32\\net.exe', 'USE'))
1.331 - mappedDrives = list()
1.332 - for line in result[1].splitlines():
1.333 - if 'USB' in line or 'Download' in line:
1.334 - parts = line.split()
1.335 - mappedDrives.append(parts[1])
1.336 -
1.337 - logger.info(mappedDrives)
1.338 - logger.info(self.drive)
1.339 - if self.drive not in mappedDrives:
1.340 - self.running = False
1.341 - else:
1.342 - command = 'USE ' + self.drive + ' /DELETE /YES'
1.343 - result = Cygwin.checkResult(Cygwin.execute('C:\\Windows\\system32\\net.exe', command))
1.344 - if string.find(str(result[1]), 'successfully',) == -1:
1.345 - logger.error(result[2])
1.346 - continue
1.347 -
1.348 -
1.349 -class os_netumount:
1.350 -
1.351 - """OpenSecurity '/netumount' handler"""
1.352 -
1.353 - def GET(self):
1.354 - # pick the arguments
1.355 - args = web.input()
1.356 -
1.357 - # we _need_ a drive_letter
1.358 - if not "drive_letter" in args:
1.359 - raise web.badrequest('no drive_letter given')
1.360 -
1.361 - driveHandler = UmountNetworkDriveHandler(args['drive_letter'])
1.362 - driveHandler.start()
1.363 - return 'Ok'
1.364 -
1.365 -
1.366 -class os_root:
1.367 -
1.368 - """OpenSecurity '/' handler"""
1.369 -
1.370 - def GET(self):
1.371 -
1.372 - res = "OpenSecurity-Client RESTFul Server { \"version\": \"%s\" }" % opensecurity.__version__
1.373 -
1.374 - # add some sample links
1.375 - res = res + """
1.376 -
1.377 -USAGE EXAMPLES:
1.378 -
1.379 -Request a password:
1.380 - (copy paste this into your browser's address field after the host:port)
1.381 -
1.382 - /password?text=Give+me+a+password+for+device+%22My+USB+Drive%22+(ID%3A+32090-AAA-X0)
1.383 -
1.384 - (eg.: http://127.0.0.1:8090/password?text=Give+me+a+password+for+device+%22My+USB+Drive%22+(ID%3A+32090-AAA-X0))
1.385 - NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
1.386 -
1.387 -
1.388 -Request a combination of user and password:
1.389 - (copy paste this into your browser's address field after the host:port)
1.390 -
1.391 - /credentials?text=Tell+the+NSA+which+credentials+to+use+in+order+to+avoid+hacking+noise+on+wire.
1.392 -
1.393 - (eg.: http://127.0.0.1:8090/credentials?text=Tell+the+NSA+which+credentials+to+use+in+order+to+avoid+hacking+noise+on+wire.)
1.394 - NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
1.395 -
1.396 -
1.397 -Request a combination of password and keyfile:
1.398 - (copy paste this into your browser's address field after the host:port)
1.399 -
1.400 - /keyfile?text=Your%20private%20RSA%20Keyfile%3A
1.401 -
1.402 - (eg.: http://127.0.0.1:8090//keyfile?text=Your%20private%20RSA%20Keyfile%3A)
1.403 - NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
1.404 -
1.405 -
1.406 -Start a Browser:
1.407 - (copy paste this into your browser's address field after the host:port)
1.408 -
1.409 - /application?vm=Debian+7&app=Browser
1.410 -
1.411 - (e.g. http://127.0.0.1:8090/application?vm=Debian+7&app=Browser)
1.412 - """
1.413 -
1.414 - return res
1.415 -
1.416 -
1.417 -class ProcessResultBouncer(threading.Thread):
1.418 -
1.419 - """A class to post the result of a given process - assuming it to be in JSON - to a REST Api."""
1.420 -
1.421 - def __init__(self, process, remote_ip, resource):
1.422 -
1.423 - """ctor"""
1.424 -
1.425 - threading.Thread.__init__(self)
1.426 - self._process = process
1.427 - self._remote_ip = remote_ip
1.428 - self._resource = resource
1.429 -
1.430 -
1.431 - def stop(self):
1.432 -
1.433 - """stop thread"""
1.434 - self.running = False
1.435 -
1.436 -
1.437 - def run(self):
1.438 -
1.439 - """run the thread"""
1.440 -
1.441 - # invoke the user dialog as a subprocess
1.442 - result = self._process.communicate()[0]
1.443 - if self._process.returncode != 0:
1.444 - print 'user request has been aborted.'
1.445 - return
1.446 -
1.447 - # all ok, tell send request back appropriate destination
1.448 - try:
1.449 - j = json.loads(result)
1.450 - except:
1.451 - print 'error in password parsing'
1.452 - return
1.453 -
1.454 - # by provided a 'data' we turn this into a POST statement
1.455 - url_addr = 'http://' + self._remote_ip + ':58080' + self._resource
1.456 - req = urllib2.Request(url_addr, urllib.urlencode(j))
1.457 - try:
1.458 - res = urllib2.urlopen(req)
1.459 - except:
1.460 - print 'failed to contact: ' + url_addr
1.461 - return
1.462 -
1.463 -
1.464 -class RESTServerThread(threading.Thread):
1.465 -
1.466 - """Thread for serving the REST API."""
1.467 -
1.468 - def __init__(self, port):
1.469 -
1.470 - """ctor"""
1.471 - threading.Thread.__init__(self)
1.472 - self._port = port
1.473 -
1.474 - def stop(self):
1.475 -
1.476 - """stop thread"""
1.477 - self.running = False
1.478 -
1.479 -
1.480 - def run(self):
1.481 -
1.482 - """run the thread"""
1.483 - _serve(self._port)
1.484 -
1.485 -
1.486 -
1.487 -def is_already_running(port = 8090):
1.488 -
1.489 - """check if this is started twice"""
1.490 -
1.491 - try:
1.492 - s = socket.create_connection(('127.0.0.1', port), 0.5)
1.493 - except:
1.494 - return False
1.495 -
1.496 - return True
1.497 -
1.498 -
1.499 -def _bounce_vm_logs():
1.500 -
1.501 - """grab all logs from the VMs and push them to the log servers"""
1.502 -
1.503 - global log_file_lock
1.504 -
1.505 - # pick the highest current number
1.506 - cur = 0
1.507 - for f in glob.iglob(os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.*')):
1.508 - try:
1.509 - n = f.split('.')[-1:][0]
1.510 - if cur < int(n):
1.511 - cur = int(n)
1.512 - except:
1.513 - pass
1.514 -
1.515 - cur = cur + 1
1.516 -
1.517 - # first add new vm logs to our existing one: rename the log file
1.518 - log_file_name_new = os.path.join(Environment('OpenSecurity').log_path, 'vm_new.log')
1.519 - log_file_name_cur = os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.' + str(cur))
1.520 - log_file_lock.acquire()
1.521 - try:
1.522 - os.rename(log_file_name_new, log_file_name_cur)
1.523 - print('new log file: ' + log_file_name_cur)
1.524 - except:
1.525 - pass
1.526 - log_file_lock.release()
1.527 -
1.528 - # now we have a list of next log files to dump
1.529 - log_files = glob.glob(os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.*'))
1.530 - log_files.sort()
1.531 - for log_file in log_files:
1.532 -
1.533 - try:
1.534 - f = open(log_file, 'rb')
1.535 - while True:
1.536 - l = pickle.load(f)
1.537 - _push_log(l)
1.538 -
1.539 - except EOFError:
1.540 -
1.541 - try:
1.542 - os.remove(log_file)
1.543 - except:
1.544 - logger.warning('tried to delete log file (pushed to EOF) "' + log_file + '" but failed')
1.545 -
1.546 - except:
1.547 - logger.warning('encountered error while pushing log file "' + log_file + '"')
1.548 - break
1.549 -
1.550 - # start bouncer again ...
1.551 - global log_file_bouncer
1.552 - log_file_bouncer = threading.Timer(5.0, _bounce_vm_logs)
1.553 - log_file_bouncer.start()
1.554 -
1.555 -
1.556 -def _push_log(log):
1.557 - """POST a single log to log server
1.558 -
1.559 - @param log the log POST param
1.560 - """
1.561 -
1.562 - try:
1.563 - key = win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE, 'SOFTWARE\OpenSecurity')
1.564 - log_server_url = str(win32api.RegQueryValueEx(key, 'LogServerURL')[0])
1.565 - win32api.RegCloseKey(key)
1.566 - except:
1.567 - logger.critical('Cannot open Registry HKEY_LOCAL_MACHINE\SOFTWARE\OpenSecurity and get LogServerURL value')
1.568 - raise
1.569 -
1.570 - # by provided a 'data' we turn this into a POST statement
1.571 - d = urllib.urlencode(log)
1.572 - req = urllib2.Request(log_server_url, d)
1.573 - urllib2.urlopen(req)
1.574 - logger.debug('pushed log to server: ' + str(log_server_url))
1.575 -
1.576 -
1.577 -def _serve(port):
1.578 -
1.579 - """Start the REST server"""
1.580 -
1.581 - global server
1.582 -
1.583 - # start the VM-log bouncer timer
1.584 - global log_file_bouncer
1.585 - log_file_bouncer = threading.Timer(5.0, _bounce_vm_logs)
1.586 - log_file_bouncer.start()
1.587 -
1.588 - # trick the web.py server
1.589 - sys.argv = [__file__, str(port)]
1.590 - server = web.application(opensecurity_urls, globals())
1.591 - server.run()
1.592 -
1.593 -
1.594 -def serve(port = 8090, background = False):
1.595 -
1.596 - """Start serving the REST Api
1.597 - port ... port number to listen on
1.598 - background ... cease into background (spawn thread) and return immediately"""
1.599 -
1.600 - # start threaded or direct version
1.601 - if background == True:
1.602 - t = RESTServerThread(port)
1.603 - t.start()
1.604 - else:
1.605 - _serve(port)
1.606 -
1.607 -def stop():
1.608 -
1.609 - """Stop serving the REST Api"""
1.610 -
1.611 - global server
1.612 - if server is None:
1.613 - return
1.614 -
1.615 - global log_file_bouncer
1.616 - if log_file_bouncer is not None:
1.617 - log_file_bouncer.cancel()
1.618 -
1.619 - server.stop()
1.620 -
1.621 -# start
1.622 -if __name__ == "__main__":
1.623 - serve()
1.624 -
1.625 +#!/bin/env python
1.626 +# -*- coding: utf-8 -*-
1.627 +
1.628 +# ------------------------------------------------------------
1.629 +# opensecurity_client_restful_server
1.630 +#
1.631 +# the OpenSecurity client RESTful server
1.632 +#
1.633 +# Autor: Oliver Maurhart, <oliver.maurhart@ait.ac.at>
1.634 +#
1.635 +# Copyright (C) 2013 AIT Austrian Institute of Technology
1.636 +# AIT Austrian Institute of Technology GmbH
1.637 +# Donau-City-Strasse 1 | 1220 Vienna | Austria
1.638 +# http://www.ait.ac.at
1.639 +#
1.640 +# This program is free software; you can redistribute it and/or
1.641 +# modify it under the terms of the GNU General Public License
1.642 +# as published by the Free Software Foundation version 2.
1.643 +#
1.644 +# This program is distributed in the hope that it will be useful,
1.645 +# but WITHOUT ANY WARRANTY; without even the implied warranty of
1.646 +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1.647 +# GNU General Public License for more details.
1.648 +#
1.649 +# You should have received a copy of the GNU General Public License
1.650 +# along with this program; if not, write to the Free Software
1.651 +# Foundation, Inc., 51 Franklin Street, Fifth Floor,
1.652 +# Boston, MA 02110-1301, USA.
1.653 +# ------------------------------------------------------------
1.654 +
1.655 +
1.656 +# ------------------------------------------------------------
1.657 +# imports
1.658 +
1.659 +import getpass
1.660 +import glob
1.661 +import json
1.662 +import os
1.663 +import os.path
1.664 +import pickle
1.665 +import platform
1.666 +import socket
1.667 +import subprocess
1.668 +import sys
1.669 +import threading
1.670 +import time
1.671 +import urllib
1.672 +import urllib2
1.673 +import web
1.674 +import threading
1.675 +import time
1.676 +import string
1.677 +import win32api
1.678 +import win32con
1.679 +
1.680 +from opensecurity_util import logger, setupLogger, OpenSecurityException
1.681 +if sys.platform == 'win32' or sys.platform == 'cygwin':
1.682 + from cygwin import Cygwin
1.683 +
1.684 +# local
1.685 +import __init__ as opensecurity
1.686 +from environment import Environment
1.687 +
1.688 +
1.689 +# ------------------------------------------------------------
1.690 +# const
1.691 +
1.692 +
1.693 +"""All the URLs we know mapping to class handler"""
1.694 +opensecurity_urls = (
1.695 + '/credentials', 'os_credentials',
1.696 + '/keyfile', 'os_keyfile',
1.697 + '/log', 'os_log',
1.698 + '/notification', 'os_notification',
1.699 + '/password', 'os_password',
1.700 + '/netmount', 'os_netmount',
1.701 + '/netumount', 'os_netumount',
1.702 + '/', 'os_root'
1.703 +)
1.704 +
1.705 +
1.706 +# ------------------------------------------------------------
1.707 +# vars
1.708 +
1.709 +
1.710 +"""lock for read/write log file"""
1.711 +log_file_lock = threading.Lock()
1.712 +
1.713 +"""timer for the log file bouncer"""
1.714 +log_file_bouncer = None
1.715 +
1.716 +
1.717 +"""The REST server object"""
1.718 +server = None
1.719 +
1.720 +
1.721 +# ------------------------------------------------------------
1.722 +# code
1.723 +
1.724 +
1.725 +class os_credentials:
1.726 +
1.727 + """OpenSecurity '/credentials' handler.
1.728 +
1.729 + This is called on GET /credentials?text=TEXT.
1.730 + Ideally this should pop up a user dialog to insert his
1.731 + credentials based the given TEXT.
1.732 + """
1.733 +
1.734 + def GET(self):
1.735 +
1.736 + # pick the arguments
1.737 + args = web.input()
1.738 +
1.739 + # we _need_ a text
1.740 + if not "text" in args:
1.741 + raise web.badrequest('no text given')
1.742 +
1.743 + # remember remote ip
1.744 + remote_ip = web.ctx.environ['REMOTE_ADDR']
1.745 +
1.746 + # create the process which queries the user
1.747 + dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
1.748 + process_command = [sys.executable, dlg_image, 'credentials', args.text]
1.749 + process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)
1.750 +
1.751 + # run process result handling in seprate thread (not to block main one)
1.752 + bouncer = ProcessResultBouncer(process, remote_ip, '/credentials')
1.753 + bouncer.start()
1.754 +
1.755 + return 'user queried for credentials'
1.756 +
1.757 +
1.758 +class os_keyfile:
1.759 +
1.760 + """OpenSecurity '/keyfile' handler.
1.761 +
1.762 + This is called on GET /keyfile?text=TEXT.
1.763 + Ideally this should pop up a user dialog to insert his
1.764 + password along with a keyfile.
1.765 + """
1.766 +
1.767 + def GET(self):
1.768 +
1.769 + # pick the arguments
1.770 + args = web.input()
1.771 +
1.772 + # we _need_ a text
1.773 + if not "text" in args:
1.774 + raise web.badrequest('no text given')
1.775 +
1.776 + # remember remote ip
1.777 + remote_ip = web.ctx.environ['REMOTE_ADDR']
1.778 +
1.779 + # create the process which queries the user
1.780 + dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
1.781 + process_command = [sys.executable, dlg_image, 'keyfile', args.text]
1.782 + process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)
1.783 +
1.784 + # run process result handling in seprate thread (not to block main one)
1.785 + bouncer = ProcessResultBouncer(process, remote_ip, '/keyfile')
1.786 + bouncer.start()
1.787 +
1.788 + return 'user queried for password and keyfile'
1.789 +
1.790 +
1.791 +class os_log:
1.792 +
1.793 + """OpenSecurity '/log' handler.
1.794 +
1.795 + This is called on GET or POST on the log function /log
1.796 + """
1.797 +
1.798 + def GET(self):
1.799 +
1.800 + # pick the arguments
1.801 + self.POST()
1.802 +
1.803 +
1.804 + def POST(self):
1.805 +
1.806 + # pick the arguments
1.807 + args = web.input()
1.808 + args['user'] = getpass.getuser()
1.809 + args['system'] = platform.node() + " " + platform.system() + " " + platform.release()
1.810 +
1.811 + # add these to new data to log
1.812 + global log_file_lock
1.813 + log_file_name = os.path.join(Environment('OpenSecurity').log_path, 'vm_new.log')
1.814 + log_file_lock.acquire()
1.815 + pickle.dump(args, open(log_file_name, 'ab'))
1.816 + log_file_lock.release()
1.817 +
1.818 + return "Ok"
1.819 +
1.820 +
1.821 +class os_notification:
1.822 +
1.823 + """OpenSecurity '/notification' handler.
1.824 +
1.825 + This is called on GET /notification?msgtype=TYPE&text=TEXT.
1.826 + This will pop up an OpenSecurity notifcation window
1.827 + """
1.828 +
1.829 + def GET(self):
1.830 +
1.831 + # pick the arguments
1.832 + args = web.input()
1.833 +
1.834 + # we _need_ a type
1.835 + if not "msgtype" in args:
1.836 + raise web.badrequest('no msgtype given')
1.837 +
1.838 + if not args.msgtype in ['information', 'warning', 'critical']:
1.839 + raise web.badrequest('Unknown value for msgtype')
1.840 +
1.841 + # we _need_ a text
1.842 + if not "text" in args:
1.843 + raise web.badrequest('no text given')
1.844 +
1.845 + # invoke the user dialog as a subprocess
1.846 + dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.py')
1.847 + process_command = [sys.executable, dlg_image, 'notification-' + args.msgtype, args.text]
1.848 + process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)
1.849 +
1.850 + return "Ok"
1.851 +
1.852 +
1.853 +class os_password:
1.854 +
1.855 + """OpenSecurity '/password' handler.
1.856 +
1.857 + This is called on GET /password?text=TEXT.
1.858 + Ideally this should pop up a user dialog to insert his
1.859 + password based device name.
1.860 + """
1.861 +
1.862 + def GET(self):
1.863 +
1.864 + # pick the arguments
1.865 + args = web.input()
1.866 +
1.867 + # we _need_ a text
1.868 + if not "text" in args:
1.869 + raise web.badrequest('no text given')
1.870 +
1.871 + # remember remote ip
1.872 + remote_ip = web.ctx.environ['REMOTE_ADDR']
1.873 +
1.874 + # create the process which queries the user
1.875 + dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
1.876 + process_command = [sys.executable, dlg_image, 'password', args.text]
1.877 + process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)
1.878 +
1.879 + # run process result handling in seprate thread (not to block main one)
1.880 + bouncer = ProcessResultBouncer(process, remote_ip, '/password')
1.881 + bouncer.start()
1.882 +
1.883 + return 'user queried for password'
1.884 +
1.885 +# handles netumount request
1.886 +class MountNetworkDriveHandler(threading.Thread):
1.887 + drive = None
1.888 + resource = None
1.889 +
1.890 + def __init__(self, drv, net_path):
1.891 + threading.Thread.__init__(self)
1.892 + self.drive = drv
1.893 + self.networkPath = net_path
1.894 +
1.895 + def run(self):
1.896 + #Check for drive availability
1.897 + if os.path.exists(self.drive):
1.898 + logger.error("Drive letter is already in use: " + self.drive)
1.899 + return 1
1.900 +
1.901 + #Check for network resource availability
1.902 + retry = 5
1.903 + while not os.path.exists(self.networkPath):
1.904 + time.sleep(1)
1.905 + if retry == 0:
1.906 + return 1
1.907 + logger.info("Path not accessible: " + self.networkPath + " retrying")
1.908 + retry-=1
1.909 +
1.910 + command = 'USE ' + self.drive + ' ' + self.networkPath + ' /PERSISTENT:NO'
1.911 +
1.912 + result = Cygwin.checkResult(Cygwin.execute('C:\\Windows\\system32\\NET', command))
1.913 + if string.find(result[1], 'successfully',) == -1:
1.914 + logger.error("Failed: NET " + command)
1.915 + return 1
1.916 + return 0
1.917 +
1.918 +class os_netmount:
1.919 +
1.920 + """OpenSecurity '/netmount' handler"""
1.921 +
1.922 + def GET(self):
1.923 + # pick the arguments
1.924 + args = web.input()
1.925 +
1.926 + # we _need_ a net_resource
1.927 + if not "net_resource" in args:
1.928 + raise web.badrequest('no net_resource given')
1.929 +
1.930 + # we _need_ a drive_letter
1.931 + if not "drive_letter" in args:
1.932 + raise web.badrequest('no drive_letter given')
1.933 +
1.934 + driveHandler = MountNetworkDriveHandler(args['drive_letter'], args['net_resource'])
1.935 + driveHandler.start()
1.936 + driveHandler.join(None)
1.937 + return 'Ok'
1.938 +
1.939 +
1.940 +
1.941 +# handles netumount request
1.942 +class UmountNetworkDriveHandler(threading.Thread):
1.943 + drive = None
1.944 + running = True
1.945 +
1.946 + def __init__(self, drv):
1.947 + threading.Thread.__init__(self)
1.948 + self.drive = drv
1.949 +
1.950 + def run(self):
1.951 + while self.running:
1.952 + result = Cygwin.checkResult(Cygwin.execute('C:\\Windows\\system32\\net.exe', 'USE'))
1.953 + mappedDrives = list()
1.954 + for line in result[1].splitlines():
1.955 + if 'USB' in line or 'Download' in line:
1.956 + parts = line.split()
1.957 + mappedDrives.append(parts[1])
1.958 +
1.959 + logger.info(mappedDrives)
1.960 + logger.info(self.drive)
1.961 + if self.drive not in mappedDrives:
1.962 + self.running = False
1.963 + else:
1.964 + command = 'USE ' + self.drive + ' /DELETE /YES'
1.965 + result = Cygwin.checkResult(Cygwin.execute('C:\\Windows\\system32\\net.exe', command))
1.966 + if string.find(str(result[1]), 'successfully',) == -1:
1.967 + logger.error(result[2])
1.968 + continue
1.969 +
1.970 +
1.971 +class os_netumount:
1.972 +
1.973 + """OpenSecurity '/netumount' handler"""
1.974 +
1.975 + def GET(self):
1.976 + # pick the arguments
1.977 + args = web.input()
1.978 +
1.979 + # we _need_ a drive_letter
1.980 + if not "drive_letter" in args:
1.981 + raise web.badrequest('no drive_letter given')
1.982 +
1.983 + driveHandler = UmountNetworkDriveHandler(args['drive_letter'])
1.984 + driveHandler.start()
1.985 + driveHandler.join(None)
1.986 + return 'Ok'
1.987 +
1.988 +
1.989 +class os_root:
1.990 +
1.991 + """OpenSecurity '/' handler"""
1.992 +
1.993 + def GET(self):
1.994 +
1.995 + res = "OpenSecurity-Client RESTFul Server { \"version\": \"%s\" }" % opensecurity.__version__
1.996 +
1.997 + # add some sample links
1.998 + res = res + """
1.999 +
1.1000 +USAGE EXAMPLES:
1.1001 +
1.1002 +Request a password:
1.1003 + (copy paste this into your browser's address field after the host:port)
1.1004 +
1.1005 + /password?text=Give+me+a+password+for+device+%22My+USB+Drive%22+(ID%3A+32090-AAA-X0)
1.1006 +
1.1007 + (eg.: http://127.0.0.1:8090/password?text=Give+me+a+password+for+device+%22My+USB+Drive%22+(ID%3A+32090-AAA-X0))
1.1008 + NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
1.1009 +
1.1010 +
1.1011 +Request a combination of user and password:
1.1012 + (copy paste this into your browser's address field after the host:port)
1.1013 +
1.1014 + /credentials?text=Tell+the+NSA+which+credentials+to+use+in+order+to+avoid+hacking+noise+on+wire.
1.1015 +
1.1016 + (eg.: http://127.0.0.1:8090/credentials?text=Tell+the+NSA+which+credentials+to+use+in+order+to+avoid+hacking+noise+on+wire.)
1.1017 + NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
1.1018 +
1.1019 +
1.1020 +Request a combination of password and keyfile:
1.1021 + (copy paste this into your browser's address field after the host:port)
1.1022 +
1.1023 + /keyfile?text=Your%20private%20RSA%20Keyfile%3A
1.1024 +
1.1025 + (eg.: http://127.0.0.1:8090//keyfile?text=Your%20private%20RSA%20Keyfile%3A)
1.1026 + NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
1.1027 +
1.1028 +
1.1029 +Start a Browser:
1.1030 + (copy paste this into your browser's address field after the host:port)
1.1031 +
1.1032 + /application?vm=Debian+7&app=Browser
1.1033 +
1.1034 + (e.g. http://127.0.0.1:8090/application?vm=Debian+7&app=Browser)
1.1035 + """
1.1036 +
1.1037 + return res
1.1038 +
1.1039 +
1.1040 +class ProcessResultBouncer(threading.Thread):
1.1041 +
1.1042 + """A class to post the result of a given process - assuming it to be in JSON - to a REST Api."""
1.1043 +
1.1044 + def __init__(self, process, remote_ip, resource):
1.1045 +
1.1046 + """ctor"""
1.1047 +
1.1048 + threading.Thread.__init__(self)
1.1049 + self._process = process
1.1050 + self._remote_ip = remote_ip
1.1051 + self._resource = resource
1.1052 +
1.1053 +
1.1054 + def stop(self):
1.1055 +
1.1056 + """stop thread"""
1.1057 + self.running = False
1.1058 +
1.1059 +
1.1060 + def run(self):
1.1061 +
1.1062 + """run the thread"""
1.1063 +
1.1064 + # invoke the user dialog as a subprocess
1.1065 + result = self._process.communicate()[0]
1.1066 + if self._process.returncode != 0:
1.1067 + print 'user request has been aborted.'
1.1068 + return
1.1069 +
1.1070 + # all ok, tell send request back appropriate destination
1.1071 + try:
1.1072 + j = json.loads(result)
1.1073 + except:
1.1074 + print 'error in password parsing'
1.1075 + return
1.1076 +
1.1077 + # by provided a 'data' we turn this into a POST statement
1.1078 + url_addr = 'http://' + self._remote_ip + ':58080' + self._resource
1.1079 + req = urllib2.Request(url_addr, urllib.urlencode(j))
1.1080 + try:
1.1081 + res = urllib2.urlopen(req)
1.1082 + except:
1.1083 + print 'failed to contact: ' + url_addr
1.1084 + return
1.1085 +
1.1086 +
1.1087 +class RESTServerThread(threading.Thread):
1.1088 +
1.1089 + """Thread for serving the REST API."""
1.1090 +
1.1091 + def __init__(self, port):
1.1092 +
1.1093 + """ctor"""
1.1094 + threading.Thread.__init__(self)
1.1095 + self._port = port
1.1096 +
1.1097 + def stop(self):
1.1098 +
1.1099 + """stop thread"""
1.1100 + self.running = False
1.1101 +
1.1102 +
1.1103 + def run(self):
1.1104 +
1.1105 + """run the thread"""
1.1106 + _serve(self._port)
1.1107 +
1.1108 +
1.1109 +
1.1110 +def is_already_running(port = 8090):
1.1111 +
1.1112 + """check if this is started twice"""
1.1113 +
1.1114 + try:
1.1115 + s = socket.create_connection(('127.0.0.1', port), 0.5)
1.1116 + except:
1.1117 + return False
1.1118 +
1.1119 + return True
1.1120 +
1.1121 +
1.1122 +def _bounce_vm_logs():
1.1123 +
1.1124 + """grab all logs from the VMs and push them to the log servers"""
1.1125 +
1.1126 + global log_file_lock
1.1127 +
1.1128 + # pick the highest current number
1.1129 + cur = 0
1.1130 + for f in glob.iglob(os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.*')):
1.1131 + try:
1.1132 + n = f.split('.')[-1:][0]
1.1133 + if cur < int(n):
1.1134 + cur = int(n)
1.1135 + except:
1.1136 + pass
1.1137 +
1.1138 + cur = cur + 1
1.1139 +
1.1140 + # first add new vm logs to our existing one: rename the log file
1.1141 + log_file_name_new = os.path.join(Environment('OpenSecurity').log_path, 'vm_new.log')
1.1142 + log_file_name_cur = os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.' + str(cur))
1.1143 + log_file_lock.acquire()
1.1144 + try:
1.1145 + os.rename(log_file_name_new, log_file_name_cur)
1.1146 + print('new log file: ' + log_file_name_cur)
1.1147 + except:
1.1148 + pass
1.1149 + log_file_lock.release()
1.1150 +
1.1151 + # now we have a list of next log files to dump
1.1152 + log_files = glob.glob(os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.*'))
1.1153 + log_files.sort()
1.1154 + for log_file in log_files:
1.1155 +
1.1156 + try:
1.1157 + f = open(log_file, 'rb')
1.1158 + while True:
1.1159 + l = pickle.load(f)
1.1160 + _push_log(l)
1.1161 +
1.1162 + except EOFError:
1.1163 +
1.1164 + try:
1.1165 + os.remove(log_file)
1.1166 + except:
1.1167 + logger.warning('tried to delete log file (pushed to EOF) "' + log_file + '" but failed')
1.1168 +
1.1169 + except:
1.1170 + logger.warning('encountered error while pushing log file "' + log_file + '"')
1.1171 + break
1.1172 +
1.1173 + # start bouncer again ...
1.1174 + global log_file_bouncer
1.1175 + log_file_bouncer = threading.Timer(5.0, _bounce_vm_logs)
1.1176 + log_file_bouncer.start()
1.1177 +
1.1178 +
1.1179 +def _push_log(log):
1.1180 + """POST a single log to log server
1.1181 +
1.1182 + @param log the log POST param
1.1183 + """
1.1184 +
1.1185 + try:
1.1186 + key = win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE, 'SOFTWARE\OpenSecurity')
1.1187 + log_server_url = str(win32api.RegQueryValueEx(key, 'LogServerURL')[0])
1.1188 + win32api.RegCloseKey(key)
1.1189 + except:
1.1190 + logger.critical('Cannot open Registry HKEY_LOCAL_MACHINE\SOFTWARE\OpenSecurity and get LogServerURL value')
1.1191 + raise
1.1192 +
1.1193 + # by provided a 'data' we turn this into a POST statement
1.1194 + d = urllib.urlencode(log)
1.1195 + req = urllib2.Request(log_server_url, d)
1.1196 + urllib2.urlopen(req)
1.1197 + logger.debug('pushed log to server: ' + str(log_server_url))
1.1198 +
1.1199 +
1.1200 +def _serve(port):
1.1201 +
1.1202 + """Start the REST server"""
1.1203 +
1.1204 + global server
1.1205 +
1.1206 + # start the VM-log bouncer timer
1.1207 + global log_file_bouncer
1.1208 + log_file_bouncer = threading.Timer(5.0, _bounce_vm_logs)
1.1209 + log_file_bouncer.start()
1.1210 +
1.1211 + # trick the web.py server
1.1212 + sys.argv = [__file__, str(port)]
1.1213 + server = web.application(opensecurity_urls, globals())
1.1214 + server.run()
1.1215 +
1.1216 +
1.1217 +def serve(port = 8090, background = False):
1.1218 +
1.1219 + """Start serving the REST Api
1.1220 + port ... port number to listen on
1.1221 + background ... cease into background (spawn thread) and return immediately"""
1.1222 +
1.1223 + # start threaded or direct version
1.1224 + if background == True:
1.1225 + t = RESTServerThread(port)
1.1226 + t.start()
1.1227 + else:
1.1228 + _serve(port)
1.1229 +
1.1230 +def stop():
1.1231 +
1.1232 + """Stop serving the REST Api"""
1.1233 +
1.1234 + global server
1.1235 + if server is None:
1.1236 + return
1.1237 +
1.1238 + global log_file_bouncer
1.1239 + if log_file_bouncer is not None:
1.1240 + log_file_bouncer.cancel()
1.1241 +
1.1242 + server.stop()
1.1243 +
1.1244 +# start
1.1245 +if __name__ == "__main__":
1.1246 + serve()
1.1247 +