1
mirror of https://github.com/home-assistant/core synced 2024-10-01 05:30:36 +02:00

Fix cloudhooks coming in for non existing webhooks (#36836)

* Fix cloudhooks coming in for non existing webhooks

* Fix tests"
This commit is contained in:
Paulus Schoutsen 2020-06-15 16:30:40 -07:00 committed by GitHub
parent 02f174e2e6
commit 3ee3ae7633
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 76 additions and 10 deletions

View File

@ -19,7 +19,7 @@ from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util.aiohttp import MockRequest from homeassistant.util.aiohttp import MockRequest
from . import alexa_config, google_config, utils from . import alexa_config, google_config, utils
from .const import DISPATCHER_REMOTE_UPDATE from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN
from .prefs import CloudPreferences from .prefs import CloudPreferences
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -182,6 +182,7 @@ class CloudClient(Interface):
headers=payload["headers"], headers=payload["headers"],
method=payload["method"], method=payload["method"],
query_string=payload["query"], query_string=payload["query"],
mock_source=DOMAIN,
) )
response = await self._hass.components.webhook.async_handle_webhook( response = await self._hass.components.webhook.async_handle_webhook(

View File

@ -12,6 +12,7 @@ from homeassistant.const import HTTP_OK
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.network import get_url from homeassistant.helpers.network import get_url
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.util.aiohttp import MockRequest
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -76,9 +77,15 @@ async def async_handle_webhook(hass, webhook_id, request):
# Always respond successfully to not give away if a hook exists or not. # Always respond successfully to not give away if a hook exists or not.
if webhook is None: if webhook is None:
peer_ip = request[KEY_REAL_IP] if isinstance(request, MockRequest):
received_from = request.mock_source
else:
received_from = request[KEY_REAL_IP]
_LOGGER.warning( _LOGGER.warning(
"Received message for unregistered webhook %s from %s", webhook_id, peer_ip "Received message for unregistered webhook %s from %s",
webhook_id,
received_from,
) )
# Look at content to provide some context for received webhook # Look at content to provide some context for received webhook
# Limit to 64 chars to avoid flooding the log # Limit to 64 chars to avoid flooding the log

View File

@ -1,4 +1,5 @@
"""Utilities to help with aiohttp.""" """Utilities to help with aiohttp."""
import io
import json import json
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from urllib.parse import parse_qsl from urllib.parse import parse_qsl
@ -8,12 +9,29 @@ from multidict import CIMultiDict, MultiDict
from homeassistant.const import HTTP_OK from homeassistant.const import HTTP_OK
class MockStreamReader:
"""Small mock to imitate stream reader."""
def __init__(self, content: bytes) -> None:
"""Initialize mock stream reader."""
self._content = io.BytesIO(content)
async def read(self, byte_count: int = -1) -> bytes:
"""Read bytes."""
if byte_count == -1:
return self._content.read()
return self._content.read(byte_count)
class MockRequest: class MockRequest:
"""Mock an aiohttp request.""" """Mock an aiohttp request."""
mock_source: Optional[str] = None
def __init__( def __init__(
self, self,
content: bytes, content: bytes,
mock_source: str,
method: str = "GET", method: str = "GET",
status: int = HTTP_OK, status: int = HTTP_OK,
headers: Optional[Dict[str, str]] = None, headers: Optional[Dict[str, str]] = None,
@ -27,6 +45,7 @@ class MockRequest:
self.headers: CIMultiDict[str] = CIMultiDict(headers or {}) self.headers: CIMultiDict[str] = CIMultiDict(headers or {})
self.query_string = query_string or "" self.query_string = query_string or ""
self._content = content self._content = content
self.mock_source = mock_source
@property @property
def query(self) -> "MultiDict[str]": def query(self) -> "MultiDict[str]":
@ -38,6 +57,11 @@ class MockRequest:
"""Return the body as text.""" """Return the body as text."""
return self._content.decode("utf-8") return self._content.decode("utf-8")
@property
def content(self) -> MockStreamReader:
"""Return the body as text."""
return MockStreamReader(self._content)
async def json(self) -> Any: async def json(self) -> Any:
"""Return the body as JSON.""" """Return the body as JSON."""
return json.loads(self._text) return json.loads(self._text)

View File

@ -114,12 +114,14 @@ async def test_view(hass):
"""Test view.""" """Test view."""
hass.config_entries.flow.async_init = AsyncMock() hass.config_entries.flow.async_init = AsyncMock()
request = aiohttp.MockRequest(b"", query_string="code=test_code") request = aiohttp.MockRequest(
b"", query_string="code=test_code", mock_source="test"
)
request.app = {"hass": hass} request.app = {"hass": hass}
view = config_flow.AmbiclimateAuthCallbackView() view = config_flow.AmbiclimateAuthCallbackView()
assert await view.get(request) == "OK!" assert await view.get(request) == "OK!"
request = aiohttp.MockRequest(b"", query_string="") request = aiohttp.MockRequest(b"", query_string="", mock_source="test")
request.app = {"hass": hass} request.app = {"hass": hass}
view = config_flow.AmbiclimateAuthCallbackView() view = config_flow.AmbiclimateAuthCallbackView()
assert await view.get(request) == "No code" assert await view.get(request) == "No code"

View File

@ -141,7 +141,7 @@ async def test_handler_google_actions_disabled(hass, mock_cloud_fixture):
assert resp["payload"]["errorCode"] == "deviceTurnedOff" assert resp["payload"]["errorCode"] == "deviceTurnedOff"
async def test_webhook_msg(hass): async def test_webhook_msg(hass, caplog):
"""Test webhook msg.""" """Test webhook msg."""
with patch("hass_nabucasa.Cloud.start"): with patch("hass_nabucasa.Cloud.start"):
setup = await async_setup_component(hass, "cloud", {"cloud": {}}) setup = await async_setup_component(hass, "cloud", {"cloud": {}})
@ -151,7 +151,14 @@ async def test_webhook_msg(hass):
await cloud.client.prefs.async_initialize() await cloud.client.prefs.async_initialize()
await cloud.client.prefs.async_update( await cloud.client.prefs.async_update(
cloudhooks={ cloudhooks={
"hello": {"webhook_id": "mock-webhook-id", "cloudhook_id": "mock-cloud-id"} "mock-webhook-id": {
"webhook_id": "mock-webhook-id",
"cloudhook_id": "mock-cloud-id",
},
"no-longere-existing": {
"webhook_id": "no-longere-existing",
"cloudhook_id": "mock-nonexisting-id",
},
} }
) )
@ -183,6 +190,31 @@ async def test_webhook_msg(hass):
assert len(received) == 1 assert len(received) == 1
assert await received[0].json() == {"hello": "world"} assert await received[0].json() == {"hello": "world"}
# Non existing webhook
caplog.clear()
response = await cloud.client.async_webhook_message(
{
"cloudhook_id": "mock-nonexisting-id",
"body": '{"nonexisting": "payload"}',
"headers": {"content-type": "application/json"},
"method": "POST",
"query": None,
}
)
assert response == {
"status": 200,
"body": None,
"headers": {"Content-Type": "application/octet-stream"},
}
assert (
"Received message for unregistered webhook no-longere-existing from cloud"
in caplog.text
)
assert '{"nonexisting": "payload"}' in caplog.text
async def test_google_config_expose_entity(hass, mock_cloud_setup, mock_cloud_login): async def test_google_config_expose_entity(hass, mock_cloud_setup, mock_cloud_login):
"""Test Google config exposing entity method uses latest config.""" """Test Google config exposing entity method uses latest config."""

View File

@ -5,14 +5,14 @@ from homeassistant.util import aiohttp
async def test_request_json(): async def test_request_json():
"""Test a JSON request.""" """Test a JSON request."""
request = aiohttp.MockRequest(b'{"hello": 2}') request = aiohttp.MockRequest(b'{"hello": 2}', mock_source="test")
assert request.status == 200 assert request.status == 200
assert await request.json() == {"hello": 2} assert await request.json() == {"hello": 2}
async def test_request_text(): async def test_request_text():
"""Test a JSON request.""" """Test a JSON request."""
request = aiohttp.MockRequest(b"hello", status=201) request = aiohttp.MockRequest(b"hello", status=201, mock_source="test")
assert request.status == 201 assert request.status == 201
assert await request.text() == "hello" assert await request.text() == "hello"
@ -20,7 +20,7 @@ async def test_request_text():
async def test_request_post_query(): async def test_request_post_query():
"""Test a JSON request.""" """Test a JSON request."""
request = aiohttp.MockRequest( request = aiohttp.MockRequest(
b"hello=2&post=true", query_string="get=true", method="POST" b"hello=2&post=true", query_string="get=true", method="POST", mock_source="test"
) )
assert request.method == "POST" assert request.method == "POST"
assert await request.post() == {"hello": "2", "post": "true"} assert await request.post() == {"hello": "2", "post": "true"}