diff options
-rw-r--r-- | fg21sim/webui/app.py | 13 | ||||
-rw-r--r-- | fg21sim/webui/websocket.py | 87 |
2 files changed, 88 insertions, 12 deletions
diff --git a/fg21sim/webui/app.py b/fg21sim/webui/app.py index 63d80be..4fa0a05 100644 --- a/fg21sim/webui/app.py +++ b/fg21sim/webui/app.py @@ -2,16 +2,20 @@ # MIT license """ -Web user interface (UI) of "fg21sim" based upon Tornado_. +Web user interface (UI) of "fg21sim" based upon Tornado_ web server and +using the WebSocket_ protocol. .. _Tornado: http://www.tornadoweb.org/ + +.. _WebSocket: https://en.wikipedia.org/wiki/WebSocket , + http://caniuse.com/#feat=websockets """ import os import tornado.web -from .websocket import EchoWSHandler +from .websocket import FG21simWSHandler class IndexHandler(tornado.web.RequestHandler): @@ -20,7 +24,8 @@ class IndexHandler(tornado.web.RequestHandler): _settings = { - # The static files will be served from the default "/static/" URI + # The static files will be served from the default "/static/" URI. + # Recommend to use `{{ static_url(filepath) }}` in the templates. "static_path": os.path.join(os.path.dirname(__file__), "static"), "template_path": os.path.join(os.path.dirname(__file__), "templates"), } @@ -32,6 +37,6 @@ def make_application(**kwargs): appplication = tornado.web.Application( handlers=[ (r"/", IndexHandler), - (r"/ws", EchoWSHandler), + (r"/ws", FG21simWSHandler), ], **settings) return appplication diff --git a/fg21sim/webui/websocket.py b/fg21sim/webui/websocket.py index ab846df..5b9d5ea 100644 --- a/fg21sim/webui/websocket.py +++ b/fg21sim/webui/websocket.py @@ -3,25 +3,96 @@ """ Communicate with the "fg21sim" simulation program through the Web UI using -the WebSocket_ technique, which provides full-duplex communication channels +the WebSocket_ protocol, which provides full-duplex communication channels over a single TCP connection. -.. _WebSocket: https://en.wikipedia.org/wiki/WebSocket , - http://caniuse.com/#feat=websockets +.. _WebSocket: https://en.wikipedia.org/wiki/WebSocket + + +References +---------- +- Tornado WebSocket: + http://www.tornadoweb.org/en/stable/websocket.html +- Can I Use: WebSocket: + http://caniuse.com/#feat=websockets """ +from urllib.parse import urlparse +import logging + import tornado.websocket -class EchoWSHandler(tornado.websocket.WebSocketHandler): +logger = logging.getLogger(__name__) + + +class FG21simWSHandler(tornado.websocket.WebSocketHandler): + """ + WebSocket for bi-directional communication between the Web UI and + the server, which can deal with the configurations and execute the + simulation task. + + Generally, WebSocket send and receive data as *string*. Therefore, + the more complex data are stringified as JSON string before sending, + which will be parsed after receive. + + Each message (as a JSON object or Python dictionary) has a ``type`` + field which will be used to determine the following action to take. + + Attributes + ---------- + name : str + Name to distinguish this WebSocket handle. + from_localhost : bool + Set to ``True`` if the access is from the localhost, + otherwise ``False``. + """ + name = "fg21sim" + from_localhost = None + def open(self): - print("WebSocket opened") + """Invoked when a new WebSocket is opened by the client.""" + logger.info("WebSocket: %s: opened" % self.name) def on_message(self, message): - print("Message received: %s" % message) + """Handle incoming messages.""" + logger.info("WebSocket: %s: received: %s" % (self.name, message)) msg_back = message[::-1] - print("Message sent back: %s" % msg_back) + logger.info("WebSocket: %s: sent: %s" % (self.name, msg_back)) self.write_message(msg_back) def on_close(self): - print("WebSocket closed") + """Invoked when a new WebSocket is closed by the client.""" + if hasattr(self, "close_code"): + code = self.close_code + if hasattr(self, "close_reason"): + reason = self.close_reason + logger.info("WebSocket: {0}: closed by client: {1}, {2}".format( + self.name, code, reason)) + + def check_origin(self, origin): + """Check the origin of the WebSocket access. + + Attributes + ---------- + from_localhost : bool + Set to ``True`` if the access is from the localhost, + otherwise ``False``. + + NOTE + ---- + Currently, only allow access from the ``localhost`` + (i.e., 127.0.0.1) and local LAN. + """ + logger.info("WebSocket: {0}: access origin: {1}".format( + self.name, origin)) + # NOTE: `urlparse`: `scheme://netloc/path;parameters?query#fragment` + netloc = urlparse(origin).netloc + # NOTE: `netloc` example: `user:pass@example.com:8080` + host = netloc.split("@")[-1].split(":")[0] + if host in ["localhost", "127.0.0.1"]: + self.from_localhost = True + logger.info("WebSocket: %s: access from localhost" % self.name) + return True + # XXX/TODO: check whether from the local LAN ?? + return False |