some last fixes ... for 0.2.7
2 # -*- coding: utf-8 -*-
4 # ------------------------------------------------------------
5 # opensecurity_client_restful_server
7 # the OpenSecurity client RESTful server
9 # Autor: Oliver Maurhart, <oliver.maurhart@ait.ac.at>
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
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.
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.
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 # ------------------------------------------------------------
32 # ------------------------------------------------------------
60 from PyQt4 import QtGui
62 from opensecurity_util import logger, setupLogger, OpenSecurityException
63 if sys.platform == 'win32' or sys.platform == 'cygwin':
64 from cygwin import Cygwin
67 import __init__ as opensecurity
68 from environment import Environment
71 # ------------------------------------------------------------
75 """All the URLs we know mapping to class handler"""
77 '/credentials', 'os_credentials',
78 '/keyfile', 'os_keyfile',
80 '/message', 'os_message',
81 '/notification', 'os_notification',
82 '/password', 'os_password',
83 '/netmount', 'os_netmount',
84 '/netumount', 'os_netumount',
85 '/netcleanup', 'os_netcleanup',
90 # ------------------------------------------------------------
94 """lock for read/write log file"""
95 log_file_lock = threading.Lock()
97 """timer for the log file bouncer"""
98 log_file_bouncer = None
100 """The REST server object"""
104 """The System Tray Icon instance"""
107 # ------------------------------------------------------------
111 class os_credentials:
113 """OpenSecurity '/credentials' handler.
115 This is called on GET /credentials?text=TEXT.
116 Ideally this should pop up a user dialog to insert his
117 credentials based the given TEXT.
126 if not "text" in args:
127 raise web.badrequest('no text given')
130 remote_ip = web.ctx.environ['REMOTE_ADDR']
132 # create the process which queries the user
133 dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
134 process_command = [sys.executable, dlg_image, 'credentials', args.text]
136 # run process result handling in seprate thread (not to block main one)
137 bouncer = ProcessResultBouncer(process_command, remote_ip, '/credentials')
140 return 'user queried for credentials'
145 """OpenSecurity '/keyfile' handler.
147 This is called on GET /keyfile?text=TEXT.
148 Ideally this should pop up a user dialog to insert his
149 password along with a keyfile.
158 if not "text" in args:
159 raise web.badrequest('no text given')
162 remote_ip = web.ctx.environ['REMOTE_ADDR']
164 # create the process which queries the user
165 dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
166 process_command = [sys.executable, dlg_image, 'keyfile', args.text]
168 # run process result handling in seprate thread (not to block main one)
169 bouncer = ProcessResultBouncer(process_command, remote_ip, '/keyfile')
172 return 'user queried for password and keyfile'
177 """OpenSecurity '/log' handler.
179 This is called on GET or POST on the log function /log
192 args['user'] = getpass.getuser()
193 args['system'] = platform.node() + " " + platform.system() + " " + platform.release()
195 # add these to new data to log
197 log_file_name = os.path.join(Environment('OpenSecurity').log_path, 'vm_new.log')
198 log_file_lock.acquire()
199 pickle.dump(args, open(log_file_name, 'ab'))
200 log_file_lock.release()
207 """OpenSecurity '/message' handler.
209 This is called on GET /message?text=TEXTi&timeout=TIMEOUT.
210 This pops up the typical tray message (like a ballon on windows).
222 if not "text" in args:
223 raise web.badrequest('no text given')
226 if "timeout" in args:
228 timeout=int(args.timeout)
232 if tray_icon is None:
233 raise web.badrequest('unable to access tray icon instance')
235 tray_icon.showMessage('OpenSecurity', args.text, QtGui.QSystemTrayIcon.Information, timeout)
236 return 'Shown: ' + args.text + ' timeout: ' + str(timeout) + ' ms'
239 class os_notification:
241 """OpenSecurity '/notification' handler.
243 This is called on GET /notification?msgtype=TYPE&text=TEXT.
244 This will pop up an OpenSecurity notifcation window
256 if not "msgtype" in args:
257 raise web.badrequest('no msgtype given')
259 if not args.msgtype in ['information', 'warning', 'critical']:
260 raise web.badrequest('Unknown value for msgtype')
263 if not "text" in args:
264 raise web.badrequest('no text given')
266 # invoke the user dialog as a subprocess
267 dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
268 process_command = [sys.executable, dlg_image, 'notification-' + args.msgtype, args.text]
269 process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)
276 """OpenSecurity '/password' handler.
278 This is called on GET /password?text=TEXT.
279 Ideally this should pop up a user dialog to insert his
280 password based device name.
289 if not "text" in args:
290 raise web.badrequest('no text given')
293 remote_ip = web.ctx.environ['REMOTE_ADDR']
295 # create the process which queries the user
296 dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
297 process_command = [sys.executable, dlg_image, 'password', args.text]
299 # run process result handling in seprate thread (not to block main one)
300 bouncer = ProcessResultBouncer(process_command, remote_ip, '/password')
303 return 'user queried for password'
307 def genNetworkDrive():
308 logical_drives = getLogicalDrives()
309 logger.info("Used logical drive letters: "+ str(logical_drives).strip('[]') )
310 drives = list(map(chr, range(68, 91)))
312 if drive not in logical_drives:
316 def getLogicalDrives():
317 drive_bitmask = ctypes.cdll.kernel32.GetLogicalDrives()
318 drives = list(itertools.compress(string.ascii_uppercase, map(lambda x:ord(x) - ord('0'), bin(drive_bitmask)[:1:-1])))
321 def getNetworkPath(drive):
322 return win32wnet.WNetGetConnection(drive+':')
324 def getDriveType(drive):
325 return ctypes.cdll.kernel32.GetDriveTypeW(u"%s:\\"%drive)
327 def getNetworkDrive(path):
328 for drive in getLogicalDrives():
329 #if is a network drive
330 if getDriveType(drive) == 4:
331 network_path = getNetworkPath(drive)
332 if path in network_path:
336 def mapDrive(drive, networkPath, user, password):
337 if (os.path.exists(networkPath)):
338 logger.debug(networkPath + " is found...")
339 logger.debug("Trying to map " + networkPath + " on to " + drive + " .....")
341 win32wnet.WNetAddConnection2(win32netcon.RESOURCETYPE_DISK, drive, networkPath, None, user, password)
343 logger.error("Unexpected error...")
345 logger.info("Mapping successful")
348 logger.error("Network path unreachable...")
351 mount_lock = threading.Lock()
353 # handles netumount request
354 class MountNetworkDriveHandler(threading.Thread):
356 def __init__(self, net_path):
357 threading.Thread.__init__(self)
358 self.networkPath = net_path
361 #Check for network resource availability
363 while not os.path.exists(self.networkPath):
366 logger.info("Path not accessible: " + self.networkPath + " retrying")
370 drive = genNetworkDrive()
372 logger.error("Failed to assign drive letter for: " + self.networkPath)
375 logger.info("Assigned drive " + drive + " to " + self.networkPath)
377 #Check for drive availability
379 if os.path.exists(drive):
380 logger.error("Drive letter is already in use: " + drive)
383 return mapDrive(drive, self.networkPath, "", "")
387 """OpenSecurity '/netmount' handler"""
393 # we _need_ a net_resource
394 if not "net_resource" in args:
395 raise web.badrequest('no net_resource given')
397 driveHandler = MountNetworkDriveHandler(args['net_resource'])
399 driveHandler.join(None)
402 def unmapDrive(drive, force=0):
403 logger.debug("drive in use, trying to unmap...")
405 logger.debug("Executing un-forced call...")
408 win32wnet.WNetCancelConnection2(drive, 1, force)
409 logger,info(drive + "successfully unmapped...")
412 logger.error("Unmap failed, try again...")
415 # handles netumount request
416 class UmountNetworkDriveHandler(threading.Thread):
420 def __init__(self, path):
421 threading.Thread.__init__(self)
422 self.networkPath = path
426 drive = getNetworkDrive(self.networkPath)
428 logger.info("Failed to retrieve drive letter for: " + self.networkPath + ". Successfully deleted or missing.")
432 logger.info("Unmounting drive " + drive + " for " + self.networkPath)
433 result = unmapDrive(drive, force=1)
440 """OpenSecurity '/netumount' handler"""
446 # we _need_ a net_resource
447 if not "net_resource" in args:
448 raise web.badrequest('no net_resource given')
450 driveHandler = UmountNetworkDriveHandler(args['net_resource'])
452 driveHandler.join(None)
457 """OpenSecurity '/netcleanup' handler"""
463 # we _need_ a net_resource
464 if not "hostonly_ip" in args:
465 raise web.badrequest('no hostonly_ip given')
467 ip = args['hostonly_ip']
468 ip = ip[:ip.rindex('.')]
469 drives = getLogicalDrives()
471 # found network drive
472 if getDriveType(drive) == 4:
473 path = getNetworkPath(drive)
475 driveHandler = UmountNetworkDriveHandler(path)
477 driveHandler.join(None)
481 """OpenSecurity '/' handler"""
485 res = "OpenSecurity-Client RESTFul Server { \"version\": \"%s\" }" % opensecurity.__version__
487 # add some sample links
493 (copy paste this into your browser's address field after the host:port)
495 /password?text=Give+me+a+password+for+device+%22My+USB+Drive%22+(ID%3A+32090-AAA-X0)
497 (eg.: http://127.0.0.1:8090/password?text=Give+me+a+password+for+device+%22My+USB+Drive%22+(ID%3A+32090-AAA-X0))
498 NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
501 Request a combination of user and password:
502 (copy paste this into your browser's address field after the host:port)
504 /credentials?text=Tell+the+NSA+which+credentials+to+use+in+order+to+avoid+hacking+noise+on+wire.
506 (eg.: http://127.0.0.1:8090/credentials?text=Tell+the+NSA+which+credentials+to+use+in+order+to+avoid+hacking+noise+on+wire.)
507 NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
510 Request a combination of password and keyfile:
511 (copy paste this into your browser's address field after the host:port)
513 /keyfile?text=Your%20private%20RSA%20Keyfile%3A
515 (eg.: http://127.0.0.1:8090//keyfile?text=Your%20private%20RSA%20Keyfile%3A)
516 NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
520 (copy paste this into your browser's address field after the host:port)
522 /application?vm=Debian+7&app=Browser
524 (e.g. http://127.0.0.1:8090/application?vm=Debian+7&app=Browser)
530 class ProcessResultBouncer(threading.Thread):
532 """A class to post the result of a given process - assuming it to be in JSON - to a REST Api."""
534 def __init__(self, process_command, remote_ip, resource):
538 threading.Thread.__init__(self)
539 self._process_command = process_command
540 self._remote_ip = remote_ip
541 self._resource = resource
556 # invoke the user dialog as a subprocess
557 process = subprocess.Popen(self._process_command, shell = False, stdout = subprocess.PIPE)
558 result = process.communicate()[0]
559 if process.returncode != 0:
560 print 'user request has been aborted.'
563 # all ok, tell send request back appropriate destination
565 j = json.loads(result)
567 print 'error in password parsing'
570 # by provided a 'data' we turn this into a POST statement
571 url_addr = 'http://' + self._remote_ip + ':58080' + self._resource
572 req = urllib2.Request(url_addr, urllib.urlencode(j))
574 res = urllib2.urlopen(req)
575 if res.getcode() == 200:
578 except urllib2.HTTPError as e:
580 # invoke the user dialog as a subprocess
581 dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
582 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())]
583 dlg_process = subprocess.Popen(dlg_process_command, shell = False, stdout = subprocess.PIPE)
584 dlg_process.communicate()[0]
587 class RESTServerThread(threading.Thread):
589 """Thread for serving the REST API."""
591 def __init__(self, port):
594 threading.Thread.__init__(self)
610 def is_already_running(port = 8090):
612 """check if this is started twice"""
615 s = socket.create_connection(('127.0.0.1', port), 0.5)
622 def _bounce_vm_logs():
624 """grab all logs from the VMs and push them to the log servers"""
628 # pick the highest current number
630 for f in glob.iglob(os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.*')):
632 n = f.split('.')[-1:][0]
640 # first add new vm logs to our existing one: rename the log file
641 log_file_name_new = os.path.join(Environment('OpenSecurity').log_path, 'vm_new.log')
642 log_file_name_cur = os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.' + str(cur))
643 log_file_lock.acquire()
645 os.rename(log_file_name_new, log_file_name_cur)
646 print('new log file: ' + log_file_name_cur)
649 log_file_lock.release()
651 # now we have a list of next log files to dump
652 log_files = glob.glob(os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.*'))
654 for log_file in log_files:
657 f = open(log_file, 'rb')
667 logger.warning('tried to delete log file (pushed to EOF) "' + log_file + '" but failed')
670 logger.warning('encountered error while pushing log file "' + log_file + '"')
673 # start bouncer again ...
674 global log_file_bouncer
675 log_file_bouncer = threading.Timer(5.0, _bounce_vm_logs)
676 log_file_bouncer.start()
680 """POST a single log to log server
682 @param log the log POST param
685 log_server_url = "http://extern.x-net.at/opensecurity/log"
687 key = win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE, 'SOFTWARE\OpenSecurity')
688 log_server_url = str(win32api.RegQueryValueEx(key, 'LogServerURL')[0])
689 win32api.RegCloseKey(key)
691 logger.warning('Cannot open Registry HKEY_LOCAL_MACHINE\SOFTWARE\OpenSecurity and get "LogServerURL" value, using default instead')
693 # by provided a 'data' we turn this into a POST statement
694 d = urllib.urlencode(log)
695 req = urllib2.Request(log_server_url, d)
697 logger.debug('pushed log to server: ' + str(log_server_url))
702 """Start the REST server"""
706 # start the VM-log bouncer timer
707 global log_file_bouncer
708 log_file_bouncer = threading.Timer(5.0, _bounce_vm_logs)
709 log_file_bouncer.start()
711 # trick the web.py server
712 sys.argv = [__file__, str(port)]
713 server = web.application(opensecurity_urls, globals())
717 def serve(port = 8090, background = False):
719 """Start serving the REST Api
720 port ... port number to listen on
721 background ... cease into background (spawn thread) and return immediately"""
723 # start threaded or direct version
724 if background == True:
725 t = RESTServerThread(port)
732 """Stop serving the REST Api"""
738 global log_file_bouncer
739 if log_file_bouncer is not None:
740 log_file_bouncer.cancel()
745 if __name__ == "__main__":