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 opensecurity_util import logger, setupLogger, OpenSecurityException
61 if sys.platform == 'win32' or sys.platform == 'cygwin':
62 from cygwin import Cygwin
65 import __init__ as opensecurity
66 from environment import Environment
69 # ------------------------------------------------------------
73 """All the URLs we know mapping to class handler"""
75 '/credentials', 'os_credentials',
76 '/keyfile', 'os_keyfile',
78 '/notification', 'os_notification',
79 '/password', 'os_password',
80 '/netmount', 'os_netmount',
81 '/netumount', 'os_netumount',
82 '/netcleanup', 'os_netcleanup',
87 # ------------------------------------------------------------
91 """lock for read/write log file"""
92 log_file_lock = threading.Lock()
94 """timer for the log file bouncer"""
95 log_file_bouncer = None
98 """The REST server object"""
102 # ------------------------------------------------------------
106 class os_credentials:
108 """OpenSecurity '/credentials' handler.
110 This is called on GET /credentials?text=TEXT.
111 Ideally this should pop up a user dialog to insert his
112 credentials based the given TEXT.
121 if not "text" in args:
122 raise web.badrequest('no text given')
125 remote_ip = web.ctx.environ['REMOTE_ADDR']
127 # create the process which queries the user
128 dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
129 process_command = [sys.executable, dlg_image, 'credentials', args.text]
130 process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)
132 # run process result handling in seprate thread (not to block main one)
133 bouncer = ProcessResultBouncer(process, remote_ip, '/credentials')
136 return 'user queried for credentials'
141 """OpenSecurity '/keyfile' handler.
143 This is called on GET /keyfile?text=TEXT.
144 Ideally this should pop up a user dialog to insert his
145 password along with a keyfile.
154 if not "text" in args:
155 raise web.badrequest('no text given')
158 remote_ip = web.ctx.environ['REMOTE_ADDR']
160 # create the process which queries the user
161 dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
162 process_command = [sys.executable, dlg_image, 'keyfile', args.text]
163 process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)
165 # run process result handling in seprate thread (not to block main one)
166 bouncer = ProcessResultBouncer(process, remote_ip, '/keyfile')
169 return 'user queried for password and keyfile'
174 """OpenSecurity '/log' handler.
176 This is called on GET or POST on the log function /log
189 args['user'] = getpass.getuser()
190 args['system'] = platform.node() + " " + platform.system() + " " + platform.release()
192 # add these to new data to log
194 log_file_name = os.path.join(Environment('OpenSecurity').log_path, 'vm_new.log')
195 log_file_lock.acquire()
196 pickle.dump(args, open(log_file_name, 'ab'))
197 log_file_lock.release()
202 class os_notification:
204 """OpenSecurity '/notification' handler.
206 This is called on GET /notification?msgtype=TYPE&text=TEXT.
207 This will pop up an OpenSecurity notifcation window
219 if not "msgtype" in args:
220 raise web.badrequest('no msgtype given')
222 if not args.msgtype in ['information', 'warning', 'critical']:
223 raise web.badrequest('Unknown value for msgtype')
226 if not "text" in args:
227 raise web.badrequest('no text given')
229 # invoke the user dialog as a subprocess
230 dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.py')
231 process_command = [sys.executable, dlg_image, 'notification-' + args.msgtype, args.text]
232 process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)
239 """OpenSecurity '/password' handler.
241 This is called on GET /password?text=TEXT.
242 Ideally this should pop up a user dialog to insert his
243 password based device name.
252 if not "text" in args:
253 raise web.badrequest('no text given')
256 remote_ip = web.ctx.environ['REMOTE_ADDR']
258 # create the process which queries the user
259 dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
260 process_command = [sys.executable, dlg_image, 'password', args.text]
261 process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)
263 # run process result handling in seprate thread (not to block main one)
264 bouncer = ProcessResultBouncer(process, remote_ip, '/password')
267 return 'user queried for password'
269 def genNetworkDrive():
270 logical_drives = getLogicalDrives()
271 logger.info("Used logical drive letters: "+ str(logical_drives).strip('[]') )
272 drives = list(map(chr, range(68, 91)))
274 if drive not in logical_drives:
278 def getLogicalDrives():
279 drive_bitmask = ctypes.cdll.kernel32.GetLogicalDrives()
280 drives = list(itertools.compress(string.ascii_uppercase, map(lambda x:ord(x) - ord('0'), bin(drive_bitmask)[:1:-1])))
283 def getNetworkPath(drive):
284 return win32wnet.WNetGetConnection(drive+':')
286 def getDriveType(drive):
287 return ctypes.cdll.kernel32.GetDriveTypeW(u"%s:\\"%drive)
289 def getNetworkDrive(path):
290 for drive in getLogicalDrives():
291 #if is a network drive
292 if getDriveType(drive) == 4:
293 network_path = getNetworkPath(drive)
294 if path in network_path:
298 def mapDrive(drive, networkPath, user, password):
300 if (os.path.exists(networkPath)):
301 print networkPath, " is found..."
302 print "Trying to map ", networkPath, " on to ", drive, " ....."
304 win32wnet.WNetAddConnection2(win32netcon.RESOURCETYPE_DISK, drive, networkPath, None, user, password)
306 print "Unexpected error..."
308 print "Mapping successful"
311 print "Network path unreachable..."
314 # handles netumount request
315 class MountNetworkDriveHandler(threading.Thread):
317 def __init__(self, net_path):
318 threading.Thread.__init__(self)
319 self.networkPath = net_path
322 drive = genNetworkDrive()
324 logger.error("Failed to assign drive letter for: " + self.networkPath)
327 logger.info("Assigned drive " + drive + " to " + self.networkPath)
329 #Check for drive availability
331 if os.path.exists(drive):
332 logger.error("Drive letter is already in use: " + drive)
335 #Check for network resource availability
337 while not os.path.exists(self.networkPath):
340 logger.info("Path not accessible: " + self.networkPath + " retrying")
344 return mapDrive(drive, self.networkPath, "", "")
348 """OpenSecurity '/netmount' handler"""
354 # we _need_ a net_resource
355 if not "net_resource" in args:
356 raise web.badrequest('no net_resource given')
358 driveHandler = MountNetworkDriveHandler(args['net_resource'])
360 driveHandler.join(None)
363 def unmapDrive(drive, force=0):
364 print "drive in use, trying to unmap..."
366 print "Executing un-forced call..."
369 win32wnet.WNetCancelConnection2(drive, 1, force)
370 print drive, "successfully unmapped..."
373 print "Unmap failed, try again..."
376 # handles netumount request
377 class UmountNetworkDriveHandler(threading.Thread):
381 def __init__(self, path):
382 threading.Thread.__init__(self)
383 self.networkPath = path
387 drive = getNetworkDrive(self.networkPath)
389 logger.info("Failed to retrieve drive letter for: " + self.networkPath + ". Successfully deleted or missing.")
393 logger.info("Unmounting drive " + drive + " for " + self.networkPath)
394 result = unmapDrive(drive, force=1)
401 """OpenSecurity '/netumount' handler"""
407 # we _need_ a net_resource
408 if not "net_resource" in args:
409 raise web.badrequest('no net_resource given')
411 driveHandler = UmountNetworkDriveHandler(args['net_resource'])
413 driveHandler.join(None)
418 """OpenSecurity '/netcleanup' handler"""
424 # we _need_ a net_resource
425 if not "hostonly_ip" in args:
426 raise web.badrequest('no hostonly_ip given')
428 ip = args['hostonly_ip']
429 ip = ip[:ip.rindex('.')]
430 drives = getLogicalDrives()
432 # found network drive
433 if getDriveType(drive) == 4:
434 path = getNetworkPath(drive)
436 driveHandler = UmountNetworkDriveHandler(path)
438 driveHandler.join(None)
442 """OpenSecurity '/' handler"""
446 res = "OpenSecurity-Client RESTFul Server { \"version\": \"%s\" }" % opensecurity.__version__
448 # add some sample links
454 (copy paste this into your browser's address field after the host:port)
456 /password?text=Give+me+a+password+for+device+%22My+USB+Drive%22+(ID%3A+32090-AAA-X0)
458 (eg.: http://127.0.0.1:8090/password?text=Give+me+a+password+for+device+%22My+USB+Drive%22+(ID%3A+32090-AAA-X0))
459 NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
462 Request a combination of user and password:
463 (copy paste this into your browser's address field after the host:port)
465 /credentials?text=Tell+the+NSA+which+credentials+to+use+in+order+to+avoid+hacking+noise+on+wire.
467 (eg.: http://127.0.0.1:8090/credentials?text=Tell+the+NSA+which+credentials+to+use+in+order+to+avoid+hacking+noise+on+wire.)
468 NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
471 Request a combination of password and keyfile:
472 (copy paste this into your browser's address field after the host:port)
474 /keyfile?text=Your%20private%20RSA%20Keyfile%3A
476 (eg.: http://127.0.0.1:8090//keyfile?text=Your%20private%20RSA%20Keyfile%3A)
477 NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
481 (copy paste this into your browser's address field after the host:port)
483 /application?vm=Debian+7&app=Browser
485 (e.g. http://127.0.0.1:8090/application?vm=Debian+7&app=Browser)
491 class ProcessResultBouncer(threading.Thread):
493 """A class to post the result of a given process - assuming it to be in JSON - to a REST Api."""
495 def __init__(self, process, remote_ip, resource):
499 threading.Thread.__init__(self)
500 self._process = process
501 self._remote_ip = remote_ip
502 self._resource = resource
515 # invoke the user dialog as a subprocess
516 result = self._process.communicate()[0]
517 if self._process.returncode != 0:
518 print 'user request has been aborted.'
521 # all ok, tell send request back appropriate destination
523 j = json.loads(result)
525 print 'error in password parsing'
528 # by provided a 'data' we turn this into a POST statement
529 url_addr = 'http://' + self._remote_ip + ':58080' + self._resource
530 req = urllib2.Request(url_addr, urllib.urlencode(j))
532 res = urllib2.urlopen(req)
534 print 'failed to contact: ' + url_addr
538 class RESTServerThread(threading.Thread):
540 """Thread for serving the REST API."""
542 def __init__(self, port):
545 threading.Thread.__init__(self)
561 def is_already_running(port = 8090):
563 """check if this is started twice"""
566 s = socket.create_connection(('127.0.0.1', port), 0.5)
573 def _bounce_vm_logs():
575 """grab all logs from the VMs and push them to the log servers"""
579 # pick the highest current number
581 for f in glob.iglob(os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.*')):
583 n = f.split('.')[-1:][0]
591 # first add new vm logs to our existing one: rename the log file
592 log_file_name_new = os.path.join(Environment('OpenSecurity').log_path, 'vm_new.log')
593 log_file_name_cur = os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.' + str(cur))
594 log_file_lock.acquire()
596 os.rename(log_file_name_new, log_file_name_cur)
597 print('new log file: ' + log_file_name_cur)
600 log_file_lock.release()
602 # now we have a list of next log files to dump
603 log_files = glob.glob(os.path.join(Environment('OpenSecurity').log_path, 'vm_cur.log.*'))
605 for log_file in log_files:
608 f = open(log_file, 'rb')
618 logger.warning('tried to delete log file (pushed to EOF) "' + log_file + '" but failed')
621 logger.warning('encountered error while pushing log file "' + log_file + '"')
624 # start bouncer again ...
625 global log_file_bouncer
626 log_file_bouncer = threading.Timer(5.0, _bounce_vm_logs)
627 log_file_bouncer.start()
631 """POST a single log to log server
633 @param log the log POST param
636 log_server_url = "http://extern.x-net.at/opensecurity/log"
638 key = win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE, 'SOFTWARE\OpenSecurity')
639 log_server_url = str(win32api.RegQueryValueEx(key, 'LogServerURL')[0])
640 win32api.RegCloseKey(key)
642 logger.warning('Cannot open Registry HKEY_LOCAL_MACHINE\SOFTWARE\OpenSecurity and get "LogServerURL" value, using default instead')
644 # by provided a 'data' we turn this into a POST statement
645 d = urllib.urlencode(log)
646 req = urllib2.Request(log_server_url, d)
648 logger.debug('pushed log to server: ' + str(log_server_url))
653 """Start the REST server"""
657 # start the VM-log bouncer timer
658 global log_file_bouncer
659 log_file_bouncer = threading.Timer(5.0, _bounce_vm_logs)
660 log_file_bouncer.start()
662 # trick the web.py server
663 sys.argv = [__file__, str(port)]
664 server = web.application(opensecurity_urls, globals())
668 def serve(port = 8090, background = False):
670 """Start serving the REST Api
671 port ... port number to listen on
672 background ... cease into background (spawn thread) and return immediately"""
674 # start threaded or direct version
675 if background == True:
676 t = RESTServerThread(port)
683 """Stop serving the REST Api"""
689 global log_file_bouncer
690 if log_file_bouncer is not None:
691 log_file_bouncer.cancel()
696 if __name__ == "__main__":