Python3 Migrate

This commit is contained in:
MariuszC
2020-01-18 20:01:00 +01:00
parent ea05af2d15
commit 6cd7e0fe44
691 changed files with 201846 additions and 598 deletions

View File

@@ -0,0 +1,53 @@
import logging
import os
import mopidy
from mopidy import config as config_lib
from mopidy import exceptions, ext
logger = logging.getLogger(__name__)
class Extension(ext.Extension):
dist_name = "Mopidy-HTTP"
ext_name = "http"
version = mopidy.__version__
def get_default_config(self):
conf_file = os.path.join(os.path.dirname(__file__), "ext.conf")
return config_lib.read(conf_file)
def get_config_schema(self):
schema = super().get_config_schema()
schema["hostname"] = config_lib.Hostname()
schema["port"] = config_lib.Port()
schema["static_dir"] = config_lib.Deprecated()
schema["zeroconf"] = config_lib.String(optional=True)
schema["allowed_origins"] = config_lib.List(optional=True)
schema["csrf_protection"] = config_lib.Boolean(optional=True)
schema["default_app"] = config_lib.String(optional=True)
return schema
def validate_environment(self):
try:
import tornado.web # noqa
except ImportError as e:
raise exceptions.ExtensionError("tornado library not found", e)
def setup(self, registry):
from .actor import HttpFrontend
from .handlers import make_mopidy_app_factory
HttpFrontend.apps = registry["http:app"]
HttpFrontend.statics = registry["http:static"]
registry.add("frontend", HttpFrontend)
registry.add(
"http:app",
{
"name": "mopidy",
"factory": make_mopidy_app_factory(
registry["http:app"], registry["http:static"]
),
},
)

View File

@@ -0,0 +1,210 @@
import json
import logging
import secrets
import threading
import pykka
import tornado.httpserver
import tornado.ioloop
import tornado.netutil
import tornado.web
import tornado.websocket
from mopidy import exceptions, models, zeroconf
from mopidy.core import CoreListener
from mopidy.http import Extension, handlers
from mopidy.internal import formatting, network
try:
import asyncio
except ImportError:
asyncio = None
logger = logging.getLogger(__name__)
class HttpFrontend(pykka.ThreadingActor, CoreListener):
apps = []
statics = []
def __init__(self, config, core):
super().__init__()
self.hostname = network.format_hostname(config["http"]["hostname"])
self.port = config["http"]["port"]
tornado_hostname = config["http"]["hostname"]
if tornado_hostname == "::":
tornado_hostname = None
try:
logger.debug("Starting HTTP server")
sockets = tornado.netutil.bind_sockets(self.port, tornado_hostname)
self.server = HttpServer(
config=config,
core=core,
sockets=sockets,
apps=self.apps,
statics=self.statics,
)
except OSError as exc:
raise exceptions.FrontendError(f"HTTP server startup failed: {exc}")
self.zeroconf_name = config["http"]["zeroconf"]
self.zeroconf_http = None
self.zeroconf_mopidy_http = None
def on_start(self):
logger.info("HTTP server running at [%s]:%s", self.hostname, self.port)
self.server.start()
if self.zeroconf_name:
self.zeroconf_http = zeroconf.Zeroconf(
name=self.zeroconf_name, stype="_http._tcp", port=self.port
)
self.zeroconf_mopidy_http = zeroconf.Zeroconf(
name=self.zeroconf_name,
stype="_mopidy-http._tcp",
port=self.port,
)
self.zeroconf_http.publish()
self.zeroconf_mopidy_http.publish()
def on_stop(self):
if self.zeroconf_http:
self.zeroconf_http.unpublish()
if self.zeroconf_mopidy_http:
self.zeroconf_mopidy_http.unpublish()
self.server.stop()
def on_event(self, name, **data):
on_event(name, self.server.io_loop, **data)
def on_event(name, io_loop, **data):
event = data
event["event"] = name
message = json.dumps(event, cls=models.ModelJSONEncoder)
handlers.WebSocketHandler.broadcast(message, io_loop)
class HttpServer(threading.Thread):
name = "HttpServer"
def __init__(self, config, core, sockets, apps, statics):
super().__init__()
self.config = config
self.core = core
self.sockets = sockets
self.apps = apps
self.statics = statics
self.app = None
self.server = None
self.io_loop = None
def run(self):
if asyncio:
# If asyncio is available, Tornado uses it as its IO loop. Since we
# start Tornado in a another thread than the main thread, we must
# explicitly create an asyncio loop for the current thread.
asyncio.set_event_loop(asyncio.new_event_loop())
self.app = tornado.web.Application(
self._get_request_handlers(),
cookie_secret=self._get_cookie_secret(),
)
self.server = tornado.httpserver.HTTPServer(self.app)
self.server.add_sockets(self.sockets)
self.io_loop = tornado.ioloop.IOLoop.current()
self.io_loop.start()
logger.debug("Stopped HTTP server")
def stop(self):
logger.debug("Stopping HTTP server")
self.io_loop.add_callback(self.io_loop.stop)
def _get_request_handlers(self):
request_handlers = []
request_handlers.extend(self._get_app_request_handlers())
request_handlers.extend(self._get_static_request_handlers())
request_handlers.extend(self._get_default_request_handlers())
logger.debug(
"HTTP routes from extensions: %s",
formatting.indent(
"\n".join(
f"{path!r}: {handler!r}"
for (path, handler, *_) in request_handlers
)
),
)
return request_handlers
def _get_app_request_handlers(self):
result = []
for app in self.apps:
try:
request_handlers = app["factory"](self.config, self.core)
except Exception:
logger.exception("Loading %s failed.", app["name"])
continue
result.append((f"/{app['name']}", handlers.AddSlashHandler))
for handler in request_handlers:
handler = list(handler)
handler[0] = f"/{app['name']}{handler[0]}"
result.append(tuple(handler))
logger.debug("Loaded HTTP extension: %s", app["name"])
return result
def _get_static_request_handlers(self):
result = []
for static in self.statics:
result.append((f"/{static['name']}", handlers.AddSlashHandler))
result.append(
(
f"/{static['name']}/(.*)",
handlers.StaticFileHandler,
{"path": static["path"], "default_filename": "index.html"},
)
)
logger.debug("Loaded static HTTP extension: %s", static["name"])
return result
def _get_default_request_handlers(self):
sites = [app["name"] for app in self.apps + self.statics]
default_app = self.config["http"]["default_app"]
if default_app not in sites:
logger.warning(
f"HTTP server's default app {default_app!r} not found"
)
default_app = "mopidy"
logger.debug(f"Default webclient is {default_app}")
return [
(
r"/",
tornado.web.RedirectHandler,
{"url": f"/{default_app}/", "permanent": False},
)
]
def _get_cookie_secret(self):
file_path = Extension.get_data_dir(self.config) / "cookie_secret"
if not file_path.is_file():
cookie_secret = secrets.token_hex(32)
file_path.write_text(cookie_secret)
else:
cookie_secret = file_path.read_text().strip()
if not cookie_secret:
logging.error(
f"HTTP server could not find cookie secret in {file_path}"
)
return cookie_secret

View File

@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mopidy</title>
<link rel="stylesheet" type="text/css" href="mopidy.css">
</head>
<body>
<div class="box focus">
<h1>Mopidy</h1>
<p>This web server is a part of the Mopidy music server. To learn more
about Mopidy, please visit
<a href="http://www.mopidy.com/">www.mopidy.com</a>.</p>
</div>
<div class="box">
<h2>Web clients</h2>
<ul>
{% for app in apps %}
<li><a href="/{{ url_escape(app) }}/">{{ escape(app) }}</a></li>
{% end %}
</ul>
<p>Web clients which are installed as Mopidy extensions will
automatically appear here.</p>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -0,0 +1,43 @@
html {
background: #f8f8f8;
color: #555;
font-family: Geneva, Tahoma, Verdana, sans-serif;
line-height: 1.4em;
}
body {
max-width: 600px;
margin: 0 auto;
}
h1, h2 {
font-weight: 500;
line-height: 1.1em;
}
a {
color: #555;
text-decoration: none;
border-bottom: 1px dotted;
}
img {
border: 0;
}
.box {
background: white;
box-shadow: 0px 5px 5px #f0f0f0;
margin: 1em;
padding: 1em;
}
.box.focus {
background: #465158;
color: #e8ecef;
}
.box a {
color: #465158;
}
.box a:hover {
opacity: 0.8;
}
.box.focus a {
color: #e8ecef;
}

View File

@@ -0,0 +1,8 @@
[http]
enabled = true
hostname = 127.0.0.1
port = 6680
zeroconf = Mopidy HTTP server on $hostname
allowed_origins =
csrf_protection = true
default_app = mopidy

View 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()