Source code for pywwt.data_server

# Copyright 2021 the .NET Foundation
# Licensed under the three-clause BSD License

"""
An internal HTTP server for sending data to WWT when running outside of Jupyter.

You do not need to use this module unless you are a pywwt developer.
"""

import asyncio
from hashlib import md5
import logging
import mimetypes
import os.path
import socket
from threading import Thread
import time

__all__ = ['get_data_server']

_data_server = None


[docs]def get_data_server(verbose=True): """ This starts up a tornado server and returns a handle to a DataServer object which can be used to register files to serve. """ global _data_server if _data_server is not None: return _data_server from tornado.ioloop import IOLoop from tornado.web import RequestHandler, Application, StaticFileHandler from tornado.routing import PathMatches class WebServer(Application): host = None port = None def run(self, host=None, port=8886): self.host = host self.port = port try: self.listen(port) IOLoop.instance().start() finally: self.host = None self.port = None class DataServer(object): def start(self, app): self._files = {} self._thread = Thread(target=self.start_app) self._thread.daemon = True self._thread.start() self._app = app while self._app.host is None and self._app.port is None: time.sleep(0.1) @property def port(self): return self._app.port @property def host(self): return self._app.host def start_app(self): asyncio.set_event_loop(asyncio.new_event_loop()) host = socket.gethostbyname('localhost') sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('localhost', 0)) port = sock.getsockname()[1] sock.close() access_log = logging.getLogger("tornado.access") access_log.setLevel('ERROR') self._app.run(host=host, port=port) def serve_file(self, filename, extension=''): with open(filename, 'rb') as f: content = f.read() hash = md5(content).hexdigest() + extension self._files[hash] = os.path.abspath(filename) return 'http://' + self.host + ':' + str(self.port) + '/data/' + hash def static_url(self, rest): return 'http://' + self.host + ':' + str(self.port) + '/static/' + rest def get_file_contents(self, hash): with open(self._files[hash], 'rb') as f: return f.read() mimetypes.add_type('image/fits', '.fits') mimetypes.add_type('image/fits', '.fts') mimetypes.add_type('image/fits', '.fit') ds = DataServer() class DataHandler(RequestHandler): async def get(self, hash): # Do our best to set an appropriate Content-Type. filename = ds._files[hash] content_type = mimetypes.guess_type(filename)[0] if content_type is None: content_type = 'application/binary' self.set_header('Content-Type', content_type) # Add wide-open CORS headers to allow external WWT apps to access # data. This isn't needed in the default case, but comes in handy # when testing updates to the research app with an alternative # localhost port. Note that a hostile actor can just ignore these # settings, and our default stance is that data are globally # accessible, so this really shouldn't affect the level of security # we provide. self.set_header('Access-Control-Allow-Origin', '*') self.set_header('Access-Control-Allow-Methods', 'GET,HEAD') self.set_header('Access-Control-Allow-Headers', 'Content-Disposition,Content-Encoding,Content-Length,Content-Type') self.write(ds.get_file_contents(hash)) app = WebServer([ (PathMatches(r'/data/(?P<hash>\S+)'), DataHandler), (r'/static/(.*)', StaticFileHandler, { 'path': os.path.join(os.path.dirname(__file__), 'web_static'), 'default_filename': 'index.html', }), ]) ds.start(app) _data_server = ds return ds