Reduce log spam from unauthenticated websocket connections

This commit is contained in:
J. Nick Koston
2025-08-29 10:21:31 -05:00
parent 5e003627b2
commit 3fc68b00a9
2 changed files with 61 additions and 3 deletions

View File

@@ -37,6 +37,7 @@ from .messages import message_to_json_bytes
from .util import describe_request from .util import describe_request
CLOSE_MSG_TYPES = {WSMsgType.CLOSE, WSMsgType.CLOSED, WSMsgType.CLOSING} CLOSE_MSG_TYPES = {WSMsgType.CLOSE, WSMsgType.CLOSED, WSMsgType.CLOSING}
AUTH_MESSAGE_TIMEOUT = 10 # seconds
if TYPE_CHECKING: if TYPE_CHECKING:
from .connection import ActiveConnection from .connection import ActiveConnection
@@ -389,9 +390,11 @@ class WebSocketHandler:
# Auth Phase # Auth Phase
try: try:
msg = await self._wsock.receive(10) msg = await self._wsock.receive(AUTH_MESSAGE_TIMEOUT)
except TimeoutError as err: except TimeoutError as err:
raise Disconnect("Did not receive auth message within 10 seconds") from err raise Disconnect(
f"Did not receive auth message within {AUTH_MESSAGE_TIMEOUT} seconds"
) from err
if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSED, WSMsgType.CLOSING): if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSED, WSMsgType.CLOSING):
raise Disconnect("Received close message during auth phase") raise Disconnect("Received close message during auth phase")
@@ -538,6 +541,14 @@ class WebSocketHandler:
finally: finally:
if disconnect_warn is None: if disconnect_warn is None:
logger.debug("%s: Disconnected", self.description) logger.debug("%s: Disconnected", self.description)
elif connection is None:
# Auth phase disconnects (connection is None) should be logged at debug level
# as they can be from random port scanners or non-legitimate connections
logger.debug(
"%s: Disconnected during auth phase: %s",
self.description,
disconnect_warn,
)
else: else:
logger.warning( logger.warning(
"%s: Disconnected: %s", self.description, disconnect_warn "%s: Disconnected: %s", self.description, disconnect_warn

View File

@@ -2,6 +2,7 @@
import asyncio import asyncio
from datetime import timedelta from datetime import timedelta
import logging
from typing import Any, cast from typing import Any, cast
from unittest.mock import patch from unittest.mock import patch
@@ -20,7 +21,11 @@ from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from tests.common import async_call_logger_set_level, async_fire_time_changed from tests.common import async_call_logger_set_level, async_fire_time_changed
from tests.typing import MockHAClientWebSocket, WebSocketGenerator from tests.typing import (
ClientSessionGenerator,
MockHAClientWebSocket,
WebSocketGenerator,
)
@pytest.fixture @pytest.fixture
@@ -400,6 +405,48 @@ async def test_prepare_fail_connection_reset(
assert "Connection reset by peer while preparing WebSocket" in caplog.text assert "Connection reset by peer while preparing WebSocket" in caplog.text
async def test_auth_timeout_logs_at_debug(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test auth timeout is logged at debug level not warning."""
# Setup websocket API
assert await async_setup_component(hass, "websocket_api", {})
client = await hass_client()
# Patch the auth timeout to be very short (0.001 seconds)
with (
caplog.at_level(logging.DEBUG, "homeassistant.components.websocket_api"),
patch(
"homeassistant.components.websocket_api.http.AUTH_MESSAGE_TIMEOUT", 0.001
),
):
# Try to connect - will timeout quickly since we don't send auth
ws = await client.ws_connect("/api/websocket")
# Wait a bit for the timeout to trigger and cleanup to complete
await asyncio.sleep(0.1)
await ws.close()
await asyncio.sleep(0.1)
# Check that "Did not receive auth message" is logged at debug, not warning
debug_messages = [
r.message for r in caplog.records if r.levelno == logging.DEBUG
]
assert any(
"Disconnected during auth phase: Did not receive auth message" in msg
for msg in debug_messages
)
# Check it's NOT logged at warning level
warning_messages = [
r.message for r in caplog.records if r.levelno >= logging.WARNING
]
for msg in warning_messages:
assert "Did not receive auth message" not in msg
async def test_enable_coalesce( async def test_enable_coalesce(
hass: HomeAssistant, hass: HomeAssistant,
hass_ws_client: WebSocketGenerator, hass_ws_client: WebSocketGenerator,