2016-11-25 13:04:06 -08:00
|
|
|
"""The tests for the Home Assistant HTTP component."""
|
|
|
|
# pylint: disable=protected-access
|
2018-02-15 13:06:14 -08:00
|
|
|
from ipaddress import ip_network
|
2018-06-30 19:31:36 -07:00
|
|
|
from unittest.mock import patch, Mock
|
2016-11-25 13:04:06 -08:00
|
|
|
|
2018-06-30 19:31:36 -07:00
|
|
|
import pytest
|
2018-02-15 13:06:14 -08:00
|
|
|
from aiohttp import BasicAuth, web
|
|
|
|
from aiohttp.web_exceptions import HTTPUnauthorized
|
2016-11-25 13:04:06 -08:00
|
|
|
|
2018-07-13 11:43:08 +02:00
|
|
|
from homeassistant.auth.models import AccessToken, RefreshToken
|
2018-02-15 13:06:14 -08:00
|
|
|
from homeassistant.components.http.auth import setup_auth
|
|
|
|
from homeassistant.components.http.const import KEY_AUTHENTICATED
|
2018-06-30 19:31:36 -07:00
|
|
|
from homeassistant.components.http.real_ip import setup_real_ip
|
|
|
|
from homeassistant.const import HTTP_HEADER_HA_AUTH
|
|
|
|
from homeassistant.setup import async_setup_component
|
2018-02-15 13:06:14 -08:00
|
|
|
from . import mock_real_ip
|
2016-11-25 13:04:06 -08:00
|
|
|
|
2018-06-30 19:31:36 -07:00
|
|
|
|
|
|
|
ACCESS_TOKEN = 'tk.1234'
|
|
|
|
|
2016-11-25 13:04:06 -08:00
|
|
|
API_PASSWORD = 'test1234'
|
2017-05-19 07:37:39 -07:00
|
|
|
|
2016-11-25 13:04:06 -08:00
|
|
|
# Don't add 127.0.0.1/::1 as trusted, as it may interfere with other test cases
|
2018-02-15 13:06:14 -08:00
|
|
|
TRUSTED_NETWORKS = [
|
|
|
|
ip_network('192.0.2.0/24'),
|
|
|
|
ip_network('2001:DB8:ABCD::/48'),
|
|
|
|
ip_network('100.64.0.1'),
|
|
|
|
ip_network('FD01:DB8::1'),
|
|
|
|
]
|
2016-11-25 13:04:06 -08:00
|
|
|
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']
|
|
|
|
|
|
|
|
|
2018-03-09 09:51:49 +08:00
|
|
|
async def mock_handler(request):
|
2018-02-15 13:06:14 -08:00
|
|
|
"""Return if request was authenticated."""
|
|
|
|
if not request[KEY_AUTHENTICATED]:
|
|
|
|
raise HTTPUnauthorized
|
|
|
|
return web.Response(status=200)
|
2016-11-25 13:04:06 -08:00
|
|
|
|
|
|
|
|
2018-06-30 19:31:36 -07:00
|
|
|
def mock_async_get_access_token(token):
|
|
|
|
"""Return if token is valid."""
|
|
|
|
if token == ACCESS_TOKEN:
|
|
|
|
return Mock(spec=AccessToken,
|
|
|
|
token=ACCESS_TOKEN,
|
|
|
|
refresh_token=Mock(spec=RefreshToken))
|
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
2018-02-15 13:06:14 -08:00
|
|
|
@pytest.fixture
|
|
|
|
def app():
|
|
|
|
"""Fixture to setup a web.Application."""
|
|
|
|
app = web.Application()
|
2018-06-30 19:31:36 -07:00
|
|
|
mock_auth = Mock(async_get_access_token=mock_async_get_access_token)
|
|
|
|
app['hass'] = Mock(auth=mock_auth)
|
2018-02-15 13:06:14 -08:00
|
|
|
app.router.add_get('/', mock_handler)
|
2018-06-28 09:16:11 -04:00
|
|
|
setup_real_ip(app, False, [])
|
2018-02-15 13:06:14 -08:00
|
|
|
return app
|
2016-11-25 13:04:06 -08:00
|
|
|
|
|
|
|
|
2018-06-30 19:31:36 -07:00
|
|
|
@pytest.fixture
|
|
|
|
def app2():
|
|
|
|
"""Fixture to setup a web.Application without real_ip middleware."""
|
|
|
|
app = web.Application()
|
|
|
|
mock_auth = Mock(async_get_access_token=mock_async_get_access_token)
|
|
|
|
app['hass'] = Mock(auth=mock_auth)
|
|
|
|
app.router.add_get('/', mock_handler)
|
|
|
|
return app
|
|
|
|
|
|
|
|
|
2018-03-09 09:51:49 +08:00
|
|
|
async def test_auth_middleware_loaded_by_default(hass):
|
2018-02-15 13:06:14 -08:00
|
|
|
"""Test accessing to server from banned IP when feature is off."""
|
|
|
|
with patch('homeassistant.components.http.setup_auth') as mock_setup:
|
2018-03-09 09:51:49 +08:00
|
|
|
await async_setup_component(hass, 'http', {
|
2018-02-15 13:06:14 -08:00
|
|
|
'http': {}
|
|
|
|
})
|
2016-11-25 13:04:06 -08:00
|
|
|
|
2018-02-15 13:06:14 -08:00
|
|
|
assert len(mock_setup.mock_calls) == 1
|
2016-11-25 13:04:06 -08:00
|
|
|
|
|
|
|
|
2018-03-15 13:49:49 -07:00
|
|
|
async def test_access_without_password(app, aiohttp_client):
|
2018-02-15 13:06:14 -08:00
|
|
|
"""Test access without password."""
|
2018-06-30 19:31:36 -07:00
|
|
|
setup_auth(app, [], False, api_password=None)
|
2018-03-15 13:49:49 -07:00
|
|
|
client = await aiohttp_client(app)
|
2016-11-25 13:04:06 -08:00
|
|
|
|
2018-03-09 09:51:49 +08:00
|
|
|
resp = await client.get('/')
|
2018-02-15 13:06:14 -08:00
|
|
|
assert resp.status == 200
|
2016-11-25 13:04:06 -08:00
|
|
|
|
|
|
|
|
2018-03-15 13:49:49 -07:00
|
|
|
async def test_access_with_password_in_header(app, aiohttp_client):
|
2018-06-30 19:31:36 -07:00
|
|
|
"""Test access with password in header."""
|
|
|
|
setup_auth(app, [], False, api_password=API_PASSWORD)
|
2018-03-15 13:49:49 -07:00
|
|
|
client = await aiohttp_client(app)
|
2016-11-25 13:04:06 -08:00
|
|
|
|
2018-03-09 09:51:49 +08:00
|
|
|
req = await client.get(
|
2018-02-15 13:06:14 -08:00
|
|
|
'/', headers={HTTP_HEADER_HA_AUTH: API_PASSWORD})
|
2017-05-19 07:37:39 -07:00
|
|
|
assert req.status == 200
|
2016-11-25 13:04:06 -08:00
|
|
|
|
2018-03-09 09:51:49 +08:00
|
|
|
req = await client.get(
|
2018-02-15 13:06:14 -08:00
|
|
|
'/', headers={HTTP_HEADER_HA_AUTH: 'wrong-pass'})
|
|
|
|
assert req.status == 401
|
2016-11-25 13:04:06 -08:00
|
|
|
|
|
|
|
|
2018-03-15 13:49:49 -07:00
|
|
|
async def test_access_with_password_in_query(app, aiohttp_client):
|
2018-06-30 19:31:36 -07:00
|
|
|
"""Test access with password in URL."""
|
|
|
|
setup_auth(app, [], False, api_password=API_PASSWORD)
|
2018-03-15 13:49:49 -07:00
|
|
|
client = await aiohttp_client(app)
|
2016-11-25 13:04:06 -08:00
|
|
|
|
2018-03-09 09:51:49 +08:00
|
|
|
resp = await client.get('/', params={
|
2018-02-15 13:06:14 -08:00
|
|
|
'api_password': API_PASSWORD
|
|
|
|
})
|
|
|
|
assert resp.status == 200
|
2016-11-25 13:04:06 -08:00
|
|
|
|
2018-03-09 09:51:49 +08:00
|
|
|
resp = await client.get('/')
|
2018-02-15 13:06:14 -08:00
|
|
|
assert resp.status == 401
|
2016-11-25 13:04:06 -08:00
|
|
|
|
2018-03-09 09:51:49 +08:00
|
|
|
resp = await client.get('/', params={
|
2018-02-15 13:06:14 -08:00
|
|
|
'api_password': 'wrong-password'
|
|
|
|
})
|
|
|
|
assert resp.status == 401
|
2017-09-28 00:49:35 -07:00
|
|
|
|
|
|
|
|
2018-03-15 13:49:49 -07:00
|
|
|
async def test_basic_auth_works(app, aiohttp_client):
|
2017-09-28 00:49:35 -07:00
|
|
|
"""Test access with basic authentication."""
|
2018-06-30 19:31:36 -07:00
|
|
|
setup_auth(app, [], False, api_password=API_PASSWORD)
|
2018-03-15 13:49:49 -07:00
|
|
|
client = await aiohttp_client(app)
|
2017-09-28 00:49:35 -07:00
|
|
|
|
2018-03-09 09:51:49 +08:00
|
|
|
req = await client.get(
|
2018-02-15 13:06:14 -08:00
|
|
|
'/',
|
|
|
|
auth=BasicAuth('homeassistant', API_PASSWORD))
|
2017-09-28 00:49:35 -07:00
|
|
|
assert req.status == 200
|
|
|
|
|
2018-03-09 09:51:49 +08:00
|
|
|
req = await client.get(
|
2018-02-15 13:06:14 -08:00
|
|
|
'/',
|
|
|
|
auth=BasicAuth('wrong_username', API_PASSWORD))
|
|
|
|
assert req.status == 401
|
2017-09-28 00:49:35 -07:00
|
|
|
|
2018-03-09 09:51:49 +08:00
|
|
|
req = await client.get(
|
2018-02-15 13:06:14 -08:00
|
|
|
'/',
|
|
|
|
auth=BasicAuth('homeassistant', 'wrong password'))
|
|
|
|
assert req.status == 401
|
2017-09-28 00:49:35 -07:00
|
|
|
|
2018-03-09 09:51:49 +08:00
|
|
|
req = await client.get(
|
2018-02-15 13:06:14 -08:00
|
|
|
'/',
|
|
|
|
headers={
|
|
|
|
'authorization': 'NotBasic abcdefg'
|
|
|
|
})
|
2017-09-28 00:49:35 -07:00
|
|
|
assert req.status == 401
|
|
|
|
|
|
|
|
|
2018-06-30 19:31:36 -07:00
|
|
|
async def test_access_with_trusted_ip(app2, aiohttp_client):
|
2018-02-15 13:06:14 -08:00
|
|
|
"""Test access with an untrusted ip address."""
|
2018-06-30 19:31:36 -07:00
|
|
|
setup_auth(app2, TRUSTED_NETWORKS, False, api_password='some-pass')
|
2017-09-28 00:49:35 -07:00
|
|
|
|
2018-06-30 19:31:36 -07:00
|
|
|
set_mock_ip = mock_real_ip(app2)
|
|
|
|
client = await aiohttp_client(app2)
|
2017-09-28 00:49:35 -07:00
|
|
|
|
2018-06-30 19:31:36 -07:00
|
|
|
for remote_addr in UNTRUSTED_ADDRESSES:
|
|
|
|
set_mock_ip(remote_addr)
|
|
|
|
resp = await client.get('/')
|
|
|
|
assert resp.status == 401, \
|
|
|
|
"{} shouldn't be trusted".format(remote_addr)
|
|
|
|
|
|
|
|
for remote_addr in TRUSTED_ADDRESSES:
|
|
|
|
set_mock_ip(remote_addr)
|
|
|
|
resp = await client.get('/')
|
|
|
|
assert resp.status == 200, \
|
|
|
|
"{} should be trusted".format(remote_addr)
|
|
|
|
|
|
|
|
|
|
|
|
async def test_auth_active_access_with_access_token_in_header(
|
|
|
|
app, aiohttp_client):
|
|
|
|
"""Test access with access token in header."""
|
|
|
|
setup_auth(app, [], True, api_password=None)
|
2018-03-15 13:49:49 -07:00
|
|
|
client = await aiohttp_client(app)
|
2017-09-28 00:49:35 -07:00
|
|
|
|
2018-06-30 19:31:36 -07:00
|
|
|
req = await client.get(
|
|
|
|
'/', headers={'Authorization': 'Bearer {}'.format(ACCESS_TOKEN)})
|
|
|
|
assert req.status == 200
|
|
|
|
|
|
|
|
req = await client.get(
|
|
|
|
'/', headers={'AUTHORIZATION': 'Bearer {}'.format(ACCESS_TOKEN)})
|
|
|
|
assert req.status == 200
|
|
|
|
|
|
|
|
req = await client.get(
|
|
|
|
'/', headers={'authorization': 'Bearer {}'.format(ACCESS_TOKEN)})
|
|
|
|
assert req.status == 200
|
|
|
|
|
|
|
|
req = await client.get(
|
|
|
|
'/', headers={'Authorization': ACCESS_TOKEN})
|
|
|
|
assert req.status == 401
|
|
|
|
|
|
|
|
req = await client.get(
|
|
|
|
'/', headers={'Authorization': 'BEARER {}'.format(ACCESS_TOKEN)})
|
|
|
|
assert req.status == 401
|
|
|
|
|
|
|
|
req = await client.get(
|
|
|
|
'/', headers={'Authorization': 'Bearer wrong-pass'})
|
|
|
|
assert req.status == 401
|
|
|
|
|
|
|
|
|
|
|
|
async def test_auth_active_access_with_trusted_ip(app2, aiohttp_client):
|
|
|
|
"""Test access with an untrusted ip address."""
|
|
|
|
setup_auth(app2, TRUSTED_NETWORKS, True, api_password=None)
|
|
|
|
|
|
|
|
set_mock_ip = mock_real_ip(app2)
|
|
|
|
client = await aiohttp_client(app2)
|
|
|
|
|
2018-02-15 13:06:14 -08:00
|
|
|
for remote_addr in UNTRUSTED_ADDRESSES:
|
|
|
|
set_mock_ip(remote_addr)
|
2018-03-09 09:51:49 +08:00
|
|
|
resp = await client.get('/')
|
2018-02-15 13:06:14 -08:00
|
|
|
assert resp.status == 401, \
|
|
|
|
"{} shouldn't be trusted".format(remote_addr)
|
2017-09-28 00:49:35 -07:00
|
|
|
|
2018-02-15 13:06:14 -08:00
|
|
|
for remote_addr in TRUSTED_ADDRESSES:
|
|
|
|
set_mock_ip(remote_addr)
|
2018-03-09 09:51:49 +08:00
|
|
|
resp = await client.get('/')
|
2018-02-15 13:06:14 -08:00
|
|
|
assert resp.status == 200, \
|
|
|
|
"{} should be trusted".format(remote_addr)
|
2018-06-30 19:31:36 -07:00
|
|
|
|
|
|
|
|
|
|
|
async def test_auth_active_blocked_api_password_access(app, aiohttp_client):
|
|
|
|
"""Test access using api_password should be blocked when auth.active."""
|
|
|
|
setup_auth(app, [], True, api_password=API_PASSWORD)
|
|
|
|
client = await aiohttp_client(app)
|
|
|
|
|
|
|
|
req = await client.get(
|
|
|
|
'/', headers={HTTP_HEADER_HA_AUTH: API_PASSWORD})
|
|
|
|
assert req.status == 401
|
|
|
|
|
|
|
|
resp = await client.get('/', params={
|
|
|
|
'api_password': API_PASSWORD
|
|
|
|
})
|
|
|
|
assert resp.status == 401
|
|
|
|
|
|
|
|
req = await client.get(
|
|
|
|
'/',
|
|
|
|
auth=BasicAuth('homeassistant', API_PASSWORD))
|
|
|
|
assert req.status == 401
|
|
|
|
|
|
|
|
|
|
|
|
async def test_auth_legacy_support_api_password_access(app, aiohttp_client):
|
|
|
|
"""Test access using api_password if auth.support_legacy."""
|
|
|
|
setup_auth(app, [], True, support_legacy=True, api_password=API_PASSWORD)
|
|
|
|
client = await aiohttp_client(app)
|
|
|
|
|
|
|
|
req = await client.get(
|
|
|
|
'/', headers={HTTP_HEADER_HA_AUTH: API_PASSWORD})
|
|
|
|
assert req.status == 200
|
|
|
|
|
|
|
|
resp = await client.get('/', params={
|
|
|
|
'api_password': API_PASSWORD
|
|
|
|
})
|
|
|
|
assert resp.status == 200
|
|
|
|
|
|
|
|
req = await client.get(
|
|
|
|
'/',
|
|
|
|
auth=BasicAuth('homeassistant', API_PASSWORD))
|
|
|
|
assert req.status == 200
|