diff --git a/docs/changelog.rst b/docs/changelog.rst index aefcd125..3eed6d02 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,14 @@ Changelog This changelog is used to track all major changes to Mopidy. +v2.2.1 (UNRELEASED) +=================== + +- HTTP: Stop blocking connections where the network location part of the Origin + header is empty, such as websocket connections originating from local files. + (Fixes: :issue:`1711`, PR: :issue:`1712`) + + v2.2.0 (2018-09-30) =================== diff --git a/mopidy/http/handlers.py b/mopidy/http/handlers.py index 1a16d43b..220cba80 100644 --- a/mopidy/http/handlers.py +++ b/mopidy/http/handlers.py @@ -150,11 +150,18 @@ def set_mopidy_headers(request_handler): def check_origin(origin, request_headers, allowed_origins): if origin is None: - logger.debug('Origin was not set') + logger.warn('HTTP request denied for missing Origin header') return False allowed_origins.add(request_headers.get('Host')) parsed_origin = urllib.parse.urlparse(origin).netloc.lower() - return parsed_origin and parsed_origin in allowed_origins + # 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.warn('HTTP request denied for Origin "%s"', origin) + return False + return True class JsonRpcHandler(tornado.web.RequestHandler): diff --git a/tests/http/test_handlers.py b/tests/http/test_handlers.py index 23472499..5941d6b0 100644 --- a/tests/http/test_handlers.py +++ b/tests/http/test_handlers.py @@ -1,6 +1,7 @@ from __future__ import absolute_import, unicode_literals import os +import unittest import mock @@ -86,3 +87,42 @@ class WebSocketHandlerTest(tornado.testing.AsyncHTTPTestCase): for client in handlers.WebSocketHandler.clients: client.ws_connection = None handlers.WebSocketHandler.broadcast('message') + + +class CheckOriginTests(unittest.TestCase): + + def setUp(self): + self.headers = {'Host': 'localhost:6680'} + self.allowed = set() + + def test_missing_origin_blocked(self): + self.assertFalse(handlers.check_origin( + None, self.headers, self.allowed)) + + def test_empty_origin_allowed(self): + self.assertTrue(handlers.check_origin('', self.headers, self.allowed)) + + def test_chrome_file_origin_allowed(self): + self.assertTrue(handlers.check_origin( + 'file://', self.headers, self.allowed)) + + def test_firefox_null_origin_allowed(self): + self.assertTrue(handlers.check_origin( + 'null', self.headers, self.allowed)) + + def test_same_host_origin_allowed(self): + self.assertTrue(handlers.check_origin( + 'http://localhost:6680', self.headers, self.allowed)) + + def test_different_host_origin_blocked(self): + self.assertFalse(handlers.check_origin( + 'http://other:6680', self.headers, self.allowed)) + + def test_different_port_blocked(self): + self.assertFalse(handlers.check_origin( + 'http://localhost:80', self.headers, self.allowed)) + + def test_extra_origin_allowed(self): + self.allowed.add('other:6680') + self.assertTrue(handlers.check_origin( + 'http://other:6680', self.headers, self.allowed))