From 6a8789050cbda9fe05b7e012700d815a3457c0d2 Mon Sep 17 00:00:00 2001 From: Job Evers-Meltzer Date: Fri, 18 Nov 2016 12:26:04 -0600 Subject: [PATCH] Allow 0.0.0.0 for api interface For a host to be able to access the daemon running inside a docker container the damon needs to be listening to 0.0.0.0 - move the API_INTERFACE setting to the adjustablesettings - check the port matches as well as the interface --- lbrynet/conf.py | 8 ++- lbrynet/lbrynet_daemon/auth/server.py | 37 ++++++++--- tests/unit/lbrynet_daemon/auth/__init__.py | 0 tests/unit/lbrynet_daemon/auth/test_server.py | 61 +++++++++++++++++++ 4 files changed, 97 insertions(+), 9 deletions(-) create mode 100644 tests/unit/lbrynet_daemon/auth/__init__.py create mode 100644 tests/unit/lbrynet_daemon/auth/test_server.py diff --git a/lbrynet/conf.py b/lbrynet/conf.py index cfab59b08..1f840e1b4 100644 --- a/lbrynet/conf.py +++ b/lbrynet/conf.py @@ -170,7 +170,12 @@ ENVIRONMENT = Env( # # TODO: writing json on the cmd line is a pain, come up with a nicer # parser for this data structure. (maybe MAX_KEY_FEE=USD:25 - max_key_fee=(json.loads, {'USD': {'amount': 25.0, 'address': ''}}) + max_key_fee=(json.loads, {'USD': {'amount': 25.0, 'address': ''}}), + # Changing this value is not-advised as it could potentially + # expose the lbrynet daemon to the outside world which would + # give an attacker access to your wallet and you could lose + # all of your credits. + API_INTERFACE=(str, "localhost"), ) @@ -205,7 +210,6 @@ class ApplicationSettings(Settings): self.LOG_FILE_NAME = "lbrynet.log" self.LOG_POST_URL = "https://lbry.io/log-upload" self.CRYPTSD_FILE_EXTENSION = ".cryptsd" - self.API_INTERFACE = "localhost" self.API_ADDRESS = "lbryapi" self.ICON_PATH = "icons" if platform is WINDOWS else "app.icns" self.APP_NAME = "LBRY" diff --git a/lbrynet/lbrynet_daemon/auth/server.py b/lbrynet/lbrynet_daemon/auth/server.py index a0f6b8355..75b5f7712 100644 --- a/lbrynet/lbrynet_daemon/auth/server.py +++ b/lbrynet/lbrynet_daemon/auth/server.py @@ -1,4 +1,6 @@ import logging +import urlparse + from decimal import Decimal from zope.interface import implements from twisted.web import server, resource @@ -219,16 +221,37 @@ class AuthJSONRPCServer(AuthorizedBase): request.finish() def _check_headers(self, request): - origin = request.getHeader("Origin") - referer = request.getHeader("Referer") - if origin not in [None, settings.ORIGIN]: - log.warning("Attempted api call from %s", origin) - return False - if referer is not None and not referer.startswith(settings.REFERER): - log.warning("Attempted api call from %s", referer) + return ( + self._check_header_source(request, 'Origin') and + self._check_header_source(request, 'Referer')) + + def _check_header_source(self, request, header): + """Check if the source of the request is allowed based on the header value.""" + source = request.getHeader(header) + if not self._check_source_of_request(source): + log.warning("Attempted api call from invalid %s: %s", header, source) return False return True + def _check_source_of_request(self, source): + if source is None: + return True + if settings.API_INTERFACE == '0.0.0.0': + return True + server, port = self.get_server_port(source) + return ( + server == settings.API_INTERFACE and + port == settings.api_port) + + def get_server_port(self, origin): + parsed = urlparse.urlparse(origin) + server_port = parsed.netloc.split(':') + assert len(server_port) <= 2 + if len(server_port) == 2: + return server_port[0], int(server_port[1]) + else: + return server_port[0], 80 + def _check_function_path(self, function_path): if function_path not in self.callable_methods: log.warning("Unknown method: %s", function_path) diff --git a/tests/unit/lbrynet_daemon/auth/__init__.py b/tests/unit/lbrynet_daemon/auth/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/lbrynet_daemon/auth/test_server.py b/tests/unit/lbrynet_daemon/auth/test_server.py new file mode 100644 index 000000000..2410925f2 --- /dev/null +++ b/tests/unit/lbrynet_daemon/auth/test_server.py @@ -0,0 +1,61 @@ +import mock +import requests +from twisted.trial import unittest + +from lbrynet import conf +from lbrynet.lbrynet_daemon.auth import server + + +class AuthJSONRPCServerTest(unittest.TestCase): + # TODO: move to using a base class for tests + # and add useful general utilities like this + # onto it. + def setUp(self): + self.server = server.AuthJSONRPCServer(False) + + def _set_setting(self, attr, value): + original = getattr(conf.settings, attr) + setattr(conf.settings, attr, value) + self.addCleanup(lambda: setattr(conf.settings, attr, original)) + + def test_get_server_port(self): + self.assertSequenceEqual( + ('example.com', 80), self.server.get_server_port('http://example.com')) + self.assertSequenceEqual( + ('example.com', 1234), self.server.get_server_port('http://example.com:1234')) + + def test_foreign_origin_is_rejected(self): + request = mock.Mock(['getHeader']) + request.getHeader = mock.Mock(return_value='http://example.com') + self.assertFalse(self.server._check_header_source(request, 'Origin')) + + def test_wrong_port_is_rejected(self): + self._set_setting('api_port', 1234) + request = mock.Mock(['getHeader']) + request.getHeader = mock.Mock(return_value='http://localhost:9999') + self.assertFalse(self.server._check_header_source(request, 'Origin')) + + def test_matching_origin_is_allowed(self): + self._set_setting('API_INTERFACE', 'example.com') + self._set_setting('api_port', 1234) + request = mock.Mock(['getHeader']) + request.getHeader = mock.Mock(return_value='http://example.com:1234') + self.assertTrue(self.server._check_header_source(request, 'Origin')) + + def test_any_origin_is_allowed(self): + self._set_setting('API_INTERFACE', '0.0.0.0') + self._set_setting('api_port', 80) + request = mock.Mock(['getHeader']) + request.getHeader = mock.Mock(return_value='http://example.com') + self.assertTrue(self.server._check_header_source(request, 'Origin')) + request = mock.Mock(['getHeader']) + request.getHeader = mock.Mock(return_value='http://another-example.com') + self.assertTrue(self.server._check_header_source(request, 'Origin')) + + def test_matching_referer_is_allowed(self): + self._set_setting('API_INTERFACE', 'the_api') + self._set_setting('api_port', 1111) + request = mock.Mock(['getHeader']) + request.getHeader = mock.Mock(return_value='http://the_api:1111?settings') + self.assertTrue(self.server._check_header_source(request, 'Referer')) + request.getHeader.assert_called_with('Referer')