Compare commits

..

2 Commits

Author SHA1 Message Date
Stefan Agner
3e307c5c8b Fix pylint 2025-10-08 14:56:56 +02:00
Stefan Agner
1cd499b4a5 Fix WebSocket transport None race condition in proxy
Add a transport validity check before WebSocket upgrade to prevent
AssertionError when clients disconnect during handshake.

The issue occurs when a client connection is lost between the API state
check and server.prepare() call, causing request.transport to become None
and triggering "assert transport is not None" in aiohttp's _pre_start().

The fix detects the closed connection early and raises HTTPBadRequest
with a clear reason instead of crashing with an AssertionError.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 14:48:08 +02:00
7 changed files with 43 additions and 22 deletions

View File

@@ -16,7 +16,6 @@ jobs:
days-before-close: 7
stale-issue-label: "stale"
exempt-issue-labels: "no-stale,Help%20wanted,help-wanted,pinned,rfc,security"
only-issue-types: "bug"
stale-issue-message: >
There hasn't been any activity on this issue recently. Due to the
high number of incoming GitHub notifications, we have to clean some

View File

@@ -8,7 +8,7 @@ brotli==1.1.0
ciso8601==2.3.3
colorlog==6.9.0
cpe==1.3.1
cryptography==46.0.3
cryptography==46.0.2
debugpy==1.8.17
deepmerge==2.0
dirhash==0.5.0
@@ -19,11 +19,11 @@ jinja2==3.1.6
log-rate-limit==1.4.2
orjson==3.11.3
pulsectl==24.12.0
pyudev==0.24.4
pyudev==0.24.3
PyYAML==6.0.3
requests==2.32.5
securetar==2025.2.1
sentry-sdk==2.41.0
sentry-sdk==2.40.0
setuptools==80.9.0
voluptuous==0.15.2
dbus-fast==2.44.5

View File

@@ -1,8 +1,8 @@
astroid==4.0.1
astroid==3.3.11
coverage==7.10.7
mypy==1.18.2
pre-commit==4.3.0
pylint==4.0.1
pylint==3.3.9
pytest-aiohttp==1.1.0
pytest-asyncio==0.25.2
pytest-cov==7.0.0
@@ -10,7 +10,7 @@ pytest-timeout==2.4.0
pytest==8.4.2
ruff==0.14.0
time-machine==2.19.0
types-docker==7.1.0.20251009
types-docker==7.1.0.20250916
types-pyyaml==6.0.12.20250915
types-requests==2.32.4.20250913
urllib3==2.5.0

View File

@@ -222,6 +222,11 @@ class APIProxy(CoreSysAttributes):
raise HTTPBadGateway()
_LOGGER.info("Home Assistant WebSocket API request initialize")
# Check if transport is still valid before WebSocket upgrade
if request.transport is None:
_LOGGER.warning("WebSocket connection lost before upgrade")
raise web.HTTPBadRequest(reason="Connection closed")
# init server
server = web.WebSocketResponse(heartbeat=30)
await server.prepare(request)

View File

@@ -8,7 +8,7 @@ from ..const import UnsupportedReason
from .base import EvaluateBase
EXPECTED_LOGGING = "journald"
EXPECTED_STORAGE = ("overlay2", "overlayfs")
EXPECTED_STORAGE = "overlay2"
_LOGGER: logging.Logger = logging.getLogger(__name__)
@@ -41,18 +41,14 @@ class EvaluateDockerConfiguration(EvaluateBase):
storage_driver = self.sys_docker.info.storage
logging_driver = self.sys_docker.info.logging
is_unsupported = False
if storage_driver not in EXPECTED_STORAGE:
is_unsupported = True
if storage_driver != EXPECTED_STORAGE:
_LOGGER.warning(
"Docker storage driver %s is not supported!", storage_driver
)
if logging_driver != EXPECTED_LOGGING:
is_unsupported = True
_LOGGER.warning(
"Docker logging driver %s is not supported!", logging_driver
)
return is_unsupported
return storage_driver != EXPECTED_STORAGE or logging_driver != EXPECTED_LOGGING

View File

@@ -9,7 +9,7 @@ import logging
from typing import Any, cast
from unittest.mock import AsyncMock, patch
from aiohttp import ClientWebSocketResponse, WSCloseCode
from aiohttp import ClientWebSocketResponse, WSCloseCode, web
from aiohttp.http_websocket import WSMessage, WSMsgType
from aiohttp.test_utils import TestClient
import pytest
@@ -223,6 +223,32 @@ async def test_proxy_auth_abort_log(
)
async def test_websocket_transport_none(
coresys,
caplog: pytest.LogCaptureFixture,
):
"""Test WebSocket connection with transport None is handled gracefully."""
# Get the API proxy instance from coresys
api_proxy = APIProxy.__new__(APIProxy)
api_proxy.coresys = coresys
# Create a mock request with transport set to None to simulate connection loss
mock_request = AsyncMock(spec=web.Request)
mock_request.transport = None
caplog.clear()
with caplog.at_level(logging.WARNING):
# This should raise HTTPBadRequest, not AssertionError
with pytest.raises(web.HTTPBadRequest) as exc_info:
await api_proxy.websocket(mock_request)
# Verify the error reason
assert exc_info.value.reason == "Connection closed"
# Verify the warning was logged
assert "WebSocket connection lost before upgrade" in caplog.text
@pytest.mark.parametrize("path", ["", "mock_path"])
async def test_api_proxy_get_request(
api_client: TestClient,

View File

@@ -25,18 +25,13 @@ async def test_evaluation(coresys: CoreSys):
assert docker_configuration.reason in coresys.resolution.unsupported
coresys.resolution.unsupported.clear()
coresys.docker.info.storage = EXPECTED_STORAGE[0]
coresys.docker.info.storage = EXPECTED_STORAGE
coresys.docker.info.logging = "unsupported"
await docker_configuration()
assert docker_configuration.reason in coresys.resolution.unsupported
coresys.resolution.unsupported.clear()
coresys.docker.info.storage = "overlay2"
coresys.docker.info.logging = EXPECTED_LOGGING
await docker_configuration()
assert docker_configuration.reason not in coresys.resolution.unsupported
coresys.docker.info.storage = "overlayfs"
coresys.docker.info.storage = EXPECTED_STORAGE
coresys.docker.info.logging = EXPECTED_LOGGING
await docker_configuration()
assert docker_configuration.reason not in coresys.resolution.unsupported