http: allow local files to access websocket (Fixes #1711)

check_origin() still ensures the Origin header is set but now only blocks
when missing from the allowed list *if* a network location was extracted
from the header. This prevents websocket connections originating from
local files (common in Apache Cordova apps such as Mopidy-Mobile) from
being blocked; these files don't really have a sensible value for Origin
so the client browser sets the header to something like 'file://' or
'null'.

Also added some tests for check_origin().
This commit is contained in:
Nick Steel 2018-10-08 22:26:36 +01:00
parent e025f04160
commit 6e9ed9e8a9
3 changed files with 57 additions and 2 deletions

View File

@ -5,6 +5,14 @@ Changelog
This changelog is used to track all major changes to Mopidy. 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) v2.2.0 (2018-09-30)
=================== ===================

View File

@ -150,11 +150,18 @@ def set_mopidy_headers(request_handler):
def check_origin(origin, request_headers, allowed_origins): def check_origin(origin, request_headers, allowed_origins):
if origin is None: if origin is None:
logger.debug('Origin was not set') logger.warn('HTTP request denied for missing Origin header')
return False return False
allowed_origins.add(request_headers.get('Host')) allowed_origins.add(request_headers.get('Host'))
parsed_origin = urllib.parse.urlparse(origin).netloc.lower() 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): class JsonRpcHandler(tornado.web.RequestHandler):

View File

@ -1,6 +1,7 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
import os import os
import unittest
import mock import mock
@ -86,3 +87,42 @@ class WebSocketHandlerTest(tornado.testing.AsyncHTTPTestCase):
for client in handlers.WebSocketHandler.clients: for client in handlers.WebSocketHandler.clients:
client.ws_connection = None client.ws_connection = None
handlers.WebSocketHandler.broadcast('message') 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))