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
|
||||
https://home-assistant.io/components/device_tracker.owntracks/
|
||||
|
@ -64,13 +64,7 @@ def get_cipher():
|
|||
@asyncio.coroutine
|
||||
def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
"""Set up an OwnTracks tracker."""
|
||||
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)
|
||||
|
||||
context = OwnTracksContext(async_see, secret, max_gps_accuracy,
|
||||
waypoint_import, waypoint_whitelist)
|
||||
context = context_from_config(async_see, config)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_mqtt_message(topic, payload, qos):
|
||||
|
@ -179,6 +173,17 @@ def _decrypt_payload(secret, topic, ciphertext):
|
|||
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:
|
||||
"""Hold the current OwnTracks context."""
|
||||
|
||||
|
|
|
@ -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."""
|
||||
import asyncio
|
||||
import base64
|
||||
import hmac
|
||||
import logging
|
||||
|
||||
from aiohttp import hdrs
|
||||
|
||||
from homeassistant.const import HTTP_HEADER_HA_AUTH
|
||||
from .util import get_real_ip
|
||||
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])):
|
||||
authenticated = True
|
||||
|
||||
elif (hdrs.AUTHORIZATION in request.headers and
|
||||
validate_authorization_header(request)):
|
||||
authenticated = True
|
||||
|
||||
elif is_trusted_ip(request):
|
||||
authenticated = True
|
||||
|
||||
|
@ -64,3 +71,22 @@ def validate_password(request, api_password):
|
|||
"""Test if password is valid."""
|
||||
return hmac.compare_digest(
|
||||
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
|
||||
|
||||
# homeassistant.components.device_tracker.owntracks
|
||||
# homeassistant.components.device_tracker.owntracks_http
|
||||
libnacl==1.5.2
|
||||
|
||||
# homeassistant.components.dyson
|
||||
|
|
|
@ -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 unittest.mock import patch
|
||||
|
||||
import aiohttp
|
||||
import pytest
|
||||
|
||||
from homeassistant import const
|
||||
|
@ -149,3 +150,46 @@ def test_access_granted_with_trusted_ip(mock_api_client, caplog,
|
|||
|
||||
assert resp.status == 200, \
|
||||
'{} 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…
Reference in New Issue