Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
SslAesTransport: handshake again when a camera answers 401 to passthr…
…ough
  • Loading branch information
freeKC committed Sep 27, 2026
commit 1524ca1f497ff04175de32d270ad255dc6d62592
14 changes: 14 additions & 0 deletions kasa/transports/sslaestransport.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,20 @@ async def send_secure_passthrough(self, request: str) -> dict[str, Any]:
_LOGGER.debug(msg)
raise _RetryableError(msg)

if status_code == 401:
# The camera dropped the session (a C220 on 1.4.4 does this about
# every ten minutes): handshake again and retry the request.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this comment above the if condition, and remove the exact model/version numbers, and just state that some devices return this when requiring a reauth.

_LOGGER.debug(
"Device %s replied with status 401 to passthrough, "
"session expired, handshake required",
self._host,
)
self._state = TransportState.HANDSHAKE_REQUIRED
raise _RetryableError(
f"{self._host} responded with status 401 to passthrough, "
"session expired"
)

if status_code != 200:
raise KasaException(
f"{self._host} responded with an unexpected "
Expand Down
21 changes: 21 additions & 0 deletions tests/transports/test_sslaestransport.py
Original file line number Diff line number Diff line change
Expand Up @@ -797,3 +797,24 @@ async def _return_send_response(self, url: URL, json: dict[str, Any]):

def put_next_response(self, request: dict | bytes) -> None:
self._next_responses.append(request)


async def test_passthrough_401_requires_new_handshake(mocker):
"""A 401 on passthrough means the session expired: retryable, new handshake."""
host = "127.0.0.1"
mock_ssl_aes_device = MockSslAesDevice(host)
mocker.patch.object(
aiohttp.ClientSession, "post", side_effect=mock_ssl_aes_device.post
)
transport = SslAesTransport(
config=DeviceConfig(host, credentials=Credentials(MOCK_USER, MOCK_PWD))
)
request = {"method": "getDeviceInfo", "params": None}

await transport.perform_handshake()
assert transport._state is TransportState.ESTABLISHED

mock_ssl_aes_device.status_code = 401
with pytest.raises(_RetryableError, match="session expired"):
await transport.send(json_dumps(request))
assert transport._state is TransportState.HANDSHAKE_REQUIRED
Loading