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]
135 process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)
137 # run process result handling in seprate thread (not to block main one)
138 bouncer = ProcessResultBouncer(process, 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]
168 process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)
170 # run process result handling in seprate thread (not to block main one)
171 bouncer = ProcessResultBouncer(process, remote_ip, '/keyfile')
174 return 'user queried for password and keyfile'
179 """OpenSecurity '/log' handler.
181 This is called on GET or POST on the log function /log
194 args['user'] = getpass.getuser()
195 args['system'] = platform.node() + " " + platform.system() + " " + platform.release()
197 # add these to new data to log
199 log_file_name = os.path.join(Environment('OpenSecurity').log_path, 'vm_new.log')
200 log_file_lock.acquire()
201 pickle.dump(args, open(log_file_name, 'ab'))
202 log_file_lock.release()
209 """OpenSecurity '/message' handler.
211 This is called on GET /message?text=TEXTi&timeout=TIMEOUT.
212 This pops up the typical tray message (like a ballon on windows).
224 if not "text" in args:
225 raise web.badrequest('no text given')
228 if "timeout" in args:
230 timeout=int(args.timeout)
234 if tray_icon is None:
235 raise web.badrequest('unable to access tray icon instance')
237 tray_icon.showMessage('OpenSecurity', args.text, QtGui.QSystemTrayIcon.Information, timeout)
238 return 'Shown: ' + args.text + ' timeout: ' + str(timeout) + ' ms'
241 class os_notification:
243 """OpenSecurity '/notification' handler.
245 This is called on GET /notification?msgtype=TYPE&text=TEXT.
246 This will pop up an OpenSecurity notifcation window
258 if not "msgtype" in args:
259 raise web.badrequest('no msgtype given')
261 if not args.msgtype in ['information', 'warning', 'critical']:
262 raise web.badrequest('Unknown value for msgtype')
265 if not "text" in args:
266 raise web.badrequest('no text given')
268 # invoke the user dialog as a subprocess
269 dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.py')
270 process_command = [sys.executable, dlg_image, 'notification-' + args.msgtype, args.text]
271 process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)
278 """OpenSecurity '/password' handler.
280 This is called on GET /password?text=TEXT.
281 Ideally this should pop up a user dialog to insert his
282 password based device name.
291 if not "text" in args:
292 raise web.badrequest('no text given')
295 remote_ip = web.ctx.environ['REMOTE_ADDR']
297 # create the process which queries the user
298 dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
299 process_command = [sys.executable, dlg_image, 'password', args.text]
300 process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)
302 # run process result handling in seprate thread (not to block main one)
303 bouncer = ProcessResultBouncer(process, remote_ip, '/password')
306 return 'user queried for password'
310 def genNetworkDrive():
311 logical_drives = getLogicalDrives()
312 logger.info("Used logical drive letters: "+ str(logical_drives).strip('[]') )
313 drives = list(map(chr, range(68, 91)))
315 if drive not in logical_drives:
319 def getLogicalDrives():
320 drive_bitmask = ctypes.cdll.kernel32.GetLogicalDrives()
321 drives = list(itertools.compress(string.ascii_uppercase, map(lambda x:ord(x) - ord('0'), bin(drive_bitmask)[:1:-1])))
324 def getNetworkPath(drive):
325 return win32wnet.WNetGetConnection(drive+':')
327 def getDriveType(drive):
328 return ctypes.cdll.kernel32.GetDriveTypeW(u"%s:\\"%drive)
330 def getNetworkDrive(path):
331 for drive in getLogicalDrives():
332 #if is a network drive
333 if getDriveType(drive) == 4:
334 network_path = getNetworkPath(drive)
335 if path in network_path:
339 def mapDrive(drive, networkPath, user, password):
340 if (os.path.exists(networkPath)):
341 logger.debug(networkPath + " is found...")
342 logger.debug("Trying to map " + networkPath + " on to " + drive + " .....")
344 win32wnet.WNetAddConnection2(win32netcon.RESOURCETYPE_DISK, drive, networkPath, None, user, password)
346 logger.error("Unexpected error...")
348 logger.info("Mapping successful")
351 logger.error("Network path unreachable...")
354 mount_lock = threading.Lock()
356 # handles netumount request
357 class MountNetworkDriveHandler(threading.Thread):
359 def __init__(self, net_path):
360 threading.Thread.__init__(self)
361 self.networkPath = net_path
364 #Check for network resource availability
366 while not os.path.exists(self.networkPath):
369 logger.info("Path not accessible: " + self.networkPath + " retrying")
373 drive = genNetworkDrive()
375 logger.error("Failed to assign drive letter for: " + self.networkPath)
378 logger.info("Assigned drive " + drive + " to " + self.networkPath)
380 #Check for drive availability
382 if os.path.exists(drive):
383 logger.error("Drive letter is already in use: " + drive)
386 return mapDrive(drive, self.networkPath, "", "")
390 """OpenSecurity '/netmount' handler"""
396 # we _need_ a net_resource
397 if not "net_resource" in args:
398 raise web.badrequest('no net_resource given')
400 driveHandler = MountNetworkDriveHandler(args['net_resource'])
402 driveHandler.join(None)
405 def unmapDrive(drive, force=0):
406 logger.debug("drive in use, trying to unmap...")
408 logger.debug("Executing un-forced call...")
411 win32wnet.WNetCancelConnection2(drive, 1, force)
412 logger,info(drive + "successfully unmapped...")
415 logger.error("Unmap failed, try again...")
418 # handles netumount request
419 class UmountNetworkDriveHandler(threading.Thread):
423 def __init__(self, path):
424 threading.Thread.__init__(self)
425 self.networkPath = path
429 drive = getNetworkDrive(self.networkPath)
431 logger.info("Failed to retrieve drive letter for: " + self.networkPath + ". Successfully deleted or missing.")
435 logger.info("Unmounting drive " + drive + " for " + self.networkPath)
436 result = unmapDrive(drive, force=1)
443 """OpenSecurity '/netumount' handler"""
449 # we _need_ a net_resource
450 if not "net_resource" in args:
451 raise web.badrequest('no net_resource given')
453 driveHandler = UmountNetworkDriveHandler(args['net_resource'])
455 driveHandler.join(None)
460 """OpenSecurity '/netcleanup' handler"""
466 # we _need_ a net_resource
467 if not "hostonly_ip" in args:
468 raise web.badrequest('no hostonly_ip given')
470 ip = args['hostonly_ip']
471 ip = ip[:ip.rindex('.')]
472 drives = getLogicalDrives()
474 # found network drive
475 if getDriveType(drive) == 4:
476 path = getNetworkPath(drive)
478 driveHandler = UmountNetworkDriveHandler(path)
480 driveHandler.join(None)
484 """OpenSecurity '/' handler"""
488 res = "OpenSecurity-Client RESTFul Server { \"version\": \"%s\" }" % opensecurity.__version__
490 # add some sample links
496 (copy paste this into your browser's address field after the host:port)
498 /password?text=Give+me+a+password+for+device+%22My+USB+Drive%22+(ID%3A+32090-AAA-X0)
500 (eg.: http://127.0.0.1:8090/password?text=Give+me+a+password+for+device+%22My+USB+Drive%22+(ID%3A+32090-AAA-X0))
501 NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
504 Request a combination of user and password:
505 (copy paste this into your browser's address field after the host:port)
507 /credentials?text=Tell+the+NSA+which+credentials+to+use+in+order+to+avoid+hacking+noise+on+wire.
509 (eg.: http://127.0.0.1:8090/credentials?text=Tell+the+NSA+which+credentials+to+use+in+order+to+avoid+hacking+noise+on+wire.)
510 NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
513 Request a combination of password and keyfile:
514 (copy paste this into your browser's address field after the host:port)
516 /keyfile?text=Your%20private%20RSA%20Keyfile%3A
518 (eg.: http://127.0.0.1:8090//keyfile?text=Your%20private%20RSA%20Keyfile%3A)
519 NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
523 (copy paste this into your browser's address field after the host:port)
525 /application?vm=Debian+7&app=Browser
527 (e.g. http://127.0.0.1:8090/application?vm=Debian+7&app=Browser)
533 class ProcessResultBouncer(threading.Thread):
535 """A class to post the result of a given process - assuming it to be in JSON - to a REST Api."""
537 def __init__(self, process, remote_ip, resource):
541 threading.Thread.__init__(self)
542 self._process = process
543 self._remote_ip = remote_ip
544 self._resource = resource
557 # invoke the user dialog as a subprocess
558 result = self._process.communicate()[0]
559 if self._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)
576 print 'failed to contact: ' + url_addr
580 class RESTServerThread(threading.Thread):
582 """Thread for serving the REST API."""
584 def __init__(self, port):
587 threading.Thread.__init__(self)
603 def is_already_running(port = 8090):
605 """check if this is started twice"""
608 s = socket.create_connection(('127.0.0.1', port), 0.5)
615 def _bounce_vm_logs():
617 """grab all logs from the VMs and push them to the log servers"""
621 # pick the highest current number
623 for f in glob.iglob(os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.*')):
625 n = f.split('.')[-1:][0]
633 # first add new vm logs to our existing one: rename the log file
634 log_file_name_new = os.path.join(Environment('OpenSecurity').log_path, 'vm_new.log')
635 log_file_name_cur = os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.' + str(cur))
636 log_file_lock.acquire()
638 os.rename(log_file_name_new, log_file_name_cur)
639 print('new log file: ' + log_file_name_cur)
642 log_file_lock.release()
644 # now we have a list of next log files to dump
645 log_files = glob.glob(os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.*'))
647 for log_file in log_files:
650 f = open(log_file, 'rb')
660 logger.warning('tried to delete log file (pushed to EOF) "' + log_file + '" but failed')
663 logger.warning('encountered error while pushing log file "' + log_file + '"')
666 # start bouncer again ...
667 global log_file_bouncer
668 log_file_bouncer = threading.Timer(5.0, _bounce_vm_logs)
669 log_file_bouncer.start()
673 """POST a single log to log server
675 @param log the log POST param
678 log_server_url = "http://extern.x-net.at/opensecurity/log"
680 key = win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE, 'SOFTWARE\OpenSecurity')
681 log_server_url = str(win32api.RegQueryValueEx(key, 'LogServerURL')[0])
682 win32api.RegCloseKey(key)
684 logger.warning('Cannot open Registry HKEY_LOCAL_MACHINE\SOFTWARE\OpenSecurity and get "LogServerURL" value, using default instead')
686 # by provided a 'data' we turn this into a POST statement
687 d = urllib.urlencode(log)
688 req = urllib2.Request(log_server_url, d)
690 logger.debug('pushed log to server: ' + str(log_server_url))
695 """Start the REST server"""
699 # start the VM-log bouncer timer
700 global log_file_bouncer
701 log_file_bouncer = threading.Timer(5.0, _bounce_vm_logs)
702 log_file_bouncer.start()
704 # trick the web.py server
705 sys.argv = [__file__, str(port)]
706 server = web.application(opensecurity_urls, globals())
710 def serve(port = 8090, background = False):
712 """Start serving the REST Api
713 port ... port number to listen on
714 background ... cease into background (spawn thread) and return immediately"""
716 # start threaded or direct version
717 if background == True:
718 t = RESTServerThread(port)
725 """Stop serving the REST Api"""
731 global log_file_bouncer
732 if log_file_bouncer is not None:
733 log_file_bouncer.cancel()
738 if __name__ == "__main__":