diff --git a/mopidy/http/__init__.py b/mopidy/http/__init__.py index 3fa4bcd6..4f6239b9 100644 --- a/mopidy/http/__init__.py +++ b/mopidy/http/__init__.py @@ -25,6 +25,7 @@ class Extension(ext.Extension): schema['port'] = config_lib.Port() schema['static_dir'] = config_lib.Path(optional=True) schema['zeroconf'] = config_lib.String(optional=True) + schema['allowed_origins'] = config_lib.List(optional=True) return schema def validate_environment(self): diff --git a/mopidy/http/ext.conf b/mopidy/http/ext.conf index d35229bc..5750e855 100644 --- a/mopidy/http/ext.conf +++ b/mopidy/http/ext.conf @@ -4,3 +4,4 @@ hostname = 127.0.0.1 port = 6680 static_dir = zeroconf = Mopidy HTTP server on $hostname +allowed_origins = diff --git a/mopidy/http/handlers.py b/mopidy/http/handlers.py index 6250163c..2bc037a6 100644 --- a/mopidy/http/handlers.py +++ b/mopidy/http/handlers.py @@ -11,20 +11,24 @@ import tornado.websocket import mopidy from mopidy import core, models +from mopidy.compat import urllib from mopidy.internal import encoding, jsonrpc - logger = logging.getLogger(__name__) def make_mopidy_app_factory(apps, statics): def mopidy_app_factory(config, core): + origin_whitelist = { + x.lower() for x in config['http']['allowed_origins'] if x + } return [ (r'/ws/?', WebSocketHandler, { 'core': core, }), (r'/rpc', JsonRpcHandler, { 'core': core, + 'origin_whitelist': origin_whitelist, }), (r'/(.+)', StaticFileHandler, { 'path': os.path.join(os.path.dirname(__file__), 'data'), @@ -143,16 +147,31 @@ def set_mopidy_headers(request_handler): 'X-Mopidy-Version', mopidy.__version__.encode('utf-8')) +def check_origin(origin, request_headers, origin_whitelist): + if origin is None: + logger.debug('Origin was not set') + return False + origin_whitelist.add(request_headers.get('Host', None)) + parsed_origin = urllib.parse.urlparse(origin).netloc.lower() + return parsed_origin and parsed_origin in origin_whitelist + + class JsonRpcHandler(tornado.web.RequestHandler): - def initialize(self, core): + def initialize(self, core, origin_whitelist): self.jsonrpc = make_jsonrpc_wrapper(core) + self.origin_whitelist = origin_whitelist def head(self): self.set_extra_headers() self.finish() def post(self): + content_type = self.request.headers.get("Content-Type", '') + if content_type != 'application/json': + self.set_status(406, 'Content-Type must be application/json') + return + data = self.request.body if not data: return @@ -177,6 +196,18 @@ class JsonRpcHandler(tornado.web.RequestHandler): self.set_header('Accept', 'application/json') self.set_header('Content-Type', 'application/json; utf-8') + def options(self): + origin = self.request.headers.get('Origin', None) + if not check_origin(origin, self.request.headers, + self.origin_whitelist): + self.set_status(403, 'Access denied for origin %s' % origin) + return + + self.set_header("Access-Control-Allow-Origin", "%s" % origin) + self.set_header("Access-Control-Allow-Headers", "Content-Type") + self.set_status(204) + self.finish() + class ClientListHandler(tornado.web.RequestHandler):