from datetime import datetime from tornado.escape import json_encode, json_decode import tornado.ioloop import tornado.web import tornado.websocket import tornado.template import logging import json import time import asyncio from .mem import iris logger = logging.getLogger(__name__) class WebsocketHandler(tornado.websocket.WebSocketHandler): # initiate (not the actual object __init__, but run shortly after) def initialize(self, core, config): self.core = core self.config = config self.ioloop = tornado.ioloop.IOLoop.current() iris.ioloop = self.ioloop # Make available elsewhere in the Frontend def check_origin(self, origin): return True def open(self): # Get the client's IP. If it's local, then use it's proxy origin ip = self.request.remote_ip if ip == "127.0.0.1" and hasattr( self.request.headers, "X-Forwarded-For" ): ip = self.request.headers["X-Forwarded-For"] # Construct our initial client object, and add to our list of # connections client = { "connection_id": iris.generateGuid(), "ip": ip, "created": datetime.strftime(datetime.now(), "%Y-%m-%d %H:%M:%S"), } self.connection_id = client["connection_id"] iris.add_connection(connection=self, client=client) async def on_message(self, message): logger.debug("Iris websocket message received: " + message) message = json_decode(message) if "id" in message: id = message["id"] else: id = None if "jsonrpc" not in message: self.handle_result( id=id, error={ "id": id, "code": 32602, "message": ( "Invalid JSON-RPC request (missing ", "property 'jsonrpc')", ), }, ) if "params" in message: params = message["params"] # Handle hard-coded connection_id in messages # Otherwise include the origin connection of this message if "connection_id" not in params: message["params"]["connection_id"] = self.connection_id else: params = {} # call the method, as specified in payload if "method" in message: # make sure the method exists if hasattr(iris, message["method"]): try: # For async methods we need to await, but it must be # ommited for syncronous methods if asyncio.iscoroutinefunction( getattr(iris, message["method"]) ): await getattr(iris, message["method"])( ioloop=self.ioloop, data=params, callback=lambda response, error=False: self.handle_result( id=id, method=message["method"], response=response, error=error, ), ) else: getattr(iris, message["method"])( ioloop=self.ioloop, data=params, callback=lambda response, error=False: self.handle_result( id=id, method=message["method"], response=response, error=error, ), ) except Exception as e: logger.error(str(e)) else: self.handle_result( error={ "id": id, "code": 32601, "message": 'Method "' + message["method"] + '" does not exist', }, id=id, ) return else: self.handle_result( error={ "id": id, "code": 32602, "message": "Method key missing from request", }, id=id, ) return def on_close(self): iris.remove_connection(connection_id=self.connection_id) ## # Handle a response from our core # This is just our callback from an Async request ## def handle_result(self, *args, **kwargs): id = kwargs.get("id", False) method = kwargs.get("method", None) response = kwargs.get("response", None) error = kwargs.get("error", None) request_response = {"id": id, "jsonrpc": "2.0", "method": method} # We've been given an error if error: error["id"] = id request_response["error"] = error # We've been handed an AsyncHTTPClient callback. This is the case # when our request calls subsequent external requests (eg Spotify, # Genius) elif isinstance(response, tornado.httpclient.HTTPResponse): request_response["result"] = response.body # Just a regular json object, so not an external request else: request_response["result"] = response # Respond to the original request data = request_response data["recipient"] = self.connection_id iris.send_message(data=data) class HttpHandler(tornado.web.RequestHandler): def set_default_headers(self): self.set_header("Access-Control-Allow-Origin", "*") self.set_header( "Access-Control-Allow-Headers", ( "Origin, X-Requested-With, Content-Type, Accept, " "Authorization, Client-Security-Token, Accept-Encoding" ), ) def initialize(self, core, config): self.core = core self.config = config self.ioloop = tornado.ioloop.IOLoop.current() # Options request # This is a preflight request for CORS requests def options(self, slug=None): self.set_status(204) self.finish() async def get(self, slug=None): id = int(time.time()) # make sure the method exists if hasattr(iris, slug): try: # For async methods we need to await, but it must be ommited # for syncronous methods if asyncio.iscoroutinefunction(getattr(iris, slug)): await getattr(iris, slug)( ioloop=self.ioloop, request=self, callback=lambda response, error=False: self.handle_result( id=id, method=slug, response=response, error=error ), ) else: getattr(iris, slug)( ioloop=self.ioloop, request=self, callback=lambda response, error=False: self.handle_result( id=id, method=slug, response=response, error=error ), ) except Exception as e: logger.error(str(e)) else: self.handle_result( id=id, error={ "code": 32601, "message": "Method " + slug + " does not exist", }, ) return async def post(self, slug=None): id = int(time.time()) try: params = json.loads(self.request.body.decode("utf-8")) except BaseException: self.handle_result( id=id, error={"code": 32700, "message": "Missing or invalid payload"}, ) return # make sure the method exists if hasattr(iris, slug): try: if asyncio.iscoroutinefunction(getattr(iris, slug)): await getattr(iris, slug)( data=params, request=self.request, callback=lambda response=False, error=False: self.handle_result( id=id, method=slug, response=response, error=error ), ) else: getattr(iris, slug)( data=params, request=self.request, callback=lambda response=False, error=False: self.handle_result( id=id, method=slug, response=response, error=error ), ) except tornado.web.HTTPError: self.handle_result( id=id, error={"code": 32601, "message": "Invalid JSON payload"}, ) return else: self.handle_result( id=id, error={ "code": 32601, "message": "Method " + slug + " does not exist", }, ) return ## # Handle a response from our core # This is just our callback from an Async request ## def handle_result(self, *args, **kwargs): id = kwargs.get("id", None) method = kwargs.get("method", None) response = kwargs.get("response", None) error = kwargs.get("error", None) request_response = {"id": id, "jsonrpc": "2.0", "method": method} if error: request_response["error"] = error self.set_status(400) # We've been handed an AsyncHTTPClient callback. This is the case # when our request calls subsequent external requests. # We don't need to wrap non-HTTPResponse responses as these are dicts elif isinstance(response, tornado.httpclient.HTTPResponse): # Digest JSON responses into JSON content_type = response.headers.get("Content-Type") if content_type.startswith( "application/json" ) or content_type.startswith("text/json"): body = json.loads(response.body) # Non-JSON so just copy as-is else: body = json_encode(response.body) request_response["result"] = body # Regular ol successful response else: request_response["result"] = response # Write our response self.write(request_response) self.finish() ## # Customised handler for react router URLS # # This routes all URLs to the same path, so that React can handle the path etc ## class ReactRouterHandler(tornado.web.StaticFileHandler): def initialize(self, path): self.path = path self.absolute_path = path self.dirname = path.parent self.filename = path.name super().initialize(self.dirname) def get(self, path=None, include_body=True): return super().get(self.path, include_body)