8000 Add OwnTracks over HTTP by balloob · Pull Request #9582 · home-assistant/core · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content
Dismiss alert
8000

Add OwnTracks over HTTP #9582

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 28, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions homeassistant/components/device_tracker/owntracks.py
Original file line number Diff line number Diff line change
@@ -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/
Expand Down Expand Up @@ -64,13 +64,7 @@ def decrypt(ciphertext, key):
@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):
Expand Down Expand Up @@ -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."""

Expand Down
54 changes: 54 additions & 0 deletions homeassistant/components/device_tracker/owntracks_http.py
Original file line number Diff line number Diff line change
@@ -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
26 changes: 26 additions & 0 deletions homeassistant/components/http/auth.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -41,6 +44,10 @@ def auth_middleware_handler(request):
validate_password(request, request.query[DATA_API_PASSWORD])):
authenticated = True

8000 elif (hdrs.AUTHORIZATION in request.headers and
validate_authorization_header(request)):
authenticated = True

elif is_trusted_ip(request):
authenticated = True

Expand All @@ -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)
1 change: 1 addition & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,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
Expand Down
60 changes: 60 additions & 0 deletions tests/components/device_tracker/test_owntracks_http.py
Original file line number Diff line number Diff line change
@@ -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
44 changes: 44 additions & 0 deletions tests/components/http/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from ipaddress import ip_address, ip_network
from unittest.mock import patch

import aiohttp
import pytest

from homeassistant import const
Expand Down Expand Up @@ -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,
4316 headers={
'authorization': 'NotBasic abcdefg'
})

assert req.status == 401
0