Files
Iris/mopidy_iris/handlers.py

346 lines
12 KiB
Python
Raw Normal View History

2017-02-18 12:45:48 +13:00
from datetime import datetime
from tornado.escape import json_encode, json_decode
2020-02-03 16:48:39 +13:00
import tornado.ioloop
import tornado.web
import tornado.websocket
import tornado.template
import logging
import json
import time
import asyncio
from .mem import iris
2017-02-18 12:45:48 +13:00
logger = logging.getLogger(__name__)
2020-02-03 16:14:04 +13:00
2017-02-18 12:45:48 +13:00
class WebsocketHandler(tornado.websocket.WebSocketHandler):
2017-02-18 12:45:48 +13:00
# 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()
2020-02-03 16:14:04 +13:00
iris.ioloop = self.ioloop # Make available elsewhere in the Frontend
2017-02-18 12:45:48 +13:00
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
2020-02-03 16:48:39 +13:00
if ip == "127.0.0.1" and hasattr(
2020-02-06 10:12:28 +13:00
self.request.headers, "X-Forwarded-For"
):
2020-02-03 16:14:04 +13:00
ip = self.request.headers["X-Forwarded-For"]
2020-02-03 16:48:39 +13:00
# Construct our initial client object, and add to our list of
# connections
2017-02-18 12:45:48 +13:00
client = {
2020-02-03 16:14:04 +13:00
"connection_id": iris.generateGuid(),
"ip": ip,
"created": datetime.strftime(datetime.now(), "%Y-%m-%d %H:%M:%S"),
2017-02-18 12:45:48 +13:00
}
2020-02-03 16:14:04 +13:00
self.connection_id = client["connection_id"]
iris.add_connection(connection=self, client=client)
async def on_message(self, message):
2020-02-03 16:14:04 +13:00
logger.debug("Iris websocket message received: " + message)
2017-02-18 12:45:48 +13:00
message = json_decode(message)
2020-02-03 16:14:04 +13:00
if "id" in message:
id = message["id"]
2017-02-18 12:45:48 +13:00
else:
id = None
2020-02-03 16:14:04 +13:00
if "jsonrpc" not in message:
self.handle_result(
id=id,
error={
"id": id,
"code": 32602,
"message": (
"Invalid JSON-RPC request (missing ",
2020-02-07 08:08:21 +13:00
"property 'jsonrpc')",
),
2020-02-03 16:14:04 +13:00
},
)
2020-02-03 16:14:04 +13:00
if "params" in message:
params = message["params"]
2017-02-18 12:45:48 +13:00
2018-03-13 08:57:40 +13:00
# Handle hard-coded connection_id in messages
# Otherwise include the origin connection of this message
2020-02-03 16:14:04 +13:00
if "connection_id" not in params:
message["params"]["connection_id"] = self.connection_id
2018-03-13 08:57:40 +13:00
else:
params = {}
2017-02-18 12:45:48 +13:00
# call the method, as specified in payload
2020-02-03 16:14:04 +13:00
if "method" in message:
2017-02-18 12:45:48 +13:00
# make sure the method exists
2020-02-03 16:14:04 +13:00
if hasattr(iris, message["method"]):
try:
2020-02-03 16:48:39 +13:00
# For async methods we need to await, but it must be
# ommited for syncronous methods
if asyncio.iscoroutinefunction(
2020-02-06 10:12:28 +13:00
getattr(iris, message["method"])
):
2020-02-03 16:14:04 +13:00
await getattr(iris, message["method"])(
ioloop=self.ioloop,
data=params,
2020-02-06 10:12:28 +13:00
callback=lambda response, error=False: self.handle_result(
id=id,
2020-02-03 16:14:04 +13:00
method=message["method"],
response=response,
2020-02-03 16:14:04 +13:00
error=error,
),
)
else:
getattr(iris, message["method"])(
ioloop=self.ioloop,
data=params,
callback=lambda response, error=False: self.handle_result(
id=id,
2020-02-03 16:14:04 +13:00
method=message["method"],
response=response,
2020-02-03 16:14:04 +13:00
error=error,
),
)
except Exception as e:
logger.error(str(e))
2017-02-18 12:45:48 +13:00
else:
2020-02-03 16:14:04 +13:00
self.handle_result(
error={
"id": id,
"code": 32601,
2020-02-06 10:12:28 +13:00
"message": 'Method "'
+ message["method"]
+ '" does not exist',
2020-02-03 16:14:04 +13:00
},
id=id,
)
2017-10-20 21:49:41 +13:00
return
2017-02-18 12:45:48 +13:00
else:
2020-02-03 16:14:04 +13:00
self.handle_result(
error={
"id": id,
"code": 32602,
"message": "Method key missing from request",
},
id=id,
)
2017-10-20 21:49:41 +13:00
return
2017-02-18 12:45:48 +13:00
def on_close(self):
iris.remove_connection(connection_id=self.connection_id)
2017-02-18 12:45:48 +13:00
##
# Handle a response from our core
# This is just our callback from an Async request
##
2018-03-19 16:09:27 +13:00
def handle_result(self, *args, **kwargs):
2020-02-03 16:14:04 +13:00
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}
2017-10-20 21:49:41 +13:00
# We've been given an error
if error:
2020-02-03 16:14:04 +13:00
error["id"] = id
request_response["error"] = error
2017-10-20 21:49:41 +13:00
# We've been handed an AsyncHTTPClient callback. This is the case
2020-02-03 16:48:39 +13:00
# when our request calls subsequent external requests (eg Spotify,
# Genius)
2017-10-20 21:49:41 +13:00
elif isinstance(response, tornado.httpclient.HTTPResponse):
2020-02-03 16:14:04 +13:00
request_response["result"] = response.body
# Just a regular json object, so not an external request
else:
2020-02-03 16:14:04 +13:00
request_response["result"] = response
2017-10-20 21:49:41 +13:00
# Respond to the original request
data = request_response
2020-02-03 16:14:04 +13:00
data["recipient"] = self.connection_id
iris.send_message(data=data)
2017-02-18 12:45:48 +13:00
class HttpHandler(tornado.web.RequestHandler):
def set_default_headers(self):
self.set_header("Access-Control-Allow-Origin", "*")
2020-02-03 16:14:04 +13:00
self.set_header(
"Access-Control-Allow-Headers",
(
"Origin, X-Requested-With, Content-Type, Accept, "
"Authorization, Client-Security-Token, Accept-Encoding"
2020-02-07 08:08:21 +13:00
),
2020-02-03 16:14:04 +13:00
)
2017-02-18 12:45:48 +13:00
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):
2017-02-18 12:45:48 +13:00
2018-03-19 16:09:27 +13:00
id = int(time.time())
2017-02-18 22:04:05 +13:00
# make sure the method exists
if hasattr(iris, slug):
try:
2020-02-03 16:48:39 +13:00
# 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,
2020-02-06 10:12:28 +13:00
callback=lambda response, error=False: self.handle_result(
2020-02-03 16:14:04 +13:00
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:
2020-02-03 16:14:04 +13:00
self.handle_result(
id=id,
2020-02-03 16:48:39 +13:00
error={
"code": 32601,
2020-02-06 10:12:28 +13:00
"message": "Method " + slug + " does not exist",
},
2020-02-03 16:14:04 +13:00
)
2017-10-20 21:49:41 +13:00
return
async def post(self, slug=None):
2018-03-19 16:09:27 +13:00
id = int(time.time())
try:
2020-02-03 16:14:04 +13:00
params = json.loads(self.request.body.decode("utf-8"))
2020-02-03 16:48:39 +13:00
except BaseException:
2020-02-03 16:14:04 +13:00
self.handle_result(
2020-02-06 10:12:28 +13:00
id=id,
2020-02-07 08:08:21 +13:00
error={"code": 32700, "message": "Missing or invalid payload"},
2020-02-03 16:14:04 +13:00
)
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,
2020-02-06 10:12:28 +13:00
callback=lambda response=False, error=False: self.handle_result(
2020-02-03 16:14:04 +13:00
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
),
)
2020-02-03 16:48:39 +13:00
except tornado.web.HTTPError:
2020-02-03 16:14:04 +13:00
self.handle_result(
2020-02-06 10:12:28 +13:00
id=id,
error={"code": 32601, "message": "Invalid JSON payload"},
2020-02-03 16:14:04 +13:00
)
2017-10-20 21:49:41 +13:00
return
2017-02-18 12:45:48 +13:00
else:
2020-02-03 16:14:04 +13:00
self.handle_result(
id=id,
2020-02-03 16:48:39 +13:00
error={
"code": 32601,
2020-02-06 10:12:28 +13:00
"message": "Method " + slug + " does not exist",
},
2020-02-03 16:14:04 +13:00
)
2017-10-20 21:49:41 +13:00
return
##
# Handle a response from our core
# This is just our callback from an Async request
##
2018-03-19 16:09:27 +13:00
def handle_result(self, *args, **kwargs):
2020-02-03 16:14:04 +13:00
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}
2017-10-20 21:49:41 +13:00
if error:
2020-02-03 16:14:04 +13:00
request_response["error"] = error
2018-03-19 16:09:27 +13:00
self.set_status(400)
2017-10-20 21:49:41 +13:00
# We've been handed an AsyncHTTPClient callback. This is the case
2020-02-03 16:48:39 +13:00
# 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):
2017-10-20 21:49:41 +13:00
# Digest JSON responses into JSON
2020-02-03 16:14:04 +13:00
content_type = response.headers.get("Content-Type")
2020-02-03 16:48:39 +13:00
if content_type.startswith(
2020-02-06 10:12:28 +13:00
"application/json"
) or content_type.startswith("text/json"):
2020-01-22 16:19:57 +13:00
body = json.loads(response.body)
2017-10-20 21:49:41 +13:00
# Non-JSON so just copy as-is
else:
2020-01-22 16:19:57 +13:00
body = json_encode(response.body)
2020-02-03 16:14:04 +13:00
request_response["result"] = body
2017-10-20 21:49:41 +13:00
# Regular ol successful response
else:
2020-02-03 16:14:04 +13:00
request_response["result"] = response
2017-10-20 21:49:41 +13:00
# Write our response
2018-03-19 16:09:27 +13:00
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)