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 2013-2014 X-Net and AIT Austrian Institute of Technology
18 # https://www.x-net.at
20 # AIT Austrian Institute of Technology
21 # Donau City Strasse 1
24 # http://www.ait.ac.at
27 # Licensed under the Apache License, Version 2.0 (the "License");
28 # you may not use this file except in compliance with the License.
29 # You may obtain a copy of the License at
31 # http://www.apache.org/licenses/LICENSE-2.0
33 # Unless required by applicable law or agreed to in writing, software
34 # distributed under the License is distributed on an "AS IS" BASIS,
35 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
36 # See the License for the specific language governing permissions and
37 # limitations under the License.
38 # ------------------------------------------------------------
41 # ------------------------------------------------------------
69 from PyQt4 import QtGui
71 from opensecurity_util import logger, setupLogger, OpenSecurityException
72 if sys.platform == 'win32' or sys.platform == 'cygwin':
73 from cygwin import Cygwin
76 import __init__ as opensecurity
77 from environment import Environment
80 # ------------------------------------------------------------
84 """All the URLs we know mapping to class handler"""
86 '/credentials', 'os_credentials',
87 '/keyfile', 'os_keyfile',
89 '/message', 'os_message',
90 '/notification', 'os_notification',
91 '/password', 'os_password',
92 '/netmount', 'os_netmount',
93 '/netumount', 'os_netumount',
94 '/netcleanup', 'os_netcleanup',
100 # ------------------------------------------------------------
104 """lock for read/write log file"""
105 log_file_lock = threading.Lock()
107 """timer for the log file bouncer"""
108 log_file_bouncer = None
110 """The REST server object"""
111 #restful_client_server = None
113 """The System Tray Icon instance"""
117 # ------------------------------------------------------------
121 class os_credentials:
123 """OpenSecurity '/credentials' handler.
125 This is called on GET /credentials?text=TEXT.
126 Ideally this should pop up a user dialog to insert his
127 credentials based the given TEXT.
136 if not "text" in args:
137 raise web.badrequest('no text given')
140 remote_ip = web.ctx.environ['REMOTE_ADDR']
142 # create the process which queries the user
143 dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
144 process_command = [sys.executable, dlg_image, 'credentials', args.text]
146 # run process result handling in seprate thread (not to block main one)
147 bouncer = ProcessResultBouncer(process_command, remote_ip, '/credentials')
150 return 'user queried for credentials'
155 """OpenSecurity '/keyfile' handler.
157 This is called on GET /keyfile?text=TEXT.
158 Ideally this should pop up a user dialog to insert his
159 password along with a keyfile.
168 if not "text" in args:
169 raise web.badrequest('no text given')
172 remote_ip = web.ctx.environ['REMOTE_ADDR']
174 # create the process which queries the user
175 dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
176 process_command = [sys.executable, dlg_image, 'keyfile', args.text]
178 # run process result handling in seprate thread (not to block main one)
179 bouncer = ProcessResultBouncer(process_command, remote_ip, '/keyfile')
182 return 'user queried for password and keyfile'
187 """OpenSecurity '/log' handler.
189 This is called on GET or POST on the log function /log
202 args['user'] = getpass.getuser()
203 args['system'] = platform.node() + " " + platform.system() + " " + platform.release()
205 # add these to new data to log
207 log_file_name = os.path.join(Environment('OpenSecurity').log_path, 'vm_new.log')
208 log_file_lock.acquire()
209 pickle.dump(args, open(log_file_name, 'ab'))
210 log_file_lock.release()
217 """OpenSecurity '/message' handler.
219 This is called on GET /message?text=TEXTi&timeout=TIMEOUT.
220 This pops up the typical tray message (like a ballon on windows).
232 if not "text" in args:
233 raise web.badrequest('no text given')
236 if "timeout" in args:
238 timeout=int(args.timeout)
242 if tray_icon is None:
243 raise web.badrequest('unable to access tray icon instance')
245 tray_icon.showMessage('OpenSecurity', args.text, QtGui.QSystemTrayIcon.Information, timeout)
246 return 'Shown: ' + args.text + ' timeout: ' + str(timeout) + ' ms'
249 class os_notification:
251 """OpenSecurity '/notification' handler.
253 This is called on GET /notification?msgtype=TYPE&text=TEXT.
254 This will pop up an OpenSecurity notifcation window
266 if not "msgtype" in args:
267 raise web.badrequest('no msgtype given')
269 if not args.msgtype in ['information', 'warning', 'critical']:
270 raise web.badrequest('Unknown value for msgtype')
273 if not "text" in args:
274 raise web.badrequest('no text given')
276 # invoke the user dialog as a subprocess
277 dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
278 process_command = [sys.executable, dlg_image, 'notification-' + args.msgtype, args.text]
279 process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)
286 """OpenSecurity '/password' handler.
288 This is called on GET /password?text=TEXT.
289 Ideally this should pop up a user dialog to insert his
290 password based device name.
299 if not "text" in args:
300 raise web.badrequest('no text given')
303 remote_ip = web.ctx.environ['REMOTE_ADDR']
305 # create the process which queries the user
306 dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
307 process_command = [sys.executable, dlg_image, 'password', args.text]
309 # run process result handling in seprate thread (not to block main one)
310 bouncer = ProcessResultBouncer(process_command, remote_ip, '/password')
313 return 'user queried for password'
317 def genNetworkDrive():
318 logical_drives = getLogicalDrives()
319 logger.info("Used logical drive letters: "+ str(logical_drives).strip('[]') )
320 drives = list(map(chr, range(68, 91)))
322 if drive not in logical_drives:
326 def getLogicalDrives():
327 drive_bitmask = ctypes.cdll.kernel32.GetLogicalDrives()
328 drives = list(itertools.compress(string.ascii_uppercase, map(lambda x:ord(x) - ord('0'), bin(drive_bitmask)[:1:-1])))
331 def getNetworkPath(drive):
332 return win32wnet.WNetGetConnection(drive+':')
334 def getDriveType(drive):
335 return ctypes.cdll.kernel32.GetDriveTypeW(u"%s:\\"%drive)
337 def getNetworkDrive(path):
338 for drive in getLogicalDrives():
339 #if is a network drive
340 if getDriveType(drive) == 4:
341 network_path = getNetworkPath(drive)
342 if path in network_path:
346 def mapDrive(drive, networkPath, user, password):
347 if (os.path.exists(networkPath)):
348 logger.debug(networkPath + " is found...")
349 logger.debug("Trying to map " + networkPath + " on to " + drive + " .....")
351 win32wnet.WNetAddConnection2(win32netcon.RESOURCETYPE_DISK, drive, networkPath, None, user, password)
353 logger.error("Unexpected error...")
355 logger.info("Mapping successful")
358 logger.error("Network path unreachable...")
361 mount_lock = threading.Lock()
363 # handles netumount request
364 class MountNetworkDriveHandler(threading.Thread):
366 def __init__(self, net_path):
367 threading.Thread.__init__(self)
368 self.networkPath = net_path
371 #Check for network resource availability
373 while not os.path.exists(self.networkPath):
376 logger.info("Path not accessible: " + self.networkPath + " retrying")
380 drive = genNetworkDrive()
382 logger.error("Failed to assign drive letter for: " + self.networkPath)
385 logger.info("Assigned drive " + drive + " to " + self.networkPath)
387 #Check for drive availability
389 if os.path.exists(drive):
390 logger.error("Drive letter is already in use: " + drive)
393 return mapDrive(drive, self.networkPath, "", "")
397 """OpenSecurity '/netmount' handler"""
403 # we _need_ a net_resource
404 if not "net_resource" in args:
405 raise web.badrequest('no net_resource given')
407 driveHandler = MountNetworkDriveHandler(args['net_resource'])
409 driveHandler.join(None)
412 def unmapDrive(drive, force=0):
413 logger.debug("drive in use, trying to unmap...")
415 logger.debug("Executing un-forced call...")
418 win32wnet.WNetCancelConnection2(drive, 1, force)
419 logger.info(drive + "successfully unmapped...")
422 logger.error("Unmap failed, try again...")
425 # handles netumount request
426 class UmountNetworkDriveHandler(threading.Thread):
430 def __init__(self, path):
431 threading.Thread.__init__(self)
432 self.networkPath = path
436 drive = getNetworkDrive(self.networkPath)
438 logger.info("Failed to retrieve drive letter for: " + self.networkPath + ". Successfully deleted or missing.")
442 logger.info("Unmounting drive " + drive + " for " + self.networkPath)
443 result = unmapDrive(drive, force=1)
450 """OpenSecurity '/netumount' handler"""
456 # we _need_ a net_resource
457 if not "net_resource" in args:
458 raise web.badrequest('no net_resource given')
460 driveHandler = UmountNetworkDriveHandler(args['net_resource'])
462 driveHandler.join(None)
467 """OpenSecurity '/netcleanup' handler"""
473 # we _need_ a net_resource
474 if not "hostonly_ip" in args:
475 raise web.badrequest('no hostonly_ip given')
477 ip = args['hostonly_ip']
478 ip = ip[:ip.rindex('.')]
479 drives = getLogicalDrives()
481 # found network drive
482 if getDriveType(drive) == 4:
483 path = getNetworkPath(drive)
485 driveHandler = UmountNetworkDriveHandler(path)
487 driveHandler.join(None)
491 """OpenSecurity '/' handler"""
495 res = "OpenSecurity-Client RESTFul Server { \"version\": \"%s\" }" % opensecurity.__version__
497 # add some sample links
503 (copy paste this into your browser's address field after the host:port)
505 /password?text=Give+me+a+password+for+device+%22My+USB+Drive%22+(ID%3A+32090-AAA-X0)
507 (eg.: http://127.0.0.1:8090/password?text=Give+me+a+password+for+device+%22My+USB+Drive%22+(ID%3A+32090-AAA-X0))
508 NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
511 Request a combination of user and password:
512 (copy paste this into your browser's address field after the host:port)
514 /credentials?text=Tell+the+NSA+which+credentials+to+use+in+order+to+avoid+hacking+noise+on+wire.
516 (eg.: http://127.0.0.1:8090/credentials?text=Tell+the+NSA+which+credentials+to+use+in+order+to+avoid+hacking+noise+on+wire.)
517 NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
520 Request a combination of password and keyfile:
521 (copy paste this into your browser's address field after the host:port)
523 /keyfile?text=Your%20private%20RSA%20Keyfile%3A
525 (eg.: http://127.0.0.1:8090//keyfile?text=Your%20private%20RSA%20Keyfile%3A)
526 NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
530 (copy paste this into your browser's address field after the host:port)
532 /application?vm=Debian+7&app=Browser
534 (e.g. http://127.0.0.1:8090/application?vm=Debian+7&app=Browser)
542 """OpenSecurity '/quit' handler.
544 Terminate the client REST server
549 server = web.ctx.app_stack[0]
550 raise KeyboardInterrupt()
554 class ProcessResultBouncer(threading.Thread):
556 """A class to post the result of a given process - assuming it to be in JSON - to a REST Api."""
558 def __init__(self, process_command, remote_ip, resource):
562 threading.Thread.__init__(self)
563 self._process_command = process_command
564 self._remote_ip = remote_ip
565 self._resource = resource
580 # invoke the user dialog as a subprocess
581 process = subprocess.Popen(self._process_command, shell = False, stdout = subprocess.PIPE)
582 result = process.communicate()[0]
583 if process.returncode != 0:
584 print 'user request has been aborted.'
587 # all ok, tell send request back appropriate destination
589 j = json.loads(result)
591 print 'error in password parsing'
594 # by provided a 'data' we turn this into a POST statement
595 url_addr = 'http://' + self._remote_ip + ':58080' + self._resource
596 req = urllib2.Request(url_addr, urllib.urlencode(j))
598 res = urllib2.urlopen(req)
599 if res.getcode() == 200:
602 except urllib2.HTTPError as e:
604 # invoke the user dialog as a subprocess
605 dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
606 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())]
607 dlg_process = subprocess.Popen(dlg_process_command, shell = False, stdout = subprocess.PIPE)
608 dlg_process.communicate()[0]
611 class RESTServerThread(threading.Thread):
613 """Thread for serving the REST API."""
615 def __init__(self, port):
618 threading.Thread.__init__(self)
634 def is_already_running(port = 8090):
636 """check if this is started twice"""
639 s = socket.create_connection(('127.0.0.1', port), 0.5)
646 def _bounce_vm_logs():
648 """grab all logs from the VMs and push them to the log servers"""
652 # pick the highest current number
654 for f in glob.iglob(os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.*')):
656 n = f.split('.')[-1:][0]
664 # first add new vm logs to our existing one: rename the log file
665 log_file_name_new = os.path.join(Environment('OpenSecurity').log_path, 'vm_new.log')
666 log_file_name_cur = os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.' + str(cur))
667 log_file_lock.acquire()
669 os.rename(log_file_name_new, log_file_name_cur)
670 print('new log file: ' + log_file_name_cur)
673 log_file_lock.release()
675 # now we have a list of next log files to dump
676 log_files = glob.glob(os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.*'))
678 for log_file in log_files:
681 f = open(log_file, 'rb')
691 logger.warning('tried to delete log file (pushed to EOF) "' + log_file + '" but failed')
694 logger.warning('encountered error while pushing log file "' + log_file + '"')
697 # start bouncer again ...
698 global log_file_bouncer
699 log_file_bouncer = threading.Timer(5.0, _bounce_vm_logs)
700 log_file_bouncer.start()
704 """POST a single log to log server
706 @param log the log POST param
709 log_server_url = "http://extern.x-net.at/opensecurity/log"
711 key = win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE, 'SOFTWARE\OpenSecurity')
712 log_server_url = str(win32api.RegQueryValueEx(key, 'LogServerURL')[0])
713 win32api.RegCloseKey(key)
715 logger.warning('Cannot open Registry HKEY_LOCAL_MACHINE\SOFTWARE\OpenSecurity and get "LogServerURL" value, using default instead')
717 # by provided a 'data' we turn this into a POST statement
718 d = urllib.urlencode(log)
719 req = urllib2.Request(log_server_url, d)
721 logger.debug('pushed log to server: ' + str(log_server_url))
726 """Start the REST server"""
728 # start the VM-log bouncer timer
729 global log_file_bouncer
730 log_file_bouncer = threading.Timer(5.0, _bounce_vm_logs)
731 log_file_bouncer.start()
733 # trick the web.py server
734 sys.argv = [__file__, str(port)]
735 server = web.application(opensecurity_urls, globals())
738 # from this point on we received the quit call
739 # and are winding down
740 log_file_bouncer.cancel()
741 QtGui.QApplication.quit()
744 def serve(port = 8090, background = False):
746 """Start serving the REST Api
747 port ... port number to listen on
748 background ... cease into background (spawn thread) and return immediately"""
750 # start threaded or direct version
751 if background == True:
752 t = RESTServerThread(port)
759 if __name__ == "__main__":