Files
iconograph/server/server.py

339 lines
9.3 KiB
Python
Raw Normal View History

2016-04-29 16:45:58 -07:00
#!/usr/bin/python3
import argparse
import json
2016-05-03 14:49:23 -07:00
import os
import pyinotify
2016-04-29 16:45:58 -07:00
import ssl
import subprocess
import sys
import threading
import time
import uuid
from ws4py import websocket
from ws4py.server import geventserver
from ws4py.server import wsgiutils
2016-04-29 16:45:58 -07:00
parser = argparse.ArgumentParser(description='iconograph https_server')
parser.add_argument(
'--ca-cert',
dest='ca_cert',
action='store',
required=True)
2016-05-03 14:49:23 -07:00
parser.add_argument(
'--image-path',
dest='image_path',
action='store',
required=True)
2016-05-03 23:58:48 +00:00
parser.add_argument(
'--image-type',
dest='image_types',
action='append',
required=True)
2016-04-29 16:45:58 -07:00
parser.add_argument(
'--listen-host',
dest='listen_host',
action='store',
default='::')
parser.add_argument(
'--listen-port',
dest='listen_port',
type=int,
action='store',
default=443)
parser.add_argument(
'--server-key',
dest='server_key',
action='store',
required=True)
parser.add_argument(
'--server-cert',
dest='server_cert',
action='store',
required=True)
parser.add_argument(
'--static-path',
dest='static_paths',
action='append')
parser.add_argument(
'--exec-handler',
dest='exec_handlers',
action='append')
2016-04-29 16:45:58 -07:00
FLAGS = parser.parse_args()
class WebSockets(object):
def __init__(self):
self.slaves = set()
self.masters = set()
2016-05-11 00:32:29 +00:00
self.targets = {}
def __iter__(self):
return iter(self.slaves | self.masters)
@staticmethod
def Broadcast(targets, msg):
msgstr = json.dumps(msg)
for target in targets:
2016-05-11 00:32:29 +00:00
target.send(msgstr)
2016-05-11 00:55:01 +00:00
def BroadcastTargets(self):
self.Broadcast(self.masters, {
'type': 'targets',
'data': {
'targets': list(self.targets.keys()),
},
})
class BaseWSHandler(websocket.WebSocket):
def opened(self, image_types):
self.send(json.dumps({
'type': 'image_types',
'data': {
'image_types': list(image_types),
},
2016-05-11 00:32:29 +00:00
}))
def GetSlaveWSHandler(image_types, websockets):
2016-05-11 00:32:29 +00:00
_hostname = None
class SlaveWSHandler(BaseWSHandler):
def opened(self):
super().opened(image_types)
websockets.slaves.add(self)
def closed(self, code, reason=None):
websockets.slaves.remove(self)
2016-05-11 00:32:29 +00:00
if self._hostname:
del websockets.targets[self._hostname]
2016-05-11 00:55:01 +00:00
websockets.BroadcastTargets()
def received_message(self, msg):
parsed = json.loads(str(msg))
if parsed['type'] == 'report':
newmsg = {
'type': 'report',
'id': str(uuid.uuid4()),
'received': int(time.time()),
'client': self.peer_address,
'data': parsed['data'],
}
websockets.Broadcast(websockets.masters, newmsg)
2016-05-11 00:32:29 +00:00
if 'hostname' in parsed['data']:
self._hostname = parsed['data']['hostname']
websockets.targets[self._hostname] = self
2016-05-11 00:55:01 +00:00
websockets.BroadcastTargets()
2016-05-11 00:32:29 +00:00
return SlaveWSHandler
def GetMasterWSHandler(image_types, websockets):
class MasterWSHandler(BaseWSHandler):
def opened(self):
super().opened(image_types)
websockets.masters.add(self)
2016-05-11 00:55:01 +00:00
self.send(json.dumps({
'type': 'targets',
'data': {
'targets': list(websockets.targets.keys()),
},
}))
def closed(self, code, reason=None):
websockets.masters.remove(self)
def received_message(self, msg):
2016-05-11 00:32:29 +00:00
parsed = json.loads(str(msg))
if parsed['type'] == 'command':
target = parsed['target']
if target not in websockets.targets:
return
newmsg = {
'type': 'command',
'target': target,
'id': str(uuid.uuid4()),
'received': int(time.time()),
'client': self.peer_address,
'data': parsed['data'],
}
websockets.targets[target].send(json.dumps(newmsg))
return MasterWSHandler
class INotifyHandler(pyinotify.ProcessEvent):
def __init__(self, websockets):
self._websockets = websockets
def process_IN_MOVED_TO(self, event):
if event.name != 'manifest.json':
return
image_type = os.path.basename(event.path)
self._websockets.Broadcast(self._websockets, {
'type': 'new_manifest',
'data': {
'image_type': image_type,
},
})
class HTTPRequestHandler(object):
2016-05-03 14:09:45 -07:00
2016-05-03 14:49:23 -07:00
_MIME_TYPES = {
2016-05-05 00:36:22 +00:00
'.css': 'text/css',
2016-05-05 00:15:45 +00:00
'.html': 'text/html',
2016-05-03 14:49:23 -07:00
'.iso': 'application/octet-stream',
2016-05-05 00:33:30 +00:00
'.js': 'application/javascript',
2016-05-03 14:49:23 -07:00
'.json': 'application/json',
'.tar': 'application/x-tar',
2016-05-09 23:13:49 +00:00
'.woff': 'application/font-woff',
2016-05-03 14:49:23 -07:00
}
_BLOCK_SIZE = 2 ** 16
def __init__(self, image_path, image_types, exec_handlers, static_paths, websockets):
2016-05-03 14:49:23 -07:00
self._image_path = image_path
2016-05-03 23:58:48 +00:00
self._image_types = image_types
self._exec_handlers = exec_handlers
self._static_paths = static_paths
self._static_paths['static'] = os.path.join(os.path.dirname(sys.argv[0]), 'static')
slave_ws_handler = GetSlaveWSHandler(image_types, websockets)
2016-05-05 00:33:30 +00:00
self._slave_ws_handler = wsgiutils.WebSocketWSGIApplication(
protocols=['iconograph-slave'],
handler_cls=slave_ws_handler)
master_ws_handler = GetMasterWSHandler(image_types, websockets)
2016-05-05 00:33:30 +00:00
self._master_ws_handler = wsgiutils.WebSocketWSGIApplication(
protocols=['iconograph-master'],
handler_cls=master_ws_handler)
2016-05-03 14:49:23 -07:00
2016-05-03 14:09:45 -07:00
def __call__(self, env, start_response):
2016-05-03 14:49:23 -07:00
path = env['PATH_INFO']
2016-05-05 00:15:45 +00:00
if path == '/':
path = '/static/root.html'
for url, file_path in self._static_paths.items():
if path.startswith('/%s/' % url):
file_name = path[len(url) + 2:]
return self._ServeStaticFile(start_response, file_path, file_name)
2016-05-03 14:49:23 -07:00
if path.startswith('/image/'):
image_type, image_name = path[7:].split('/', 1)
return self._ServeImageFile(start_response, image_type, image_name)
elif path.startswith('/exec/'):
method = path[6:]
return self._ServeExec(start_response, method, env['QUERY_STRING'])
elif path == '/ws/slave':
return self._slave_ws_handler(env, start_response)
elif path == '/ws/master':
return self._master_ws_handler(env, start_response)
2016-05-03 14:49:23 -07:00
start_response('404 Not found', [('Content-Type', 'text/plain')])
return [b'Not found']
def _MIMEType(self, file_name):
for suffix, mime_type in self._MIME_TYPES.items():
if file_name.endswith(suffix):
return mime_type
def _ServeImageFile(self, start_response, image_type, image_name):
# Sanitize inputs
image_type = os.path.basename(image_type)
image_name = os.path.basename(image_name)
assert not image_type.startswith('.')
assert not image_name.startswith('.')
2016-05-03 23:58:48 +00:00
assert image_type in self._image_types
2016-05-03 14:49:23 -07:00
file_path = os.path.join(self._image_path, image_type, image_name)
try:
with open(file_path, 'rb') as fh:
start_response('200 OK', [('Content-Type', self._MIMEType(image_name))])
while True:
block = fh.read(self._BLOCK_SIZE)
if len(block) == 0:
break
yield block
except FileNotFoundError:
2016-05-11 23:19:43 +00:00
start_response('404 Not found', [('Content-Type', 'text/plain')])
2016-05-03 14:49:23 -07:00
return []
def _ServeStaticFile(self, start_response, file_path, file_name):
2016-05-05 00:15:45 +00:00
file_name = os.path.basename(file_name)
assert not file_name.startswith('.')
full_path = os.path.join(file_path, file_name)
2016-05-05 00:15:45 +00:00
try:
with open(full_path, 'rb') as fh:
2016-05-05 00:15:45 +00:00
start_response('200 OK', [('Content-Type', self._MIMEType(file_name))])
return [fh.read()]
except FileNotFoundError:
2016-05-11 23:19:43 +00:00
start_response('404 Not found', [('Content-Type', 'text/plain')])
2016-05-05 00:15:45 +00:00
return []
2016-05-03 14:09:45 -07:00
def _ServeExec(self, start_response, method, arg):
handler = self._exec_handlers[method]
start_response('200 OK', [])
proc = subprocess.Popen(
[handler, arg],
stdout=subprocess.PIPE)
while True:
block = proc.stdout.read(self._BLOCK_SIZE)
if len(block) == 0:
break
yield block
proc.wait()
2016-05-03 14:09:45 -07:00
class Server(object):
2016-04-29 16:45:58 -07:00
def __init__(self, listen_host, listen_port, server_key, server_cert, ca_cert, image_path, image_types, exec_handlers, static_paths):
websockets = WebSockets()
2016-04-29 16:45:58 -07:00
wm = pyinotify.WatchManager()
inotify_handler = INotifyHandler(websockets)
self._notifier = pyinotify.Notifier(wm, inotify_handler)
2016-05-03 23:58:48 +00:00
for image_type in image_types:
type_path = os.path.join(image_path, image_type)
wm.add_watch(type_path, pyinotify.IN_MOVED_TO)
2016-05-03 13:38:58 -07:00
exec_handlers = dict(x.split('=', 1) for x in (exec_handlers or []))
static_paths = dict(x.split('=', 1) for x in (static_paths or []))
http_handler = HTTPRequestHandler(image_path, image_types, exec_handlers, static_paths, websockets)
self._httpd = geventserver.WSGIServer(
2016-05-03 13:38:58 -07:00
(listen_host, listen_port),
http_handler,
2016-04-29 16:45:58 -07:00
keyfile=server_key,
certfile=server_cert,
ca_certs=ca_cert,
cert_reqs=ssl.CERT_REQUIRED,
ssl_version=ssl.PROTOCOL_TLSv1_2)
def Serve(self):
self._notify_thread = threading.Thread(target=self._notifier.loop)
self._notify_thread.daemon = True
self._notify_thread.start()
2016-04-29 16:45:58 -07:00
self._httpd.serve_forever()
def main():
server = Server(
2016-04-29 16:45:58 -07:00
FLAGS.listen_host,
FLAGS.listen_port,
FLAGS.server_key,
FLAGS.server_cert,
2016-05-03 14:49:23 -07:00
FLAGS.ca_cert,
2016-05-03 23:58:48 +00:00
FLAGS.image_path,
set(FLAGS.image_types),
FLAGS.exec_handlers,
FLAGS.static_paths)
2016-04-29 16:45:58 -07:00
server.Serve()
if __name__ == '__main__':
main()