mirror of
https://github.com/home-assistant/core.git
synced 2025-04-27 02:37:50 +00:00
parent
7c8e7d6eb0
commit
6fb55b363a
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Support the OwnTracks platform.
|
Device tracker platform that adds support for OwnTracks over MQTT.
|
||||||
|
|
||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/device_tracker.owntracks/
|
https://home-assistant.io/components/device_tracker.owntracks/
|
||||||
@ -64,13 +64,7 @@ def get_cipher():
|
|||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||||
"""Set up an OwnTracks tracker."""
|
"""Set up an OwnTracks tracker."""
|
||||||
max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY)
|
context = context_from_config(async_see, config)
|
||||||
waypoint_import = config.get(CONF_WAYPOINT_IMPORT)
|
|
||||||
waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST)
|
|
||||||
secret = config.get(CONF_SECRET)
|
|
||||||
|
|
||||||
context = OwnTracksContext(async_see, secret, max_gps_accuracy,
|
|
||||||
waypoint_import, waypoint_whitelist)
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_handle_mqtt_message(topic, payload, qos):
|
def async_handle_mqtt_message(topic, payload, qos):
|
||||||
@ -179,6 +173,17 @@ def _decrypt_payload(secret, topic, ciphertext):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def context_from_config(async_see, config):
|
||||||
|
"""Create an async context from Home Assistant config."""
|
||||||
|
max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY)
|
||||||
|
waypoint_import = config.get(CONF_WAYPOINT_IMPORT)
|
||||||
|
waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST)
|
||||||
|
secret = config.get(CONF_SECRET)
|
||||||
|
|
||||||
|
return OwnTracksContext(async_see, secret, max_gps_accuracy,
|
||||||
|
waypoint_import, waypoint_whitelist)
|
||||||
|
|
||||||
|
|
||||||
class OwnTracksContext:
|
class OwnTracksContext:
|
||||||
"""Hold the current OwnTracks context."""
|
"""Hold the current OwnTracks context."""
|
||||||
|
|
||||||
|
54
homeassistant/components/device_tracker/owntracks_http.py
Normal file
54
homeassistant/components/device_tracker/owntracks_http.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
"""
|
||||||
|
Device tracker platform that adds support for OwnTracks over HTTP.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/device_tracker.owntracks_http/
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from aiohttp.web_exceptions import HTTPInternalServerError
|
||||||
|
|
||||||
|
from homeassistant.components.http import HomeAssistantView
|
||||||
|
|
||||||
|
# pylint: disable=unused-import
|
||||||
|
from .owntracks import ( # NOQA
|
||||||
|
REQUIREMENTS, PLATFORM_SCHEMA, context_from_config, async_handle_message)
|
||||||
|
|
||||||
|
|
||||||
|
DEPENDENCIES = ['http']
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||||
|
"""Set up an OwnTracks tracker."""
|
||||||
|
context = context_from_config(async_see, config)
|
||||||
|
|
||||||
|
hass.http.register_view(OwnTracksView(context))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class OwnTracksView(HomeAssistantView):
|
||||||
|
"""View to handle OwnTracks HTTP requests."""
|
||||||
|
|
||||||
|
url = '/api/owntracks/{user}/{device}'
|
||||||
|
name = 'api:owntracks'
|
||||||
|
|
||||||
|
def __init__(self, context):
|
||||||
|
"""Initialize OwnTracks URL endpoints."""
|
||||||
|
self.context = context
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def post(self, request, user, device):
|
||||||
|
"""Handle an OwnTracks message."""
|
||||||
|
hass = request.app['hass']
|
||||||
|
|
||||||
|
message = yield from request.json()
|
||||||
|
message['topic'] = 'owntracks/{}/{}'.format(user, device)
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield from async_handle_message(hass, self.context, message)
|
||||||
|
return self.json([])
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPInternalServerError
|
@ -1,8 +1,11 @@
|
|||||||
"""Authentication for HTTP component."""
|
"""Authentication for HTTP component."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import base64
|
||||||
import hmac
|
import hmac
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from aiohttp import hdrs
|
||||||
|
|
||||||
from homeassistant.const import HTTP_HEADER_HA_AUTH
|
from homeassistant.const import HTTP_HEADER_HA_AUTH
|
||||||
from .util import get_real_ip
|
from .util import get_real_ip
|
||||||
from .const import KEY_TRUSTED_NETWORKS, KEY_AUTHENTICATED
|
from .const import KEY_TRUSTED_NETWORKS, KEY_AUTHENTICATED
|
||||||
@ -41,6 +44,10 @@ def auth_middleware(app, handler):
|
|||||||
validate_password(request, request.query[DATA_API_PASSWORD])):
|
validate_password(request, request.query[DATA_API_PASSWORD])):
|
||||||
authenticated = True
|
authenticated = True
|
||||||
|
|
||||||
|
elif (hdrs.AUTHORIZATION in request.headers and
|
||||||
|
validate_authorization_header(request)):
|
||||||
|
authenticated = True
|
||||||
|
|
||||||
elif is_trusted_ip(request):
|
elif is_trusted_ip(request):
|
||||||
authenticated = True
|
authenticated = True
|
||||||
|
|
||||||
@ -64,3 +71,22 @@ def validate_password(request, api_password):
|
|||||||
"""Test if password is valid."""
|
"""Test if password is valid."""
|
||||||
return hmac.compare_digest(
|
return hmac.compare_digest(
|
||||||
api_password, request.app['hass'].http.api_password)
|
api_password, request.app['hass'].http.api_password)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_authorization_header(request):
|
||||||
|
"""Test an authorization header if valid password."""
|
||||||
|
if hdrs.AUTHORIZATION not in request.headers:
|
||||||
|
return False
|
||||||
|
|
||||||
|
auth_type, auth = request.headers.get(hdrs.AUTHORIZATION).split(' ', 1)
|
||||||
|
|
||||||
|
if auth_type != 'Basic':
|
||||||
|
return False
|
||||||
|
|
||||||
|
decoded = base64.b64decode(auth).decode('utf-8')
|
||||||
|
username, password = decoded.split(':', 1)
|
||||||
|
|
||||||
|
if username != 'homeassistant':
|
||||||
|
return False
|
||||||
|
|
||||||
|
return validate_password(request, password)
|
||||||
|
@ -373,6 +373,7 @@ jsonrpc-websocket==0.5
|
|||||||
keyring>=9.3,<10.0
|
keyring>=9.3,<10.0
|
||||||
|
|
||||||
# homeassistant.components.device_tracker.owntracks
|
# homeassistant.components.device_tracker.owntracks
|
||||||
|
# homeassistant.components.device_tracker.owntracks_http
|
||||||
libnacl==1.5.2
|
libnacl==1.5.2
|
||||||
|
|
||||||
# homeassistant.components.dyson
|
# homeassistant.components.dyson
|
||||||
|
60
tests/components/device_tracker/test_owntracks_http.py
Normal file
60
tests/components/device_tracker/test_owntracks_http.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
"""Test the owntracks_http platform."""
|
||||||
|
import asyncio
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.common import mock_coro, mock_component
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_client(hass, test_client):
|
||||||
|
"""Start the Hass HTTP component."""
|
||||||
|
mock_component(hass, 'group')
|
||||||
|
mock_component(hass, 'zone')
|
||||||
|
with patch('homeassistant.components.device_tracker.async_load_config',
|
||||||
|
return_value=mock_coro([])):
|
||||||
|
hass.loop.run_until_complete(
|
||||||
|
async_setup_component(hass, 'device_tracker', {
|
||||||
|
'device_tracker': {
|
||||||
|
'platform': 'owntracks_http'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
return hass.loop.run_until_complete(test_client(hass.http.app))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_handle_message():
|
||||||
|
"""Mock async_handle_message."""
|
||||||
|
with patch('homeassistant.components.device_tracker.'
|
||||||
|
'owntracks_http.async_handle_message') as mock:
|
||||||
|
mock.return_value = mock_coro(None)
|
||||||
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def test_forward_message_correctly(mock_client, mock_handle_message):
|
||||||
|
"""Test that we forward messages correctly to OwnTracks handle message."""
|
||||||
|
resp = yield from mock_client.post('/api/owntracks/user/device', json={
|
||||||
|
'_type': 'test'
|
||||||
|
})
|
||||||
|
assert resp.status == 200
|
||||||
|
assert len(mock_handle_message.mock_calls) == 1
|
||||||
|
|
||||||
|
data = mock_handle_message.mock_calls[0][1][2]
|
||||||
|
assert data == {
|
||||||
|
'_type': 'test',
|
||||||
|
'topic': 'owntracks/user/device'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def test_handle_value_error(mock_client, mock_handle_message):
|
||||||
|
"""Test that we handle errors from handle message correctly."""
|
||||||
|
mock_handle_message.side_effect = ValueError
|
||||||
|
resp = yield from mock_client.post('/api/owntracks/user/device', json={
|
||||||
|
'_type': 'test'
|
||||||
|
})
|
||||||
|
assert resp.status == 500
|
@ -4,6 +4,7 @@ import asyncio
|
|||||||
from ipaddress import ip_address, ip_network
|
from ipaddress import ip_address, ip_network
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant import const
|
from homeassistant import const
|
||||||
@ -149,3 +150,46 @@ def test_access_granted_with_trusted_ip(mock_api_client, caplog,
|
|||||||
|
|
||||||
assert resp.status == 200, \
|
assert resp.status == 200, \
|
||||||
'{} should be trusted'.format(remote_addr)
|
'{} should be trusted'.format(remote_addr)
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def test_basic_auth_works(mock_api_client, caplog):
|
||||||
|
"""Test access with basic authentication."""
|
||||||
|
req = yield from mock_api_client.get(
|
||||||
|
const.URL_API,
|
||||||
|
auth=aiohttp.BasicAuth('homeassistant', API_PASSWORD))
|
||||||
|
|
||||||
|
assert req.status == 200
|
||||||
|
assert const.URL_API in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def test_basic_auth_username_homeassistant(mock_api_client, caplog):
|
||||||
|
"""Test access with basic auth requires username homeassistant."""
|
||||||
|
req = yield from mock_api_client.get(
|
||||||
|
const.URL_API,
|
||||||
|
auth=aiohttp.BasicAuth('wrong_username', API_PASSWORD))
|
||||||
|
|
||||||
|
assert req.status == 401
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def test_basic_auth_wrong_password(mock_api_client, caplog):
|
||||||
|
"""Test access with basic auth not allowed with wrong password."""
|
||||||
|
req = yield from mock_api_client.get(
|
||||||
|
const.URL_API,
|
||||||
|
auth=aiohttp.BasicAuth('homeassistant', 'wrong password'))
|
||||||
|
|
||||||
|
assert req.status == 401
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def test_authorization_header_must_be_basic_type(mock_api_client, caplog):
|
||||||
|
"""Test only basic authorization is allowed for auth header."""
|
||||||
|
req = yield from mock_api_client.get(
|
||||||
|
const.URL_API,
|
||||||
|
headers={
|
||||||
|
'authorization': 'NotBasic abcdefg'
|
||||||
|
})
|
||||||
|
|
||||||
|
assert req.status == 401
|
||||||
|
Loading…
x
Reference in New Issue
Block a user