Files
mopidy-radionet/venv/lib/python3.7/site-packages/mopidy/http/handlers.py
2020-01-18 20:01:00 +01:00

274 lines
8.8 KiB
Python

import functools
import logging
import os
import urllib
import tornado.escape
import tornado.ioloop
import tornado.web
import tornado.websocket
import mopidy
from mopidy import core, models
from mopidy.internal import jsonrpc
logger = logging.getLogger(__name__)
def make_mopidy_app_factory(apps, statics):
def mopidy_app_factory(config, core):
if not config["http"]["csrf_protection"]:
logger.warning(
"HTTP Cross-Site Request Forgery protection is disabled"
)
allowed_origins = {
x.lower() for x in config["http"]["allowed_origins"] if x
}
return [
(
r"/ws/?",
WebSocketHandler,
{
"core": core,
"allowed_origins": allowed_origins,
"csrf_protection": config["http"]["csrf_protection"],
},
),
(
r"/rpc",
JsonRpcHandler,
{
"core": core,
"allowed_origins": allowed_origins,
"csrf_protection": config["http"]["csrf_protection"],
},
),
(
r"/(.+)",
StaticFileHandler,
{"path": os.path.join(os.path.dirname(__file__), "data")},
),
(r"/", ClientListHandler, {"apps": apps, "statics": statics}),
]
return mopidy_app_factory
def make_jsonrpc_wrapper(core_actor):
inspector = jsonrpc.JsonRpcInspector(
objects={
"core.get_uri_schemes": core.Core.get_uri_schemes,
"core.get_version": core.Core.get_version,
"core.history": core.HistoryController,
"core.library": core.LibraryController,
"core.mixer": core.MixerController,
"core.playback": core.PlaybackController,
"core.playlists": core.PlaylistsController,
"core.tracklist": core.TracklistController,
}
)
return jsonrpc.JsonRpcWrapper(
objects={
"core.describe": inspector.describe,
"core.get_uri_schemes": core_actor.get_uri_schemes,
"core.get_version": core_actor.get_version,
"core.history": core_actor.history,
"core.library": core_actor.library,
"core.mixer": core_actor.mixer,
"core.playback": core_actor.playback,
"core.playlists": core_actor.playlists,
"core.tracklist": core_actor.tracklist,
},
decoders=[models.model_json_decoder],
encoders=[models.ModelJSONEncoder],
)
def _send_broadcast(client, msg):
# We could check for client.ws_connection, but we don't really
# care why the broadcast failed, we just want the rest of them
# to succeed, so catch everything.
try:
client.write_message(msg)
except Exception as exc:
logger.debug(
f"Broadcast of WebSocket message to "
f"{client.request.remote_ip} failed: {exc}"
)
# TODO: should this do the same cleanup as the on_message code?
class WebSocketHandler(tornado.websocket.WebSocketHandler):
# XXX This set is shared by all WebSocketHandler objects. This isn't
# optimal, but there's currently no use case for having more than one of
# these anyway.
clients = set()
@classmethod
def broadcast(cls, msg, io_loop):
# This can be called from outside the Tornado ioloop, so we need to
# safely cross the thread boundary by adding a callback to the loop.
for client in cls.clients:
# One callback per client to keep time we hold up the loop short
io_loop.add_callback(
functools.partial(_send_broadcast, client, msg)
)
def initialize(self, core, allowed_origins, csrf_protection):
self.jsonrpc = make_jsonrpc_wrapper(core)
self.allowed_origins = allowed_origins
self.csrf_protection = csrf_protection
def open(self):
self.set_nodelay(True)
self.clients.add(self)
logger.debug("New WebSocket connection from %s", self.request.remote_ip)
def on_close(self):
self.clients.discard(self)
logger.debug(
"Closed WebSocket connection from %s", self.request.remote_ip
)
def on_message(self, message):
if not message:
return
logger.debug(
"Received WebSocket message from %s: %r",
self.request.remote_ip,
message,
)
try:
response = self.jsonrpc.handle_json(
tornado.escape.native_str(message)
)
if response and self.write_message(response):
logger.debug(
"Sent WebSocket message to %s: %r",
self.request.remote_ip,
response,
)
except Exception as exc:
logger.error(f"WebSocket request error: {exc}")
self.close()
def check_origin(self, origin):
if not self.csrf_protection:
return True
return check_origin(origin, self.request.headers, self.allowed_origins)
def set_mopidy_headers(request_handler):
request_handler.set_header("Cache-Control", "no-cache")
request_handler.set_header("X-Mopidy-Version", mopidy.__version__.encode())
def check_origin(origin, request_headers, allowed_origins):
if origin is None:
logger.warning("HTTP request denied for missing Origin header")
return False
allowed_origins.add(request_headers.get("Host"))
parsed_origin = urllib.parse.urlparse(origin).netloc.lower()
# Some frameworks (e.g. Apache Cordova) use local files. Requests from
# these files don't really have a sensible Origin so the browser sets the
# header to something like 'file://' or 'null'. This results here in an
# empty parsed_origin which we choose to allow.
if parsed_origin and parsed_origin not in allowed_origins:
logger.warning('HTTP request denied for Origin "%s"', origin)
return False
return True
class JsonRpcHandler(tornado.web.RequestHandler):
def initialize(self, core, allowed_origins, csrf_protection):
self.jsonrpc = make_jsonrpc_wrapper(core)
self.allowed_origins = allowed_origins
self.csrf_protection = csrf_protection
def head(self):
self.set_extra_headers()
self.finish()
def post(self):
if self.csrf_protection:
content_type = self.request.headers.get("Content-Type", "")
if content_type != "application/json":
self.set_status(415, "Content-Type must be application/json")
return
data = self.request.body
if not data:
return
logger.debug(
"Received RPC message from %s: %r", self.request.remote_ip, data
)
try:
self.set_extra_headers()
response = self.jsonrpc.handle_json(tornado.escape.native_str(data))
if response and self.write(response):
logger.debug(
"Sent RPC message to %s: %r",
self.request.remote_ip,
response,
)
except Exception as e:
logger.error("HTTP JSON-RPC request error: %s", e)
self.write_error(500)
def set_extra_headers(self):
set_mopidy_headers(self)
self.set_header("Accept", "application/json")
self.set_header("Content-Type", "application/json; utf-8")
def options(self):
if self.csrf_protection:
origin = self.request.headers.get("Origin")
if not check_origin(
origin, self.request.headers, self.allowed_origins
):
self.set_status(403, f"Access denied for origin {origin}")
return
self.set_header("Access-Control-Allow-Origin", f"{origin}")
self.set_header("Access-Control-Allow-Headers", "Content-Type")
self.set_status(204)
self.finish()
class ClientListHandler(tornado.web.RequestHandler):
def initialize(self, apps, statics):
self.apps = apps
self.statics = statics
def get_template_path(self):
return os.path.dirname(__file__)
def get(self):
set_mopidy_headers(self)
names = set()
for app in self.apps:
names.add(app["name"])
for static in self.statics:
names.add(static["name"])
names.discard("mopidy")
self.render("data/clients.html", apps=sorted(list(names)))
class StaticFileHandler(tornado.web.StaticFileHandler):
def set_extra_headers(self, path):
set_mopidy_headers(self)
class AddSlashHandler(tornado.web.RequestHandler):
@tornado.web.addslash
def prepare(self):
return super().prepare()