349 lines
12 KiB
Python
Executable File
349 lines
12 KiB
Python
Executable File
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)
|