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',
91 # ------------------------------------------------------------
95 """lock for read/write log file"""
96 log_file_lock = threading.Lock()
98 """timer for the log file bouncer"""
99 log_file_bouncer = None
101 """The REST server object"""
102 #restful_client_server = None
104 """The System Tray Icon instance"""
108 # ------------------------------------------------------------
112 class os_credentials:
114 """OpenSecurity '/credentials' handler.
116 This is called on GET /credentials?text=TEXT.
117 Ideally this should pop up a user dialog to insert his
118 credentials based the given TEXT.
127 if not "text" in args:
128 raise web.badrequest('no text given')
131 remote_ip = web.ctx.environ['REMOTE_ADDR']
133 # create the process which queries the user
134 dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
135 process_command = [sys.executable, dlg_image, 'credentials', args.text]
137 # run process result handling in seprate thread (not to block main one)
138 bouncer = ProcessResultBouncer(process_command, remote_ip, '/credentials')
141 return 'user queried for credentials'
146 """OpenSecurity '/keyfile' handler.
148 This is called on GET /keyfile?text=TEXT.
149 Ideally this should pop up a user dialog to insert his
150 password along with a keyfile.
159 if not "text" in args:
160 raise web.badrequest('no text given')
163 remote_ip = web.ctx.environ['REMOTE_ADDR']
165 # create the process which queries the user
166 dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
167 process_command = [sys.executable, dlg_image, 'keyfile', args.text]
169 # run process result handling in seprate thread (not to block main one)
170 bouncer = ProcessResultBouncer(process_command, remote_ip, '/keyfile')
173 return 'user queried for password and keyfile'
178 """OpenSecurity '/log' handler.
180 This is called on GET or POST on the log function /log
193 args['user'] = getpass.getuser()
194 args['system'] = platform.node() + " " + platform.system() + " " + platform.release()
196 # add these to new data to log
198 log_file_name = os.path.join(Environment('OpenSecurity').log_path, 'vm_new.log')
199 log_file_lock.acquire()
200 pickle.dump(args, open(log_file_name, 'ab'))
201 log_file_lock.release()
208 """OpenSecurity '/message' handler.
210 This is called on GET /message?text=TEXTi&timeout=TIMEOUT.
211 This pops up the typical tray message (like a ballon on windows).
223 if not "text" in args:
224 raise web.badrequest('no text given')
227 if "timeout" in args:
229 timeout=int(args.timeout)
233 if tray_icon is None:
234 raise web.badrequest('unable to access tray icon instance')
236 tray_icon.showMessage('OpenSecurity', args.text, QtGui.QSystemTrayIcon.Information, timeout)
237 return 'Shown: ' + args.text + ' timeout: ' + str(timeout) + ' ms'
240 class os_notification:
242 """OpenSecurity '/notification' handler.
244 This is called on GET /notification?msgtype=TYPE&text=TEXT.
245 This will pop up an OpenSecurity notifcation window
257 if not "msgtype" in args:
258 raise web.badrequest('no msgtype given')
260 if not args.msgtype in ['information', 'warning', 'critical']:
261 raise web.badrequest('Unknown value for msgtype')
264 if not "text" in args:
265 raise web.badrequest('no text given')
267 # invoke the user dialog as a subprocess
268 dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
269 process_command = [sys.executable, dlg_image, 'notification-' + args.msgtype, args.text]
270 process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)
277 """OpenSecurity '/password' handler.
279 This is called on GET /password?text=TEXT.
280 Ideally this should pop up a user dialog to insert his
281 password based device name.
290 if not "text" in args:
291 raise web.badrequest('no text given')
294 remote_ip = web.ctx.environ['REMOTE_ADDR']
296 # create the process which queries the user
297 dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
298 process_command = [sys.executable, dlg_image, 'password', args.text]
300 # run process result handling in seprate thread (not to block main one)
301 bouncer = ProcessResultBouncer(process_command, remote_ip, '/password')
304 return 'user queried for password'
308 def genNetworkDrive():
309 logical_drives = getLogicalDrives()
310 logger.info("Used logical drive letters: "+ str(logical_drives).strip('[]') )
311 drives = list(map(chr, range(68, 91)))
313 if drive not in logical_drives:
317 def getLogicalDrives():
318 drive_bitmask = ctypes.cdll.kernel32.GetLogicalDrives()
319 drives = list(itertools.compress(string.ascii_uppercase, map(lambda x:ord(x) - ord('0'), bin(drive_bitmask)[:1:-1])))
322 def getNetworkPath(drive):
323 return win32wnet.WNetGetConnection(drive+':')
325 def getDriveType(drive):
326 return ctypes.cdll.kernel32.GetDriveTypeW(u"%s:\\"%drive)
328 def getNetworkDrive(path):
329 for drive in getLogicalDrives():
330 #if is a network drive
331 if getDriveType(drive) == 4:
332 network_path = getNetworkPath(drive)
333 if path in network_path:
337 def mapDrive(drive, networkPath, user, password):
338 if (os.path.exists(networkPath)):
339 logger.debug(networkPath + " is found...")
340 logger.debug("Trying to map " + networkPath + " on to " + drive + " .....")
342 win32wnet.WNetAddConnection2(win32netcon.RESOURCETYPE_DISK, drive, networkPath, None, user, password)
344 logger.error("Unexpected error...")
346 logger.info("Mapping successful")
349 logger.error("Network path unreachable...")
352 mount_lock = threading.Lock()
354 # handles netumount request
355 class MountNetworkDriveHandler(threading.Thread):
357 def __init__(self, net_path):
358 threading.Thread.__init__(self)
359 self.networkPath = net_path
362 #Check for network resource availability
364 while not os.path.exists(self.networkPath):
367 logger.info("Path not accessible: " + self.networkPath + " retrying")
371 drive = genNetworkDrive()
373 logger.error("Failed to assign drive letter for: " + self.networkPath)
376 logger.info("Assigned drive " + drive + " to " + self.networkPath)
378 #Check for drive availability
380 if os.path.exists(drive):
381 logger.error("Drive letter is already in use: " + drive)
384 return mapDrive(drive, self.networkPath, "", "")
388 """OpenSecurity '/netmount' handler"""
394 # we _need_ a net_resource
395 if not "net_resource" in args:
396 raise web.badrequest('no net_resource given')
398 driveHandler = MountNetworkDriveHandler(args['net_resource'])
400 driveHandler.join(None)
403 def unmapDrive(drive, force=0):
404 logger.debug("drive in use, trying to unmap...")
406 logger.debug("Executing un-forced call...")
409 win32wnet.WNetCancelConnection2(drive, 1, force)
410 logger.info(drive + "successfully unmapped...")
413 logger.error("Unmap failed, try again...")
416 # handles netumount request
417 class UmountNetworkDriveHandler(threading.Thread):
421 def __init__(self, path):
422 threading.Thread.__init__(self)
423 self.networkPath = path
427 drive = getNetworkDrive(self.networkPath)
429 logger.info("Failed to retrieve drive letter for: " + self.networkPath + ". Successfully deleted or missing.")
433 logger.info("Unmounting drive " + drive + " for " + self.networkPath)
434 result = unmapDrive(drive, force=1)
441 """OpenSecurity '/netumount' handler"""
447 # we _need_ a net_resource
448 if not "net_resource" in args:
449 raise web.badrequest('no net_resource given')
451 driveHandler = UmountNetworkDriveHandler(args['net_resource'])
453 driveHandler.join(None)
458 """OpenSecurity '/netcleanup' handler"""
464 # we _need_ a net_resource
465 if not "hostonly_ip" in args:
466 raise web.badrequest('no hostonly_ip given')
468 ip = args['hostonly_ip']
469 ip = ip[:ip.rindex('.')]
470 drives = getLogicalDrives()
472 # found network drive
473 if getDriveType(drive) == 4:
474 path = getNetworkPath(drive)
476 driveHandler = UmountNetworkDriveHandler(path)
478 driveHandler.join(None)
482 """OpenSecurity '/' handler"""
486 res = "OpenSecurity-Client RESTFul Server { \"version\": \"%s\" }" % opensecurity.__version__
488 # add some sample links
494 (copy paste this into your browser's address field after the host:port)
496 /password?text=Give+me+a+password+for+device+%22My+USB+Drive%22+(ID%3A+32090-AAA-X0)
498 (eg.: http://127.0.0.1:8090/password?text=Give+me+a+password+for+device+%22My+USB+Drive%22+(ID%3A+32090-AAA-X0))
499 NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
502 Request a combination of user and password:
503 (copy paste this into your browser's address field after the host:port)
505 /credentials?text=Tell+the+NSA+which+credentials+to+use+in+order+to+avoid+hacking+noise+on+wire.
507 (eg.: http://127.0.0.1:8090/credentials?text=Tell+the+NSA+which+credentials+to+use+in+order+to+avoid+hacking+noise+on+wire.)
508 NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
511 Request a combination of password and keyfile:
512 (copy paste this into your browser's address field after the host:port)
514 /keyfile?text=Your%20private%20RSA%20Keyfile%3A
516 (eg.: http://127.0.0.1:8090//keyfile?text=Your%20private%20RSA%20Keyfile%3A)
517 NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
521 (copy paste this into your browser's address field after the host:port)
523 /application?vm=Debian+7&app=Browser
525 (e.g. http://127.0.0.1:8090/application?vm=Debian+7&app=Browser)
533 """OpenSecurity '/quit' handler.
535 Terminate the client REST server
540 server = web.ctx.app_stack[0]
541 raise KeyboardInterrupt()
545 class ProcessResultBouncer(threading.Thread):
547 """A class to post the result of a given process - assuming it to be in JSON - to a REST Api."""
549 def __init__(self, process_command, remote_ip, resource):
553 threading.Thread.__init__(self)
554 self._process_command = process_command
555 self._remote_ip = remote_ip
556 self._resource = resource
571 # invoke the user dialog as a subprocess
572 process = subprocess.Popen(self._process_command, shell = False, stdout = subprocess.PIPE)
573 result = process.communicate()[0]
574 if process.returncode != 0:
575 print 'user request has been aborted.'
578 # all ok, tell send request back appropriate destination
580 j = json.loads(result)
582 print 'error in password parsing'
585 # by provided a 'data' we turn this into a POST statement
586 url_addr = 'http://' + self._remote_ip + ':58080' + self._resource
587 req = urllib2.Request(url_addr, urllib.urlencode(j))
589 res = urllib2.urlopen(req)
590 if res.getcode() == 200:
593 except urllib2.HTTPError as e:
595 # invoke the user dialog as a subprocess
596 dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
597 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())]
598 dlg_process = subprocess.Popen(dlg_process_command, shell = False, stdout = subprocess.PIPE)
599 dlg_process.communicate()[0]
602 class RESTServerThread(threading.Thread):
604 """Thread for serving the REST API."""
606 def __init__(self, port):
609 threading.Thread.__init__(self)
625 def is_already_running(port = 8090):
627 """check if this is started twice"""
630 s = socket.create_connection(('127.0.0.1', port), 0.5)
637 def _bounce_vm_logs():
639 """grab all logs from the VMs and push them to the log servers"""
643 # pick the highest current number
645 for f in glob.iglob(os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.*')):
647 n = f.split('.')[-1:][0]
655 # first add new vm logs to our existing one: rename the log file
656 log_file_name_new = os.path.join(Environment('OpenSecurity').log_path, 'vm_new.log')
657 log_file_name_cur = os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.' + str(cur))
658 log_file_lock.acquire()
660 os.rename(log_file_name_new, log_file_name_cur)
661 print('new log file: ' + log_file_name_cur)
664 log_file_lock.release()
666 # now we have a list of next log files to dump
667 log_files = glob.glob(os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.*'))
669 for log_file in log_files:
672 f = open(log_file, 'rb')
682 logger.warning('tried to delete log file (pushed to EOF) "' + log_file + '" but failed')
685 logger.warning('encountered error while pushing log file "' + log_file + '"')
688 # start bouncer again ...
689 global log_file_bouncer
690 log_file_bouncer = threading.Timer(5.0, _bounce_vm_logs)
691 log_file_bouncer.start()
695 """POST a single log to log server
697 @param log the log POST param
700 log_server_url = "http://extern.x-net.at/opensecurity/log"
702 key = win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE, 'SOFTWARE\OpenSecurity')
703 log_server_url = str(win32api.RegQueryValueEx(key, 'LogServerURL')[0])
704 win32api.RegCloseKey(key)
706 logger.warning('Cannot open Registry HKEY_LOCAL_MACHINE\SOFTWARE\OpenSecurity and get "LogServerURL" value, using default instead')
708 # by provided a 'data' we turn this into a POST statement
709 d = urllib.urlencode(log)
710 req = urllib2.Request(log_server_url, d)
712 logger.debug('pushed log to server: ' + str(log_server_url))
717 """Start the REST server"""
719 # start the VM-log bouncer timer
720 global log_file_bouncer
721 log_file_bouncer = threading.Timer(5.0, _bounce_vm_logs)
722 log_file_bouncer.start()
724 # trick the web.py server
725 sys.argv = [__file__, str(port)]
726 server = web.application(opensecurity_urls, globals())
729 # from this point on we received the quit call
730 # and are winding down
731 log_file_bouncer.cancel()
732 QtGui.QApplication.quit()
735 def serve(port = 8090, background = False):
737 """Start serving the REST Api
738 port ... port number to listen on
739 background ... cease into background (spawn thread) and return immediately"""
741 # start threaded or direct version
742 print('background=%s' % (str(background)))
743 if background == True:
744 t = RESTServerThread(port)
751 if __name__ == "__main__":