Keep the session token out of the HTTP client's debug log (#1835)

Camera logins over `SslAesTransport` send their requests to `/stok=<token>/ds`, and `HttpClient` logs every URL it posts to at debug, so the session token ends up in the debug logs people paste into issues.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZeliardM
2026-11-04 12:56:22 -05:00
committed by GitHub
parent a68f2b3304
commit 1c3be98e67
2 changed files with 41 additions and 1 deletions

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
import asyncio import asyncio
import builtins import builtins
import logging import logging
import re
import ssl import ssl
import time import time
from typing import Any from typing import Any
@@ -22,6 +23,9 @@ from .json import loads as json_loads
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# The session token in /stok=<token>/ds URLs, e.g. after a camera's secure login
_SESSION_TOKEN = re.compile(r"(stok=)[^/?#]+")
def get_cookie_jar() -> aiohttp.CookieJar: def get_cookie_jar() -> aiohttp.CookieJar:
"""Return a new cookie jar with the correct options for device communication.""" """Return a new cookie jar with the correct options for device communication."""
@@ -86,7 +90,8 @@ class HttpClient:
) )
await asyncio.sleep(sleep) await asyncio.sleep(sleep)
_LOGGER.debug("Posting to %s", url) if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug("Posting to %s", _SESSION_TOKEN.sub(r"\1REDACTED", str(url)))
response_data = None response_data = None
self._last_url = url self._last_url = url
self.client.cookie_jar.clear() self.client.cookie_jar.clear()

View File

@@ -1,4 +1,5 @@
import builtins import builtins
import logging
import re import re
import aiohttp import aiohttp
@@ -114,3 +115,37 @@ async def test_httpclient_errors(
assert mock_response.call_count == 1 assert mock_response.call_count == 1
else: else:
assert conn.call_count == 1 assert conn.call_count == 1
class _OkResponse:
status = 200
connection = None
async def __aenter__(self):
return self
async def __aexit__(self, exc_t, exc_v, exc_tb) -> None:
pass
async def read(self) -> bytes:
return b"{}"
async def test_httpclient_debug_log_hides_session_token(
mocker: MockerFixture, caplog: pytest.LogCaptureFixture
) -> None:
"""The /stok=<token>/ds session token of a camera login is not logged."""
async def _post(url, *_, **__):
return _OkResponse()
mocker.patch.object(aiohttp.ClientSession, "post", side_effect=_post)
client = HttpClient(DeviceConfig("127.0.0.1"))
with caplog.at_level(logging.DEBUG, logger="kasa.httpclient"):
await client.post(URL("https://127.0.0.1/stok=S3cr3tT0k3n/ds"), data=b"x")
await client.post(URL("http://127.0.0.1/app/handshake1"), data=b"x")
assert "S3cr3tT0k3n" not in caplog.text
assert "Posting to https://127.0.0.1/stok=REDACTED/ds" in caplog.text
assert "Posting to http://127.0.0.1/app/handshake1" in caplog.text