Hass.io ingress (#22505)

* Fix API stream of snapshot / Add ingress

* fix lint

* Fix stream handling

* Cleanup api handling

* fix typing

* Set proxy header

* Use header constant

* Enable the ingress setup

* fix lint

* Fix name

* Fix tests

* fix lint

* forward params

* Add tests for ingress

* Cleanup cookie handling with aiohttp 3.5

* Add more tests

* Fix tests

* Fix lint

* Fix header handling for steam

* forward header too

* fix lint

* fix flake
pull/22622/head
Pascal Vizeli 2019-04-01 14:16:16 +02:00 committed by GitHub
parent 42e3e878df
commit 6829ecad9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 458 additions and 88 deletions

View File

@ -20,6 +20,7 @@ from .auth import async_setup_auth
from .discovery import async_setup_discovery from .discovery import async_setup_discovery
from .handler import HassIO, HassioAPIError from .handler import HassIO, HassioAPIError
from .http import HassIOView from .http import HassIOView
from .ingress import async_setup_ingress
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -270,4 +271,7 @@ async def async_setup(hass, config):
# Init auth Hass.io feature # Init auth Hass.io feature
async_setup_auth(hass) async_setup_auth(hass)
# Init ingress Hass.io feature
async_setup_ingress(hass, host)
return True return True

View File

@ -9,6 +9,7 @@ ATTR_UUID = 'uuid'
ATTR_USERNAME = 'username' ATTR_USERNAME = 'username'
ATTR_PASSWORD = 'password' ATTR_PASSWORD = 'password'
X_HASSIO = 'X-HASSIO-KEY' X_HASSIO = 'X-Hassio-Key'
X_HASS_USER_ID = 'X-HASS-USER-ID' X_INGRESS_PATH = "X-Ingress-Path"
X_HASS_IS_ADMIN = 'X-HASS-IS-ADMIN' X_HASS_USER_ID = 'X-Hass-User-ID'
X_HASS_IS_ADMIN = 'X-Hass-Is-Admin'

View File

@ -3,10 +3,11 @@ import asyncio
import logging import logging
import os import os
import re import re
from typing import Dict, Union
import aiohttp import aiohttp
from aiohttp import web from aiohttp import web
from aiohttp.hdrs import CONTENT_TYPE from aiohttp.hdrs import CONTENT_TYPE, CONTENT_LENGTH
from aiohttp.web_exceptions import HTTPBadGateway from aiohttp.web_exceptions import HTTPBadGateway
import async_timeout import async_timeout
@ -20,7 +21,8 @@ _LOGGER = logging.getLogger(__name__)
NO_TIMEOUT = re.compile( NO_TIMEOUT = re.compile(
r'^(?:' r'^(?:'
r'|homeassistant/update' r'|homeassistant/update'
r'|host/update' r'|hassos/update'
r'|hassos/update/cli'
r'|supervisor/update' r'|supervisor/update'
r'|addons/[^/]+/(?:update|install|rebuild)' r'|addons/[^/]+/(?:update|install|rebuild)'
r'|snapshots/.+/full' r'|snapshots/.+/full'
@ -44,25 +46,26 @@ class HassIOView(HomeAssistantView):
url = "/api/hassio/{path:.+}" url = "/api/hassio/{path:.+}"
requires_auth = False requires_auth = False
def __init__(self, host, websession): def __init__(self, host: str, websession: aiohttp.ClientSession):
"""Initialize a Hass.io base view.""" """Initialize a Hass.io base view."""
self._host = host self._host = host
self._websession = websession self._websession = websession
async def _handle(self, request, path): async def _handle(
self, request: web.Request, path: str
) -> Union[web.Response, web.StreamResponse]:
"""Route data to Hass.io.""" """Route data to Hass.io."""
if _need_auth(path) and not request[KEY_AUTHENTICATED]: if _need_auth(path) and not request[KEY_AUTHENTICATED]:
return web.Response(status=401) return web.Response(status=401)
client = await self._command_proxy(path, request) return await self._command_proxy(path, request)
data = await client.read()
return _create_response(client, data)
get = _handle get = _handle
post = _handle post = _handle
async def _command_proxy(self, path, request): async def _command_proxy(
self, path: str, request: web.Request
) -> Union[web.Response, web.StreamResponse]:
"""Return a client request with proxy origin for Hass.io supervisor. """Return a client request with proxy origin for Hass.io supervisor.
This method is a coroutine. This method is a coroutine.
@ -71,29 +74,38 @@ class HassIOView(HomeAssistantView):
hass = request.app['hass'] hass = request.app['hass']
data = None data = None
headers = { headers = _init_header(request)
X_HASSIO: os.environ.get('HASSIO_TOKEN', ""),
}
user = request.get('hass_user')
if user is not None:
headers[X_HASS_USER_ID] = request['hass_user'].id
headers[X_HASS_IS_ADMIN] = str(int(request['hass_user'].is_admin))
try: try:
with async_timeout.timeout(10, loop=hass.loop): with async_timeout.timeout(10, loop=hass.loop):
data = await request.read() data = await request.read()
if data:
headers[CONTENT_TYPE] = request.content_type
else:
data = None
method = getattr(self._websession, request.method.lower()) method = getattr(self._websession, request.method.lower())
client = await method( client = await method(
"http://{}/{}".format(self._host, path), data=data, "http://{}/{}".format(self._host, path), data=data,
headers=headers, timeout=read_timeout headers=headers, timeout=read_timeout
) )
print(client.headers)
return client # Simple request
if int(client.headers.get(CONTENT_LENGTH, 0)) < 4194000:
# Return Response
body = await client.read()
return web.Response(
content_type=client.content_type,
status=client.status,
body=body,
)
# Stream response
response = web.StreamResponse(status=client.status)
response.content_type = client.content_type
await response.prepare(request)
async for data in client.content.iter_chunked(4096):
await response.write(data)
return response
except aiohttp.ClientError as err: except aiohttp.ClientError as err:
_LOGGER.error("Client error on api %s request %s", path, err) _LOGGER.error("Client error on api %s request %s", path, err)
@ -104,23 +116,30 @@ class HassIOView(HomeAssistantView):
raise HTTPBadGateway() raise HTTPBadGateway()
def _create_response(client, data): def _init_header(request: web.Request) -> Dict[str, str]:
"""Convert a response from client request.""" """Create initial header."""
return web.Response( headers = {
body=data, X_HASSIO: os.environ.get('HASSIO_TOKEN', ""),
status=client.status, CONTENT_TYPE: request.content_type,
content_type=client.content_type, }
)
# Add user data
user = request.get('hass_user')
if user is not None:
headers[X_HASS_USER_ID] = request['hass_user'].id
headers[X_HASS_IS_ADMIN] = str(int(request['hass_user'].is_admin))
return headers
def _get_timeout(path): def _get_timeout(path: str) -> int:
"""Return timeout for a URL path.""" """Return timeout for a URL path."""
if NO_TIMEOUT.match(path): if NO_TIMEOUT.match(path):
return 0 return 0
return 300 return 300
def _need_auth(path): def _need_auth(path: str) -> bool:
"""Return if a path need authentication.""" """Return if a path need authentication."""
if NO_AUTH.match(path): if NO_AUTH.match(path):
return False return False

View File

@ -0,0 +1,210 @@
"""Hass.io Add-on ingress service."""
import asyncio
from ipaddress import ip_address
import os
from typing import Dict, Union
import aiohttp
from aiohttp import web
from aiohttp import hdrs
from aiohttp.web_exceptions import HTTPBadGateway
from multidict import CIMultiDict
from homeassistant.core import callback
from homeassistant.components.http import HomeAssistantView
from homeassistant.helpers.typing import HomeAssistantType
from .const import X_HASSIO, X_INGRESS_PATH
@callback
def async_setup_ingress(hass: HomeAssistantType, host: str):
"""Auth setup."""
websession = hass.helpers.aiohttp_client.async_get_clientsession()
hassio_ingress = HassIOIngress(host, websession)
hass.http.register_view(hassio_ingress)
class HassIOIngress(HomeAssistantView):
"""Hass.io view to handle base part."""
name = "api:hassio:ingress"
url = "/api/hassio_ingress/{addon}/{path:.+}"
requires_auth = False
def __init__(self, host: str, websession: aiohttp.ClientSession):
"""Initialize a Hass.io ingress view."""
self._host = host
self._websession = websession
def _create_url(self, addon: str, path: str) -> str:
"""Create URL to service."""
return "http://{}/addons/{}/web/{}".format(self._host, addon, path)
async def _handle(
self, request: web.Request, addon: str, path: str
) -> Union[web.Response, web.StreamResponse, web.WebSocketResponse]:
"""Route data to Hass.io ingress service."""
try:
# Websocket
if _is_websocket(request):
return await self._handle_websocket(request, addon, path)
# Request
return await self._handle_request(request, addon, path)
except aiohttp.ClientError:
pass
raise HTTPBadGateway() from None
get = _handle
post = _handle
put = _handle
delete = _handle
async def _handle_websocket(
self, request: web.Request, addon: str, path: str
) -> web.WebSocketResponse:
"""Ingress route for websocket."""
ws_server = web.WebSocketResponse()
await ws_server.prepare(request)
url = self._create_url(addon, path)
source_header = _init_header(request, addon)
# Start proxy
async with self._websession.ws_connect(
url, headers=source_header
) as ws_client:
# Proxy requests
await asyncio.wait(
[
_websocket_forward(ws_server, ws_client),
_websocket_forward(ws_client, ws_server),
],
return_when=asyncio.FIRST_COMPLETED
)
return ws_server
async def _handle_request(
self, request: web.Request, addon: str, path: str
) -> Union[web.Response, web.StreamResponse]:
"""Ingress route for request."""
url = self._create_url(addon, path)
data = await request.read()
source_header = _init_header(request, addon)
async with self._websession.request(
request.method, url, headers=source_header,
params=request.query, data=data, cookies=request.cookies
) as result:
headers = _response_header(result)
# Simple request
if hdrs.CONTENT_LENGTH in result.headers and \
int(result.headers.get(hdrs.CONTENT_LENGTH, 0)) < 4194000:
# Return Response
body = await result.read()
return web.Response(
headers=headers,
status=result.status,
body=body
)
# Stream response
response = web.StreamResponse(
status=result.status, headers=headers)
response.content_type = result.content_type
try:
await response.prepare(request)
async for data in result.content:
await response.write(data)
except (aiohttp.ClientError, aiohttp.ClientPayloadError):
pass
return response
def _init_header(
request: web.Request, addon: str
) -> Union[CIMultiDict, Dict[str, str]]:
"""Create initial header."""
headers = {}
# filter flags
for name, value in request.headers.items():
if name in (hdrs.CONTENT_LENGTH, hdrs.CONTENT_TYPE):
continue
headers[name] = value
# Inject token / cleanup later on Supervisor
headers[X_HASSIO] = os.environ.get('HASSIO_TOKEN', "")
# Ingress information
headers[X_INGRESS_PATH] = "/api/hassio_ingress/{}".format(addon)
# Set X-Forwarded-For
forward_for = request.headers.get(hdrs.X_FORWARDED_FOR)
connected_ip = ip_address(request.transport.get_extra_info('peername')[0])
if forward_for:
forward_for = "{}, {!s}".format(forward_for, connected_ip)
else:
forward_for = "{!s}".format(connected_ip)
headers[hdrs.X_FORWARDED_FOR] = forward_for
# Set X-Forwarded-Host
forward_host = request.headers.get(hdrs.X_FORWARDED_HOST)
if not forward_host:
forward_host = request.host
headers[hdrs.X_FORWARDED_HOST] = forward_host
# Set X-Forwarded-Proto
forward_proto = request.headers.get(hdrs.X_FORWARDED_PROTO)
if not forward_proto:
forward_proto = request.url.scheme
headers[hdrs.X_FORWARDED_PROTO] = forward_proto
return headers
def _response_header(response: aiohttp.ClientResponse) -> Dict[str, str]:
"""Create response header."""
headers = {}
for name, value in response.headers.items():
if name in (hdrs.TRANSFER_ENCODING, hdrs.CONTENT_LENGTH,
hdrs.CONTENT_TYPE):
continue
headers[name] = value
return headers
def _is_websocket(request: web.Request) -> bool:
"""Return True if request is a websocket."""
headers = request.headers
if headers.get(hdrs.CONNECTION) == "Upgrade" and \
headers.get(hdrs.UPGRADE) == "websocket":
return True
return False
async def _websocket_forward(ws_from, ws_to):
"""Handle websocket message directly."""
async for msg in ws_from:
if msg.type == aiohttp.WSMsgType.TEXT:
await ws_to.send_str(msg.data)
elif msg.type == aiohttp.WSMsgType.BINARY:
await ws_to.send_bytes(msg.data)
elif msg.type == aiohttp.WSMsgType.PING:
await ws_to.ping()
elif msg.type == aiohttp.WSMsgType.PONG:
await ws_to.pong()
elif ws_to.closed:
await ws_to.close(code=ws_to.close_code, message=msg.extra)

View File

@ -1,29 +1,22 @@
"""The tests for the hassio component.""" """The tests for the hassio component."""
import asyncio import asyncio
from unittest.mock import patch, Mock, MagicMock from unittest.mock import patch
import pytest import pytest
from homeassistant.const import HTTP_HEADER_HA_AUTH from homeassistant.const import HTTP_HEADER_HA_AUTH
from tests.common import mock_coro
from . import API_PASSWORD from . import API_PASSWORD
@asyncio.coroutine @asyncio.coroutine
def test_forward_request(hassio_client): def test_forward_request(hassio_client, aioclient_mock):
"""Test fetching normal path.""" """Test fetching normal path."""
response = MagicMock() aioclient_mock.post("http://127.0.0.1/beer", text="response")
response.read.return_value = mock_coro('data')
with patch('homeassistant.components.hassio.HassIOView._command_proxy', resp = yield from hassio_client.post('/api/hassio/beer', headers={
Mock(return_value=mock_coro(response))), \ HTTP_HEADER_HA_AUTH: API_PASSWORD
patch('homeassistant.components.hassio.http' })
'._create_response') as mresp:
mresp.return_value = 'response'
resp = yield from hassio_client.post('/api/hassio/beer', headers={
HTTP_HEADER_HA_AUTH: API_PASSWORD
})
# Check we got right response # Check we got right response
assert resp.status == 200 assert resp.status == 200
@ -31,8 +24,7 @@ def test_forward_request(hassio_client):
assert body == 'response' assert body == 'response'
# Check we forwarded command # Check we forwarded command
assert len(mresp.mock_calls) == 1 assert len(aioclient_mock.mock_calls) == 1
assert mresp.mock_calls[0][1] == (response, 'data')
@asyncio.coroutine @asyncio.coroutine
@ -55,18 +47,13 @@ def test_auth_required_forward_request(hassio_noauth_client, build_type):
'app/index.html', 'app/hassio-app.html', 'app/index.html', 'app/index.html', 'app/hassio-app.html', 'app/index.html',
'app/hassio-app.html', 'app/some-chunk.js', 'app/app.js', 'app/hassio-app.html', 'app/some-chunk.js', 'app/app.js',
]) ])
def test_forward_request_no_auth_for_panel(hassio_client, build_type): def test_forward_request_no_auth_for_panel(
hassio_client, build_type, aioclient_mock):
"""Test no auth needed for .""" """Test no auth needed for ."""
response = MagicMock() aioclient_mock.get(
response.read.return_value = mock_coro('data') "http://127.0.0.1/{}".format(build_type), text="response")
with patch('homeassistant.components.hassio.HassIOView._command_proxy', resp = yield from hassio_client.get('/api/hassio/{}'.format(build_type))
Mock(return_value=mock_coro(response))), \
patch('homeassistant.components.hassio.http.'
'_create_response') as mresp:
mresp.return_value = 'response'
resp = yield from hassio_client.get(
'/api/hassio/{}'.format(build_type))
# Check we got right response # Check we got right response
assert resp.status == 200 assert resp.status == 200
@ -74,22 +61,16 @@ def test_forward_request_no_auth_for_panel(hassio_client, build_type):
assert body == 'response' assert body == 'response'
# Check we forwarded command # Check we forwarded command
assert len(mresp.mock_calls) == 1 assert len(aioclient_mock.mock_calls) == 1
assert mresp.mock_calls[0][1] == (response, 'data')
@asyncio.coroutine @asyncio.coroutine
def test_forward_request_no_auth_for_logo(hassio_client): def test_forward_request_no_auth_for_logo(hassio_client, aioclient_mock):
"""Test no auth needed for .""" """Test no auth needed for ."""
response = MagicMock() aioclient_mock.get(
response.read.return_value = mock_coro('data') "http://127.0.0.1/addons/bl_b392/logo", text="response")
with patch('homeassistant.components.hassio.HassIOView._command_proxy', resp = yield from hassio_client.get('/api/hassio/addons/bl_b392/logo')
Mock(return_value=mock_coro(response))), \
patch('homeassistant.components.hassio.http.'
'_create_response') as mresp:
mresp.return_value = 'response'
resp = yield from hassio_client.get('/api/hassio/addons/bl_b392/logo')
# Check we got right response # Check we got right response
assert resp.status == 200 assert resp.status == 200
@ -97,24 +78,18 @@ def test_forward_request_no_auth_for_logo(hassio_client):
assert body == 'response' assert body == 'response'
# Check we forwarded command # Check we forwarded command
assert len(mresp.mock_calls) == 1 assert len(aioclient_mock.mock_calls) == 1
assert mresp.mock_calls[0][1] == (response, 'data')
@asyncio.coroutine @asyncio.coroutine
def test_forward_log_request(hassio_client): def test_forward_log_request(hassio_client, aioclient_mock):
"""Test fetching normal log path doesn't remove ANSI color escape codes.""" """Test fetching normal log path doesn't remove ANSI color escape codes."""
response = MagicMock() aioclient_mock.get(
response.read.return_value = mock_coro('data') "http://127.0.0.1/beer/logs", text="\033[32mresponse\033[0m")
with patch('homeassistant.components.hassio.HassIOView._command_proxy', resp = yield from hassio_client.get('/api/hassio/beer/logs', headers={
Mock(return_value=mock_coro(response))), \ HTTP_HEADER_HA_AUTH: API_PASSWORD
patch('homeassistant.components.hassio.http.' })
'_create_response') as mresp:
mresp.return_value = '\033[32mresponse\033[0m'
resp = yield from hassio_client.get('/api/hassio/beer/logs', headers={
HTTP_HEADER_HA_AUTH: API_PASSWORD
})
# Check we got right response # Check we got right response
assert resp.status == 200 assert resp.status == 200
@ -122,8 +97,7 @@ def test_forward_log_request(hassio_client):
assert body == '\033[32mresponse\033[0m' assert body == '\033[32mresponse\033[0m'
# Check we forwarded command # Check we forwarded command
assert len(mresp.mock_calls) == 1 assert len(aioclient_mock.mock_calls) == 1
assert mresp.mock_calls[0][1] == (response, 'data')
@asyncio.coroutine @asyncio.coroutine
@ -151,5 +125,5 @@ async def test_forwarding_user_info(hassio_client, hass_admin_user,
assert len(aioclient_mock.mock_calls) == 1 assert len(aioclient_mock.mock_calls) == 1
req_headers = aioclient_mock.mock_calls[0][-1] req_headers = aioclient_mock.mock_calls[0][-1]
req_headers['X-HASS-USER-ID'] == hass_admin_user.id req_headers['X-Hass-User-ID'] == hass_admin_user.id
req_headers['X-HASS-IS-ADMIN'] == '1' req_headers['X-Hass-Is-Admin'] == '1'

View File

@ -0,0 +1,162 @@
"""The tests for the hassio component."""
from aiohttp.hdrs import X_FORWARDED_FOR, X_FORWARDED_HOST, X_FORWARDED_PROTO
from aiohttp.client_exceptions import WSServerHandshakeError
import pytest
@pytest.mark.parametrize(
'build_type', [
("a3_vl", "test/beer/ping?index=1"), ("core", "index.html"),
("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5")
])
async def test_ingress_request_get(
hassio_client, build_type, aioclient_mock):
"""Test no auth needed for ."""
aioclient_mock.get("http://127.0.0.1/addons/{}/web/{}".format(
build_type[0], build_type[1]), text="test")
resp = await hassio_client.get(
'/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]),
headers={"X-Test-Header": "beer"}
)
# Check we got right response
assert resp.status == 200
body = await resp.text()
assert body == "test"
# Check we forwarded command
assert len(aioclient_mock.mock_calls) == 1
assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456"
assert aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == \
"/api/hassio_ingress/{}".format(build_type[0])
assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer"
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR]
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST]
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO]
@pytest.mark.parametrize(
'build_type', [
("a3_vl", "test/beer/ping?index=1"), ("core", "index.html"),
("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5")
])
async def test_ingress_request_post(
hassio_client, build_type, aioclient_mock):
"""Test no auth needed for ."""
aioclient_mock.post("http://127.0.0.1/addons/{}/web/{}".format(
build_type[0], build_type[1]), text="test")
resp = await hassio_client.post(
'/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]),
headers={"X-Test-Header": "beer"}
)
# Check we got right response
assert resp.status == 200
body = await resp.text()
assert body == "test"
# Check we forwarded command
assert len(aioclient_mock.mock_calls) == 1
assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456"
assert aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == \
"/api/hassio_ingress/{}".format(build_type[0])
assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer"
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR]
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST]
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO]
@pytest.mark.parametrize(
'build_type', [
("a3_vl", "test/beer/ping?index=1"), ("core", "index.html"),
("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5")
])
async def test_ingress_request_put(
hassio_client, build_type, aioclient_mock):
"""Test no auth needed for ."""
aioclient_mock.put("http://127.0.0.1/addons/{}/web/{}".format(
build_type[0], build_type[1]), text="test")
resp = await hassio_client.put(
'/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]),
headers={"X-Test-Header": "beer"}
)
# Check we got right response
assert resp.status == 200
body = await resp.text()
assert body == "test"
# Check we forwarded command
assert len(aioclient_mock.mock_calls) == 1
assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456"
assert aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == \
"/api/hassio_ingress/{}".format(build_type[0])
assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer"
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR]
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST]
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO]
@pytest.mark.parametrize(
'build_type', [
("a3_vl", "test/beer/ping?index=1"), ("core", "index.html"),
("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5")
])
async def test_ingress_request_delete(
hassio_client, build_type, aioclient_mock):
"""Test no auth needed for ."""
aioclient_mock.delete("http://127.0.0.1/addons/{}/web/{}".format(
build_type[0], build_type[1]), text="test")
resp = await hassio_client.delete(
'/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]),
headers={"X-Test-Header": "beer"}
)
# Check we got right response
assert resp.status == 200
body = await resp.text()
assert body == "test"
# Check we forwarded command
assert len(aioclient_mock.mock_calls) == 1
assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456"
assert aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == \
"/api/hassio_ingress/{}".format(build_type[0])
assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer"
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR]
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST]
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO]
@pytest.mark.parametrize(
'build_type', [
("a3_vl", "test/beer/ws"), ("core", "ws.php"),
("local", "panel/config/stream"), ("jk_921", "hulk")
])
async def test_ingress_websocket(
hassio_client, build_type, aioclient_mock):
"""Test no auth needed for ."""
aioclient_mock.get("http://127.0.0.1/addons/{}/web/{}".format(
build_type[0], build_type[1]))
# Ignore error because we can setup a full IO infrastructure
with pytest.raises(WSServerHandshakeError):
await hassio_client.ws_connect(
'/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]),
headers={"X-Test-Header": "beer"}
)
# Check we forwarded command
assert len(aioclient_mock.mock_calls) == 1
assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456"
assert aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == \
"/api/hassio_ingress/{}".format(build_type[0])
assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer"
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR]
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST]
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO]

View File

@ -207,7 +207,7 @@ def test_setup_hassio_no_additional_data(hass, aioclient_mock):
assert result assert result
assert aioclient_mock.call_count == 3 assert aioclient_mock.call_count == 3
assert aioclient_mock.mock_calls[-1][3]['X-HASSIO-KEY'] == "123456" assert aioclient_mock.mock_calls[-1][3]['X-Hassio-Key'] == "123456"
@asyncio.coroutine @asyncio.coroutine

View File

@ -102,7 +102,7 @@ class AiohttpClientMocker:
async def match_request(self, method, url, *, data=None, auth=None, async def match_request(self, method, url, *, data=None, auth=None,
params=None, headers=None, allow_redirects=None, params=None, headers=None, allow_redirects=None,
timeout=None, json=None): timeout=None, json=None, cookies=None):
"""Match a request against pre-registered requests.""" """Match a request against pre-registered requests."""
data = data or json data = data or json
url = URL(url) url = URL(url)