Files
Iris/mopidy_iris/handlers.py

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)