From 4c01b47945e3be7275f30ecb64c10a554b215760 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Nov 2016 18:35:58 -0800 Subject: [PATCH 1/5] device_tracker.see should not call async methods (#4377) --- homeassistant/components/device_tracker/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 6c6ae3adea6..082602b09f8 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -242,7 +242,7 @@ class DeviceTracker(object): if device.track: device.update_ha_state() - self.hass.bus.async_fire(EVENT_NEW_DEVICE, device) + self.hass.bus.fire(EVENT_NEW_DEVICE, device) # During init, we ignore the group if self.group is not None: From 09c29737de16e8e08113afeb9b731d525fda9388 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Nov 2016 20:59:29 -0800 Subject: [PATCH 2/5] Fix device tracker sending invalid event data --- homeassistant/components/device_tracker/__init__.py | 7 +++++-- tests/components/device_tracker/test_init.py | 13 ++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 082602b09f8..13194f88894 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -31,7 +31,7 @@ from homeassistant.util.yaml import dump from homeassistant.helpers.event import track_utc_time_change from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, - DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME) + DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID) DOMAIN = 'device_tracker' DEPENDENCIES = ['zone'] @@ -242,7 +242,10 @@ class DeviceTracker(object): if device.track: device.update_ha_state() - self.hass.bus.fire(EVENT_NEW_DEVICE, device) + self.hass.bus.fire(EVENT_NEW_DEVICE, { + ATTR_ENTITY_ID: device.entity_id, + ATTR_HOST_NAME: device.host_name, + }) # During init, we ignore the group if self.group is not None: diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 1f95c38cd7f..e4576ec9830 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -1,5 +1,6 @@ """The tests for the device tracker component.""" # pylint: disable=protected-access +import json import logging import unittest from unittest.mock import call, patch @@ -15,6 +16,7 @@ from homeassistant.const import ( STATE_HOME, STATE_NOT_HOME, CONF_PLATFORM) import homeassistant.components.device_tracker as device_tracker from homeassistant.exceptions import HomeAssistantError +from homeassistant.remote import JSONEncoder from tests.common import ( get_test_home_assistant, fire_time_changed, fire_service_discovered, @@ -324,7 +326,16 @@ class TestComponentsDeviceTracker(unittest.TestCase): device_tracker.see(self.hass, 'mac_1', host_name='hello') self.hass.block_till_done() - self.assertEqual(1, len(test_events)) + + assert len(test_events) == 1 + + # Assert we can serialize the event + json.dumps(test_events[0].as_dict(), cls=JSONEncoder) + + assert test_events[0].data == { + 'entity_id': 'device_tracker.hello', + 'host_name': 'hello', + } # pylint: disable=invalid-name def test_not_write_duplicate_yaml_keys(self): From fc2df34206a2f6069bfcaa00653b6fa74653c05e Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Mon, 14 Nov 2016 12:09:16 -0500 Subject: [PATCH 3/5] Pin versions on linters for tests The linters really need to specify an exact version, because when either flake8 or pylint release a new version, a whole lot of new issues are caught, causing failures on the code unrelated to the patches being pushed. Pinning is a best practice for linters. This allows patches which move forward the linter version to happen with any code fixes required for it to pass. --- requirements_test.txt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 933bb8a7c7b..19a70665b56 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,5 +1,10 @@ -flake8>=3.0.4 -pylint>=1.5.6 +# linters such as flake8 and pylint should be pinned, as new releases +# make new things fail. Manually update these pins when pulling in a +# new version +flake8==3.0.4 +pylint==1.6.4 +mypy-lang==0.4.5 +pydocstyle==1.1.1 coveralls>=1.1 pytest>=2.9.2 pytest-aiohttp>=0.1.3 @@ -7,7 +12,5 @@ pytest-asyncio>=0.5.0 pytest-cov>=2.3.1 pytest-timeout>=1.0.0 pytest-catchlog>=1.2.2 -pydocstyle>=1.0.0 requests_mock>=1.0 -mypy-lang>=0.4 mock-open>=1.3.1 From 96b8d8fcfa4545b8c316a8c958ab18fd954a76dd Mon Sep 17 00:00:00 2001 From: hexa- Date: Sun, 13 Nov 2016 01:14:39 +0100 Subject: [PATCH 4/5] http: reimplement X-Forwarded-For parsing (#4355) This feature needs to be enabled through the `http.use_x_forwarded_for` option, satisfying security concerns of spoofed remote addresses in untrusted network environments. The testsuite was enhanced to explicitly test the functionality of the header. Fixes #4265. Signed-off-by: Martin Weinelt --- homeassistant/components/emulated_hue.py | 1 + homeassistant/components/http.py | 23 +++++++++++------ homeassistant/const.py | 1 + tests/components/test_http.py | 32 ++++++++++++++++++++---- tests/scripts/test_check_config.py | 3 ++- 5 files changed, 47 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/emulated_hue.py b/homeassistant/components/emulated_hue.py index 187ee0de603..0f06ed631ca 100644 --- a/homeassistant/components/emulated_hue.py +++ b/homeassistant/components/emulated_hue.py @@ -76,6 +76,7 @@ def setup(hass, yaml_config): ssl_certificate=None, ssl_key=None, cors_origins=[], + use_x_forwarded_for=False, trusted_networks=[] ) diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 5886693c64f..fabff7add53 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -28,7 +28,7 @@ from homeassistant import util from homeassistant.const import ( SERVER_PORT, HTTP_HEADER_HA_AUTH, # HTTP_HEADER_CACHE_CONTROL, CONTENT_TYPE_JSON, ALLOWED_CORS_HEADERS, EVENT_HOMEASSISTANT_STOP, - EVENT_HOMEASSISTANT_START) + EVENT_HOMEASSISTANT_START, HTTP_HEADER_X_FORWARDED_FOR) import homeassistant.helpers.config_validation as cv from homeassistant.components import persistent_notification @@ -42,6 +42,7 @@ CONF_DEVELOPMENT = 'development' CONF_SSL_CERTIFICATE = 'ssl_certificate' CONF_SSL_KEY = 'ssl_key' CONF_CORS_ORIGINS = 'cors_allowed_origins' +CONF_USE_X_FORWARDED_FOR = 'use_x_forwarded_for' CONF_TRUSTED_NETWORKS = 'trusted_networks' DATA_API_PASSWORD = 'api_password' @@ -82,6 +83,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile, vol.Optional(CONF_SSL_KEY): cv.isfile, vol.Optional(CONF_CORS_ORIGINS): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_USE_X_FORWARDED_FOR, default=False): cv.boolean, vol.Optional(CONF_TRUSTED_NETWORKS): vol.All(cv.ensure_list, [ip_network]) }), @@ -125,6 +127,7 @@ def setup(hass, config): ssl_certificate = conf.get(CONF_SSL_CERTIFICATE) ssl_key = conf.get(CONF_SSL_KEY) cors_origins = conf.get(CONF_CORS_ORIGINS, []) + use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False) trusted_networks = [ ip_network(trusted_network) for trusted_network in conf.get(CONF_TRUSTED_NETWORKS, [])] @@ -138,6 +141,7 @@ def setup(hass, config): ssl_certificate=ssl_certificate, ssl_key=ssl_key, cors_origins=cors_origins, + use_x_forwarded_for=use_x_forwarded_for, trusted_networks=trusted_networks ) @@ -248,7 +252,7 @@ class HomeAssistantWSGI(object): def __init__(self, hass, development, api_password, ssl_certificate, ssl_key, server_host, server_port, cors_origins, - trusted_networks): + use_x_forwarded_for, trusted_networks): """Initialize the WSGI Home Assistant server.""" import aiohttp_cors @@ -260,6 +264,7 @@ class HomeAssistantWSGI(object): self.ssl_key = ssl_key self.server_host = server_host self.server_port = server_port + self.use_x_forwarded_for = use_x_forwarded_for self.trusted_networks = trusted_networks self.event_forwarder = None self._handler = None @@ -366,11 +371,15 @@ class HomeAssistantWSGI(object): yield from self._handler.finish_connections(60.0) yield from self.app.cleanup() - @staticmethod - def get_real_ip(request): + def get_real_ip(self, request): """Return the clients correct ip address, even in proxied setups.""" - peername = request.transport.get_extra_info('peername') - return peername[0] if peername is not None else None + if self.use_x_forwarded_for \ + and HTTP_HEADER_X_FORWARDED_FOR in request.headers: + return request.headers.get( + HTTP_HEADER_X_FORWARDED_FOR).split(',')[0] + else: + peername = request.transport.get_extra_info('peername') + return peername[0] if peername is not None else None def is_trusted_ip(self, remote_addr): """Match an ip address against trusted CIDR networks.""" @@ -452,7 +461,7 @@ def request_handler_factory(view, handler): @asyncio.coroutine def handle(request): """Handle incoming request.""" - remote_addr = HomeAssistantWSGI.get_real_ip(request) + remote_addr = view.hass.http.get_real_ip(request) # Auth code verbose on purpose authenticated = False diff --git a/homeassistant/const.py b/homeassistant/const.py index cdc846c49d3..d0fc145edde 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -360,6 +360,7 @@ HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' HTTP_HEADER_CACHE_CONTROL = 'Cache-Control' HTTP_HEADER_EXPIRES = 'Expires' HTTP_HEADER_ORIGIN = 'Origin' +HTTP_HEADER_X_FORWARDED_FOR = 'X-Forwarded-For' HTTP_HEADER_X_REQUESTED_WITH = 'X-Requested-With' HTTP_HEADER_ACCEPT = 'Accept' HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN = 'Access-Control-Allow-Origin' diff --git a/tests/components/test_http.py b/tests/components/test_http.py index 42a0498ae60..28ded4d6b44 100644 --- a/tests/components/test_http.py +++ b/tests/components/test_http.py @@ -22,6 +22,10 @@ HA_HEADERS = { # Don't add 127.0.0.1/::1 as trusted, as it may interfere with other test cases TRUSTED_NETWORKS = ['192.0.2.0/24', '2001:DB8:ABCD::/48', '100.64.0.1', 'FD01:DB8::1'] +TRUSTED_ADDRESSES = ['100.64.0.1', '192.0.2.100', 'FD01:DB8::1', + '2001:DB8:ABCD::1'] +UNTRUSTED_ADDRESSES = ['198.51.100.1', '2001:DB8:FA1::1', '127.0.0.1', '::1'] + CORS_ORIGINS = [HTTP_BASE_URL, HTTP_BASE] @@ -85,10 +89,19 @@ class TestHttp: assert req.status_code == 401 + def test_access_denied_with_x_forwarded_for(self, caplog): + """Test access denied through the X-Forwarded-For http header.""" + hass.http.use_x_forwarded_for = True + for remote_addr in UNTRUSTED_ADDRESSES: + req = requests.get(_url(const.URL_API), headers={ + const.HTTP_HEADER_X_FORWARDED_FOR: remote_addr}) + + assert req.status_code == 401, \ + "{} shouldn't be trusted".format(remote_addr) + def test_access_denied_with_untrusted_ip(self, caplog): """Test access with an untrusted ip address.""" - for remote_addr in ['198.51.100.1', '2001:DB8:FA1::1', '127.0.0.1', - '::1']: + for remote_addr in UNTRUSTED_ADDRESSES: with patch('homeassistant.components.http.' 'HomeAssistantWSGI.get_real_ip', return_value=remote_addr): @@ -138,10 +151,19 @@ class TestHttp: # assert const.URL_API in logs assert API_PASSWORD not in logs - def test_access_with_trusted_ip(self, caplog): + def test_access_granted_with_x_forwarded_for(self, caplog): + """Test access denied through the X-Forwarded-For http header.""" + hass.http.use_x_forwarded_for = True + for remote_addr in TRUSTED_ADDRESSES: + req = requests.get(_url(const.URL_API), headers={ + const.HTTP_HEADER_X_FORWARDED_FOR: remote_addr}) + + assert req.status_code == 200, \ + "{} should be trusted".format(remote_addr) + + def test_access_granted_with_trusted_ip(self, caplog): """Test access with trusted addresses.""" - for remote_addr in ['100.64.0.1', '192.0.2.100', 'FD01:DB8::1', - '2001:DB8:ABCD::1']: + for remote_addr in TRUSTED_ADDRESSES: with patch('homeassistant.components.http.' 'HomeAssistantWSGI.get_real_ip', return_value=remote_addr): diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index f0ef9efb2d1..e709d4693c7 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -165,7 +165,8 @@ class TestCheckConfig(unittest.TestCase): self.assertDictEqual({ 'components': {'http': {'api_password': 'abc123', - 'server_port': 8123}}, + 'server_port': 8123, + 'use_x_forwarded_for': False}}, 'except': {}, 'secret_cache': {secrets_path: {'http_pw': 'abc123'}}, 'secrets': {'http_pw': 'abc123'}, From 44bc057fdba7373360ed2867d805b9f03a018ca1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Nov 2016 21:34:40 -0800 Subject: [PATCH 5/5] Version bump to 0.32.4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d0fc145edde..b9971f63750 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 32 -PATCH_VERSION = '3' +PATCH_VERSION = '4' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2)