Python3 Migrate
This commit is contained in:
273
venv/lib/python3.7/site-packages/mopidy/http/handlers.py
Normal file
273
venv/lib/python3.7/site-packages/mopidy/http/handlers.py
Normal file
@@ -0,0 +1,273 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user