mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 07:07:28 +00:00
commit
6745e83a6c
@ -31,7 +31,7 @@ from homeassistant.util.yaml import dump
|
|||||||
from homeassistant.helpers.event import track_utc_time_change
|
from homeassistant.helpers.event import track_utc_time_change
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE,
|
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'
|
DOMAIN = 'device_tracker'
|
||||||
DEPENDENCIES = ['zone']
|
DEPENDENCIES = ['zone']
|
||||||
@ -242,7 +242,10 @@ class DeviceTracker(object):
|
|||||||
if device.track:
|
if device.track:
|
||||||
device.update_ha_state()
|
device.update_ha_state()
|
||||||
|
|
||||||
self.hass.bus.async_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
|
# During init, we ignore the group
|
||||||
if self.group is not None:
|
if self.group is not None:
|
||||||
|
@ -76,6 +76,7 @@ def setup(hass, yaml_config):
|
|||||||
ssl_certificate=None,
|
ssl_certificate=None,
|
||||||
ssl_key=None,
|
ssl_key=None,
|
||||||
cors_origins=[],
|
cors_origins=[],
|
||||||
|
use_x_forwarded_for=False,
|
||||||
trusted_networks=[]
|
trusted_networks=[]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ from homeassistant import util
|
|||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
SERVER_PORT, HTTP_HEADER_HA_AUTH, # HTTP_HEADER_CACHE_CONTROL,
|
SERVER_PORT, HTTP_HEADER_HA_AUTH, # HTTP_HEADER_CACHE_CONTROL,
|
||||||
CONTENT_TYPE_JSON, ALLOWED_CORS_HEADERS, EVENT_HOMEASSISTANT_STOP,
|
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
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.components import persistent_notification
|
from homeassistant.components import persistent_notification
|
||||||
|
|
||||||
@ -42,6 +42,7 @@ CONF_DEVELOPMENT = 'development'
|
|||||||
CONF_SSL_CERTIFICATE = 'ssl_certificate'
|
CONF_SSL_CERTIFICATE = 'ssl_certificate'
|
||||||
CONF_SSL_KEY = 'ssl_key'
|
CONF_SSL_KEY = 'ssl_key'
|
||||||
CONF_CORS_ORIGINS = 'cors_allowed_origins'
|
CONF_CORS_ORIGINS = 'cors_allowed_origins'
|
||||||
|
CONF_USE_X_FORWARDED_FOR = 'use_x_forwarded_for'
|
||||||
CONF_TRUSTED_NETWORKS = 'trusted_networks'
|
CONF_TRUSTED_NETWORKS = 'trusted_networks'
|
||||||
|
|
||||||
DATA_API_PASSWORD = 'api_password'
|
DATA_API_PASSWORD = 'api_password'
|
||||||
@ -82,6 +83,7 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile,
|
vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile,
|
||||||
vol.Optional(CONF_SSL_KEY): cv.isfile,
|
vol.Optional(CONF_SSL_KEY): cv.isfile,
|
||||||
vol.Optional(CONF_CORS_ORIGINS): vol.All(cv.ensure_list, [cv.string]),
|
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.Optional(CONF_TRUSTED_NETWORKS):
|
||||||
vol.All(cv.ensure_list, [ip_network])
|
vol.All(cv.ensure_list, [ip_network])
|
||||||
}),
|
}),
|
||||||
@ -125,6 +127,7 @@ def setup(hass, config):
|
|||||||
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
|
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
|
||||||
ssl_key = conf.get(CONF_SSL_KEY)
|
ssl_key = conf.get(CONF_SSL_KEY)
|
||||||
cors_origins = conf.get(CONF_CORS_ORIGINS, [])
|
cors_origins = conf.get(CONF_CORS_ORIGINS, [])
|
||||||
|
use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False)
|
||||||
trusted_networks = [
|
trusted_networks = [
|
||||||
ip_network(trusted_network)
|
ip_network(trusted_network)
|
||||||
for trusted_network in conf.get(CONF_TRUSTED_NETWORKS, [])]
|
for trusted_network in conf.get(CONF_TRUSTED_NETWORKS, [])]
|
||||||
@ -138,6 +141,7 @@ def setup(hass, config):
|
|||||||
ssl_certificate=ssl_certificate,
|
ssl_certificate=ssl_certificate,
|
||||||
ssl_key=ssl_key,
|
ssl_key=ssl_key,
|
||||||
cors_origins=cors_origins,
|
cors_origins=cors_origins,
|
||||||
|
use_x_forwarded_for=use_x_forwarded_for,
|
||||||
trusted_networks=trusted_networks
|
trusted_networks=trusted_networks
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -248,7 +252,7 @@ class HomeAssistantWSGI(object):
|
|||||||
|
|
||||||
def __init__(self, hass, development, api_password, ssl_certificate,
|
def __init__(self, hass, development, api_password, ssl_certificate,
|
||||||
ssl_key, server_host, server_port, cors_origins,
|
ssl_key, server_host, server_port, cors_origins,
|
||||||
trusted_networks):
|
use_x_forwarded_for, trusted_networks):
|
||||||
"""Initialize the WSGI Home Assistant server."""
|
"""Initialize the WSGI Home Assistant server."""
|
||||||
import aiohttp_cors
|
import aiohttp_cors
|
||||||
|
|
||||||
@ -260,6 +264,7 @@ class HomeAssistantWSGI(object):
|
|||||||
self.ssl_key = ssl_key
|
self.ssl_key = ssl_key
|
||||||
self.server_host = server_host
|
self.server_host = server_host
|
||||||
self.server_port = server_port
|
self.server_port = server_port
|
||||||
|
self.use_x_forwarded_for = use_x_forwarded_for
|
||||||
self.trusted_networks = trusted_networks
|
self.trusted_networks = trusted_networks
|
||||||
self.event_forwarder = None
|
self.event_forwarder = None
|
||||||
self._handler = None
|
self._handler = None
|
||||||
@ -366,11 +371,15 @@ class HomeAssistantWSGI(object):
|
|||||||
yield from self._handler.finish_connections(60.0)
|
yield from self._handler.finish_connections(60.0)
|
||||||
yield from self.app.cleanup()
|
yield from self.app.cleanup()
|
||||||
|
|
||||||
@staticmethod
|
def get_real_ip(self, request):
|
||||||
def get_real_ip(request):
|
|
||||||
"""Return the clients correct ip address, even in proxied setups."""
|
"""Return the clients correct ip address, even in proxied setups."""
|
||||||
peername = request.transport.get_extra_info('peername')
|
if self.use_x_forwarded_for \
|
||||||
return peername[0] if peername is not None else None
|
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):
|
def is_trusted_ip(self, remote_addr):
|
||||||
"""Match an ip address against trusted CIDR networks."""
|
"""Match an ip address against trusted CIDR networks."""
|
||||||
@ -452,7 +461,7 @@ def request_handler_factory(view, handler):
|
|||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def handle(request):
|
def handle(request):
|
||||||
"""Handle incoming 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
|
# Auth code verbose on purpose
|
||||||
authenticated = False
|
authenticated = False
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"""Constants used by Home Assistant components."""
|
"""Constants used by Home Assistant components."""
|
||||||
MAJOR_VERSION = 0
|
MAJOR_VERSION = 0
|
||||||
MINOR_VERSION = 32
|
MINOR_VERSION = 32
|
||||||
PATCH_VERSION = '3'
|
PATCH_VERSION = '4'
|
||||||
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
|
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
|
||||||
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
|
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
|
||||||
REQUIRED_PYTHON_VER = (3, 4, 2)
|
REQUIRED_PYTHON_VER = (3, 4, 2)
|
||||||
@ -360,6 +360,7 @@ HTTP_HEADER_CONTENT_LENGTH = 'Content-Length'
|
|||||||
HTTP_HEADER_CACHE_CONTROL = 'Cache-Control'
|
HTTP_HEADER_CACHE_CONTROL = 'Cache-Control'
|
||||||
HTTP_HEADER_EXPIRES = 'Expires'
|
HTTP_HEADER_EXPIRES = 'Expires'
|
||||||
HTTP_HEADER_ORIGIN = 'Origin'
|
HTTP_HEADER_ORIGIN = 'Origin'
|
||||||
|
HTTP_HEADER_X_FORWARDED_FOR = 'X-Forwarded-For'
|
||||||
HTTP_HEADER_X_REQUESTED_WITH = 'X-Requested-With'
|
HTTP_HEADER_X_REQUESTED_WITH = 'X-Requested-With'
|
||||||
HTTP_HEADER_ACCEPT = 'Accept'
|
HTTP_HEADER_ACCEPT = 'Accept'
|
||||||
HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN = 'Access-Control-Allow-Origin'
|
HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN = 'Access-Control-Allow-Origin'
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
flake8>=3.0.4
|
# linters such as flake8 and pylint should be pinned, as new releases
|
||||||
pylint>=1.5.6
|
# 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
|
coveralls>=1.1
|
||||||
pytest>=2.9.2
|
pytest>=2.9.2
|
||||||
pytest-aiohttp>=0.1.3
|
pytest-aiohttp>=0.1.3
|
||||||
@ -7,7 +12,5 @@ pytest-asyncio>=0.5.0
|
|||||||
pytest-cov>=2.3.1
|
pytest-cov>=2.3.1
|
||||||
pytest-timeout>=1.0.0
|
pytest-timeout>=1.0.0
|
||||||
pytest-catchlog>=1.2.2
|
pytest-catchlog>=1.2.2
|
||||||
pydocstyle>=1.0.0
|
|
||||||
requests_mock>=1.0
|
requests_mock>=1.0
|
||||||
mypy-lang>=0.4
|
|
||||||
mock-open>=1.3.1
|
mock-open>=1.3.1
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""The tests for the device tracker component."""
|
"""The tests for the device tracker component."""
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import call, patch
|
from unittest.mock import call, patch
|
||||||
@ -15,6 +16,7 @@ from homeassistant.const import (
|
|||||||
STATE_HOME, STATE_NOT_HOME, CONF_PLATFORM)
|
STATE_HOME, STATE_NOT_HOME, CONF_PLATFORM)
|
||||||
import homeassistant.components.device_tracker as device_tracker
|
import homeassistant.components.device_tracker as device_tracker
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.remote import JSONEncoder
|
||||||
|
|
||||||
from tests.common import (
|
from tests.common import (
|
||||||
get_test_home_assistant, fire_time_changed, fire_service_discovered,
|
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')
|
device_tracker.see(self.hass, 'mac_1', host_name='hello')
|
||||||
|
|
||||||
self.hass.block_till_done()
|
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
|
# pylint: disable=invalid-name
|
||||||
def test_not_write_duplicate_yaml_keys(self):
|
def test_not_write_duplicate_yaml_keys(self):
|
||||||
|
@ -22,6 +22,10 @@ HA_HEADERS = {
|
|||||||
# Don't add 127.0.0.1/::1 as trusted, as it may interfere with other test cases
|
# 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',
|
TRUSTED_NETWORKS = ['192.0.2.0/24', '2001:DB8:ABCD::/48', '100.64.0.1',
|
||||||
'FD01:DB8::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]
|
CORS_ORIGINS = [HTTP_BASE_URL, HTTP_BASE]
|
||||||
|
|
||||||
@ -85,10 +89,19 @@ class TestHttp:
|
|||||||
|
|
||||||
assert req.status_code == 401
|
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):
|
def test_access_denied_with_untrusted_ip(self, caplog):
|
||||||
"""Test access with an untrusted ip address."""
|
"""Test access with an untrusted ip address."""
|
||||||
for remote_addr in ['198.51.100.1', '2001:DB8:FA1::1', '127.0.0.1',
|
for remote_addr in UNTRUSTED_ADDRESSES:
|
||||||
'::1']:
|
|
||||||
with patch('homeassistant.components.http.'
|
with patch('homeassistant.components.http.'
|
||||||
'HomeAssistantWSGI.get_real_ip',
|
'HomeAssistantWSGI.get_real_ip',
|
||||||
return_value=remote_addr):
|
return_value=remote_addr):
|
||||||
@ -138,10 +151,19 @@ class TestHttp:
|
|||||||
# assert const.URL_API in logs
|
# assert const.URL_API in logs
|
||||||
assert API_PASSWORD not 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."""
|
"""Test access with trusted addresses."""
|
||||||
for remote_addr in ['100.64.0.1', '192.0.2.100', 'FD01:DB8::1',
|
for remote_addr in TRUSTED_ADDRESSES:
|
||||||
'2001:DB8:ABCD::1']:
|
|
||||||
with patch('homeassistant.components.http.'
|
with patch('homeassistant.components.http.'
|
||||||
'HomeAssistantWSGI.get_real_ip',
|
'HomeAssistantWSGI.get_real_ip',
|
||||||
return_value=remote_addr):
|
return_value=remote_addr):
|
||||||
|
@ -165,7 +165,8 @@ class TestCheckConfig(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertDictEqual({
|
self.assertDictEqual({
|
||||||
'components': {'http': {'api_password': 'abc123',
|
'components': {'http': {'api_password': 'abc123',
|
||||||
'server_port': 8123}},
|
'server_port': 8123,
|
||||||
|
'use_x_forwarded_for': False}},
|
||||||
'except': {},
|
'except': {},
|
||||||
'secret_cache': {secrets_path: {'http_pw': 'abc123'}},
|
'secret_cache': {secrets_path: {'http_pw': 'abc123'}},
|
||||||
'secrets': {'http_pw': 'abc123'},
|
'secrets': {'http_pw': 'abc123'},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user