-
Notifications
You must be signed in to change notification settings - Fork 1
Send as form, not just form_encoded #118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
312cab4
4b2ddac
2b2abf7
9f066f3
7891ec7
8d8c267
0a1fe93
dc85ca5
7c64369
0507e91
d4a4d45
dbedf00
1d724a6
470c012
7d78630
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -5,14 +5,17 @@ | |||||||||||||
from abc import ABC | ||||||||||||||
import asyncio | ||||||||||||||
from collections.abc import Callable | ||||||||||||||
import contextlib | ||||||||||||||
from http.cookies import SimpleCookie | ||||||||||||||
import json | ||||||||||||||
import logging | ||||||||||||||
import time | ||||||||||||||
from typing import Any, Generic, TypeVar | ||||||||||||||
from urllib.parse import urlparse | ||||||||||||||
|
||||||||||||||
import aiohttp | ||||||||||||||
from mashumaro.exceptions import InvalidFieldValue, MissingField | ||||||||||||||
from yarl import URL | ||||||||||||||
|
||||||||||||||
from .data import ( | ||||||||||||||
AirOSDataBaseClass, | ||||||||||||||
|
@@ -65,7 +68,13 @@ def __init__( | |||||||||||||
|
||||||||||||||
self.base_url = f"{scheme}://{hostname}" | ||||||||||||||
|
||||||||||||||
self.session = session | ||||||||||||||
# self.session = session | ||||||||||||||
self.session = aiohttp.ClientSession( | ||||||||||||||
connector=aiohttp.TCPConnector(verify_ssl=False, force_close=True), | ||||||||||||||
cookie_jar=aiohttp.CookieJar(), | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
self.api_version: int = 8 | ||||||||||||||
|
||||||||||||||
self._use_json_for_login_post = False | ||||||||||||||
self._auth_cookie: str | None = None | ||||||||||||||
|
@@ -76,7 +85,7 @@ def __init__( | |||||||||||||
# Mostly 8.x API endpoints, login/status are the same in 6.x | ||||||||||||||
self._login_urls = { | ||||||||||||||
"default": f"{self.base_url}/api/auth", | ||||||||||||||
"v6_alternative": f"{self.base_url}/login.cgi", | ||||||||||||||
"v6_login": f"{self.base_url}/login.cgi", | ||||||||||||||
} | ||||||||||||||
self._status_cgi_url = f"{self.base_url}/status.cgi" | ||||||||||||||
# Presumed 8.x only endpoints | ||||||||||||||
|
@@ -201,10 +210,19 @@ def _get_authenticated_headers( | |||||||||||||
headers["Content-Type"] = "application/x-www-form-urlencoded" | ||||||||||||||
|
||||||||||||||
if self._csrf_id: # pragma: no cover | ||||||||||||||
_LOGGER.error("TESTv%s - CSRF ID found %s", self.api_version, self._csrf_id) | ||||||||||||||
headers["X-CSRF-ID"] = self._csrf_id | ||||||||||||||
|
||||||||||||||
""" | ||||||||||||||
if self._auth_cookie: # pragma: no cover | ||||||||||||||
headers["Cookie"] = f"AIROS_{self._auth_cookie}" | ||||||||||||||
_LOGGER.error( | ||||||||||||||
"TESTv%s - auth_cookie found: AIROS_%s", | ||||||||||||||
self.api_version, | ||||||||||||||
self._auth_cookie, | ||||||||||||||
) | ||||||||||||||
# headers["Cookie"] = f"AIROS_{self._auth_cookie}" | ||||||||||||||
headers["Cookie"] = self._auth_cookie | ||||||||||||||
""" | ||||||||||||||
|
||||||||||||||
return headers | ||||||||||||||
|
||||||||||||||
|
@@ -215,8 +233,17 @@ def _store_auth_data(self, response: aiohttp.ClientResponse) -> None: | |||||||||||||
# Parse all Set-Cookie headers to ensure we don't miss AIROS_* cookie | ||||||||||||||
cookie = SimpleCookie() | ||||||||||||||
for set_cookie in response.headers.getall("Set-Cookie", []): | ||||||||||||||
_LOGGER.error( | ||||||||||||||
"TESTv%s - regular cookie handling: %s", self.api_version, set_cookie | ||||||||||||||
) | ||||||||||||||
cookie.load(set_cookie) | ||||||||||||||
for key, morsel in cookie.items(): | ||||||||||||||
_LOGGER.error( | ||||||||||||||
"TESTv%s - AIROS_cookie handling: %s with %s", | ||||||||||||||
self.api_version, | ||||||||||||||
key, | ||||||||||||||
morsel.value, | ||||||||||||||
) | ||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
if key.startswith("AIROS_"): | ||||||||||||||
self._auth_cookie = morsel.key[6:] + "=" + morsel.value | ||||||||||||||
break | ||||||||||||||
|
@@ -227,10 +254,11 @@ async def _request_json( | |||||||||||||
url: str, | ||||||||||||||
headers: dict[str, Any] | None = None, | ||||||||||||||
json_data: dict[str, Any] | None = None, | ||||||||||||||
form_data: dict[str, Any] | None = None, | ||||||||||||||
form_data: dict[str, Any] | aiohttp.FormData | None = None, | ||||||||||||||
authenticated: bool = False, | ||||||||||||||
ct_json: bool = False, | ||||||||||||||
ct_form: bool = False, | ||||||||||||||
allow_redirects: bool = True, | ||||||||||||||
) -> dict[str, Any] | Any: | ||||||||||||||
"""Make an authenticated API request and return JSON response.""" | ||||||||||||||
# Pass the content type flags to the header builder | ||||||||||||||
|
@@ -242,27 +270,138 @@ async def _request_json( | |||||||||||||
if headers: | ||||||||||||||
request_headers.update(headers) | ||||||||||||||
|
||||||||||||||
# Potential XM fix - not sure, might have been login issue | ||||||||||||||
if self.api_version == 6 and url.startswith(self._status_cgi_url): | ||||||||||||||
# Ensure all HAR-matching headers are present | ||||||||||||||
request_headers["Accept"] = "application/json, text/javascript, */*; q=0.01" | ||||||||||||||
request_headers["Accept-Encoding"] = "gzip, deflate, br, zstd" | ||||||||||||||
request_headers["Accept-Language"] = "pl" | ||||||||||||||
request_headers["Cache-Control"] = "no-cache" | ||||||||||||||
request_headers["Connection"] = "keep-alive" | ||||||||||||||
request_headers["Host"] = ( | ||||||||||||||
urlparse(self.base_url).hostname or "192.168.1.142" | ||||||||||||||
) | ||||||||||||||
request_headers["Pragma"] = "no-cache" | ||||||||||||||
request_headers["Referer"] = f"{self.base_url}/index.cgi" | ||||||||||||||
request_headers["Sec-Fetch-Dest"] = "empty" | ||||||||||||||
request_headers["Sec-Fetch-Mode"] = "cors" | ||||||||||||||
request_headers["Sec-Fetch-Site"] = "same-origin" | ||||||||||||||
request_headers["User-Agent"] = ( | ||||||||||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" | ||||||||||||||
) | ||||||||||||||
request_headers["X-Requested-With"] = "XMLHttpRequest" | ||||||||||||||
request_headers["sec-ch-ua"] = ( | ||||||||||||||
'"Google Chrome";v="141", "Not?A_Brand";v="8", "Chromium";v="141"' | ||||||||||||||
) | ||||||||||||||
request_headers["sec-ch-ua-mobile"] = "?0" | ||||||||||||||
request_headers["sec-ch-ua-platform"] = '"Windows"' | ||||||||||||||
Comment on lines
+274
to
+297
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: Hardcoded IP fallback in Host header. Line 274 uses a hardcoded IP Apply this diff: request_headers["Host"] = (
- urlparse(self.base_url).hostname or "192.168.1.142"
+ urlparse(self.base_url).hostname or "device.local"
) 🤖 Prompt for AI Agents
|
||||||||||||||
if url.startswith(self._login_urls["v6_login"]): | ||||||||||||||
request_headers["Referrer"] = f"{self.base_url}/login.cgi" | ||||||||||||||
request_headers["Origin"] = self.base_url | ||||||||||||||
request_headers["Accept"] = ( | ||||||||||||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7" | ||||||||||||||
) | ||||||||||||||
request_headers["User-Agent"] = ( | ||||||||||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" | ||||||||||||||
) | ||||||||||||||
request_headers["Sec-Fetch-Dest"] = "document" | ||||||||||||||
request_headers["Sec-Fetch-Mode"] = "navigate" | ||||||||||||||
request_headers["Sec-Fetch-Site"] = "same-origin" | ||||||||||||||
request_headers["Sec-Fetch-User"] = "?1" | ||||||||||||||
request_headers["Cache-Control"] = "no-cache" | ||||||||||||||
request_headers["Pragma"] = "no-cache" | ||||||||||||||
|
||||||||||||||
try: | ||||||||||||||
if url not in self._login_urls.values() and not self.connected: | ||||||||||||||
if ( | ||||||||||||||
url not in self._login_urls.values() | ||||||||||||||
and url != f"{self.base_url}/" | ||||||||||||||
and not self.connected | ||||||||||||||
): | ||||||||||||||
_LOGGER.error("Not connected, login first") | ||||||||||||||
raise AirOSDeviceConnectionError from None | ||||||||||||||
|
||||||||||||||
Comment on lines
314
to
322
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blocker: Auth gate and v6/root handling — wrong conditions, JSON decode, and version flip.
@@
- if (
- url not in self._login_urls.values()
- and url != f"{self.base_url}/"
- and not self.connected
- ):
+ if (
+ authenticated
+ and url not in self._login_urls.values()
+ and url != f"{self.base_url}/"
+ and not self.connected
+ ):
_LOGGER.error("Not connected, login first")
raise AirOSDeviceConnectionError from None
@@
- # v6 responds with a 302 redirect and empty body
- if url != self._login_urls["v6_login"]:
- self.api_version = 6
- response.raise_for_status()
+ # v6 login (302/empty) and root prefetch may not be JSON; don't raise here
+ if url not in {self._login_urls["v6_login"], f"{self.base_url}/"}:
+ response.raise_for_status()
@@
- response_text = await response.text()
- _LOGGER.error("Successfully fetched %s from %s", response_text, url)
+ response_text = await response.text()
+ _LOGGER.debug("Fetched response from %s", url)
@@
- _LOGGER.error("TESTv%s - response: %s", self.api_version, response_text)
- # V6 responds with empty body on login, not JSON
- if url.startswith(self._login_urls["v6_login"]):
- self._store_auth_data(response)
- self.connected = True
- return {}
+ _LOGGER.debug("Response body from %s: %s", url, response_text[:200])
+ # Root cookie prefetch: no JSON expected
+ if method == "GET" and url == f"{self.base_url}/":
+ return {}
+ # v6 login: often empty body/redirect; do not JSON-decode
+ if url == self._login_urls["v6_login"]:
+ return {} if not response_text.strip() else response_text Also applies to: 308-315, 321-328 🤖 Prompt for AI Agents
|
||||||||||||||
if self.api_version == 6 and url.startswith(self._status_cgi_url): | ||||||||||||||
_LOGGER.error( | ||||||||||||||
"TESTv%s - adding timestamp to status url!", self.api_version | ||||||||||||||
) | ||||||||||||||
timestamp = int(time.time() * 1000) | ||||||||||||||
url = f"{self._status_cgi_url}?_={timestamp}" | ||||||||||||||
|
||||||||||||||
_LOGGER.error("TESTv%s - Trying with URL: %s", self.api_version, url) | ||||||||||||||
async with self.session.request( | ||||||||||||||
method, | ||||||||||||||
url, | ||||||||||||||
json=json_data, | ||||||||||||||
data=form_data, | ||||||||||||||
headers=request_headers, # Pass the constructed headers | ||||||||||||||
allow_redirects=allow_redirects, | ||||||||||||||
) as response: | ||||||||||||||
response.raise_for_status() | ||||||||||||||
_LOGGER.error( | ||||||||||||||
"TESTv%s - Response code: %s", self.api_version, response.status | ||||||||||||||
) | ||||||||||||||
_LOGGER.error( | ||||||||||||||
"TESTv%s - Response headers: %s", | ||||||||||||||
self.api_version, | ||||||||||||||
dict(response.headers), | ||||||||||||||
) | ||||||||||||||
Comment on lines
+330
to
+346
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use debug level for diagnostic logs. Lines 310, 319-320, and 322-326 log diagnostic information at error level. These are not errors but informational messages for debugging request flow. Apply this diff: - _LOGGER.error("TESTv%s - Trying with URL: %s", self.api_version, url)
+ _LOGGER.debug("Requesting URL: %s (api_version=%s)", url, self.api_version)
async with self.session.request(
...
) as response:
- _LOGGER.error(
- "TESTv%s - Response code: %s", self.api_version, response.status
- )
- _LOGGER.error(
- "TESTv%s - Response headers: %s",
- self.api_version,
- dict(response.headers),
- )
+ _LOGGER.debug("Response status: %s (api_version=%s)", response.status, self.api_version)
+ _LOGGER.debug("Response headers: %s (api_version=%s)", dict(response.headers), self.api_version) 🤖 Prompt for AI Agents
|
||||||||||||||
_LOGGER.error( | ||||||||||||||
"TESTv%s - Response history: %s", self.api_version, response.history | ||||||||||||||
) | ||||||||||||||
_LOGGER.error( | ||||||||||||||
"TESTv%s - Session cookies: %s", | ||||||||||||||
self.api_version, | ||||||||||||||
self.session.cookie_jar.filter_cookies(URL(url)), | ||||||||||||||
) | ||||||||||||||
Comment on lines
+323
to
+354
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use debug level for diagnostic logs. Lines 318-320, 324, and 333-348 log diagnostic information (timestamp addition, request URL, response status/headers/history/cookies) at error level. These are informational debug messages, not errors. Apply this diff: if self.api_version == 6 and url.startswith(self._status_cgi_url):
- _LOGGER.error(
- "TESTv%s - adding timestamp to status url!", self.api_version
- )
+ _LOGGER.debug("Adding timestamp to status URL (api_version=%s)", self.api_version)
timestamp = int(time.time() * 1000)
url = f"{self._status_cgi_url}?_={timestamp}"
- _LOGGER.error("TESTv%s - Trying with URL: %s", self.api_version, url)
+ _LOGGER.debug("Requesting URL: %s (api_version=%s)", url, self.api_version)
async with self.session.request(
...
) as response:
- _LOGGER.error(
- "TESTv%s - Response code: %s", self.api_version, response.status
- )
- _LOGGER.error(
- "TESTv%s - Response headers: %s",
- self.api_version,
- dict(response.headers),
- )
- _LOGGER.error(
- "TESTv%s - Response history: %s", self.api_version, response.history
- )
- _LOGGER.error(
- "TESTv%s - Session cookies: %s",
- self.api_version,
- self.session.cookie_jar.filter_cookies(URL(url)),
- )
+ _LOGGER.debug("Response status: %s (api_version=%s)", response.status, self.api_version)
+ _LOGGER.debug("Response headers: %s (api_version=%s)", dict(response.headers), self.api_version)
+ _LOGGER.debug("Response history: %s (api_version=%s)", response.history, self.api_version)
+ _LOGGER.debug("Session cookies: %s (api_version=%s)",
+ self.session.cookie_jar.filter_cookies(URL(url)), self.api_version) Based on learnings. 🤖 Prompt for AI Agents
|
||||||||||||||
|
||||||||||||||
# v6 responds with a 302 redirect and empty body | ||||||||||||||
if not url.startswith(self._login_urls["v6_login"]): | ||||||||||||||
self.api_version = 6 | ||||||||||||||
response.raise_for_status() | ||||||||||||||
|
||||||||||||||
response_text = await response.text() | ||||||||||||||
_LOGGER.debug("Successfully fetched JSON from %s", url) | ||||||||||||||
_LOGGER.error("Successfully fetched %s from %s", response_text, url) | ||||||||||||||
if not response_text.strip(): | ||||||||||||||
_LOGGER.error( | ||||||||||||||
"TESTv%s - Empty response from %s despite %s", | ||||||||||||||
self.api_version, | ||||||||||||||
url, | ||||||||||||||
response.status, | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
# If this is the login request, we need to store the new auth data | ||||||||||||||
if url in self._login_urls.values(): | ||||||||||||||
self._store_auth_data(response) | ||||||||||||||
self.connected = True | ||||||||||||||
|
||||||||||||||
_LOGGER.error("TESTv%s - response: %s", self.api_version, response_text) | ||||||||||||||
|
||||||||||||||
location = response.headers.get("Location") | ||||||||||||||
if location and isinstance(location, str) and location.startswith("/"): | ||||||||||||||
_LOGGER.error( | ||||||||||||||
"TESTv%s - Following redirect to: %s", | ||||||||||||||
self.api_version, | ||||||||||||||
location, | ||||||||||||||
) | ||||||||||||||
await self._request_json( | ||||||||||||||
"GET", | ||||||||||||||
f"{self.base_url}{location}", | ||||||||||||||
headers={ | ||||||||||||||
"Referer": self._login_urls["v6_login"], | ||||||||||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36", | ||||||||||||||
}, | ||||||||||||||
authenticated=True, | ||||||||||||||
allow_redirects=False, | ||||||||||||||
) | ||||||||||||||
else: | ||||||||||||||
_LOGGER.error( | ||||||||||||||
"TESTv%s - no location header found to follow in response to %s", | ||||||||||||||
self.api_version, | ||||||||||||||
url, | ||||||||||||||
) | ||||||||||||||
# V6 responds with empty body on login, not JSON | ||||||||||||||
if url.startswith(self._login_urls["v6_login"]): | ||||||||||||||
return {} | ||||||||||||||
|
||||||||||||||
Comment on lines
+376
to
+404
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use debug level for redirect and response logging. Lines 370, 374-378, 390-394 log normal redirect-following flow at error level. These are informational debug messages. Apply this diff: - _LOGGER.error("TESTv%s - response: %s", self.api_version, response_text)
+ _LOGGER.debug("Response body (api_version=%s): %s", self.api_version, response_text[:200])
location = response.headers.get("Location")
if location and isinstance(location, str) and location.startswith("/"):
- _LOGGER.error(
- "TESTv%s - Following redirect to: %s",
- self.api_version,
- location,
- )
+ _LOGGER.debug("Following redirect to: %s (api_version=%s)", location, self.api_version)
await self._request_json(
...
)
else:
- _LOGGER.error(
- "TESTv%s - no location header found to follow in response to %s",
- self.api_version,
- url,
- )
+ _LOGGER.debug("No location header in response to %s (api_version=%s)", url, self.api_version) Based on learnings. 🤖 Prompt for AI Agents
|
||||||||||||||
return json.loads(response_text) | ||||||||||||||
except aiohttp.ClientResponseError as err: | ||||||||||||||
_LOGGER.error( | ||||||||||||||
|
@@ -287,32 +426,126 @@ async def login(self) -> None: | |||||||||||||
"""Login to AirOS device.""" | ||||||||||||||
payload = {"username": self.username, "password": self.password} | ||||||||||||||
try: | ||||||||||||||
_LOGGER.error("TESTv%s - Trying default v8 login URL", self.api_version) | ||||||||||||||
await self._request_json( | ||||||||||||||
"POST", self._login_urls["default"], json_data=payload | ||||||||||||||
) | ||||||||||||||
except AirOSUrlNotFoundError: | ||||||||||||||
pass # Try next URL | ||||||||||||||
_LOGGER.error( | ||||||||||||||
"TESTv%s - gives URL not found, trying alternative v6 URL", | ||||||||||||||
self.api_version, | ||||||||||||||
) | ||||||||||||||
# Try next URL | ||||||||||||||
except AirOSConnectionSetupError as err: | ||||||||||||||
_LOGGER.error("TESTv%s - failed to login to v8 URL", self.api_version) | ||||||||||||||
raise AirOSConnectionSetupError("Failed to login to AirOS device") from err | ||||||||||||||
else: | ||||||||||||||
_LOGGER.error("TESTv%s - returning from v8 login", self.api_version) | ||||||||||||||
Comment on lines
+429
to
+443
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. MINOR: Use debug level for login flow logs. Lines 394, 399-402, 405, and 408 log normal login flow information at error level. These should be debug level. Apply this diff: - _LOGGER.error("TESTv%s - Trying default v8 login URL", self.api_version)
+ _LOGGER.debug("Attempting v8 login (api_version=%s)", self.api_version)
await self._request_json(
"POST", self._login_urls["default"], json_data=payload
)
except AirOSUrlNotFoundError:
- _LOGGER.error(
- "TESTv%s - gives URL not found, trying alternative v6 URL",
- self.api_version,
- )
+ _LOGGER.debug("v8 login not found, attempting v6 login (api_version=%s)", self.api_version)
# Try next URL
except AirOSConnectionSetupError as err:
- _LOGGER.error("TESTv%s - failed to login to v8 URL", self.api_version)
+ _LOGGER.debug("v8 login failed (api_version=%s)", self.api_version)
raise AirOSConnectionSetupError("Failed to login to AirOS device") from err
else:
- _LOGGER.error("TESTv%s - returning from v8 login", self.api_version)
+ _LOGGER.debug("v8 login succeeded (api_version=%s)", self.api_version)
return 🤖 Prompt for AI Agents
|
||||||||||||||
return | ||||||||||||||
|
||||||||||||||
try: # Alternative URL | ||||||||||||||
# Start of v6, go for cookies | ||||||||||||||
_LOGGER.error( | ||||||||||||||
"TESTv%s - Trying to get /index.cgi first for cookies", self.api_version | ||||||||||||||
) | ||||||||||||||
with contextlib.suppress(Exception): | ||||||||||||||
cookieresponse = await self._request_json( | ||||||||||||||
"GET", | ||||||||||||||
f"{self.base_url}/index.cgi", | ||||||||||||||
authenticated=True, | ||||||||||||||
headers={ | ||||||||||||||
"Referer": f"{self.base_url}/login.cgi", | ||||||||||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36", | ||||||||||||||
}, | ||||||||||||||
) | ||||||||||||||
_LOGGER.error( | ||||||||||||||
"TESTv%s - Cookie response: %s", self.api_version, cookieresponse | ||||||||||||||
) | ||||||||||||||
if isinstance(cookieresponse, aiohttp.ClientResponse): | ||||||||||||||
_LOGGER.debug( | ||||||||||||||
"TESTv%s - Finalization redirect chain: %s", | ||||||||||||||
self.api_version, | ||||||||||||||
cookieresponse.history, | ||||||||||||||
) | ||||||||||||||
else: | ||||||||||||||
_LOGGER.debug( | ||||||||||||||
"TESTv%s - Finalization response is not a ClientResponse: %s", | ||||||||||||||
self.api_version, | ||||||||||||||
type(cookieresponse), | ||||||||||||||
) | ||||||||||||||
Comment on lines
+446
to
+474
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Major: Overly broad exception suppression in cookie prefetch. Line 450 uses Apply this diff: # Start of v6, go for cookies
- _LOGGER.error(
- "TESTv%s - Trying to get /index.cgi first for cookies", self.api_version
- )
- with contextlib.suppress(Exception):
+ _LOGGER.debug("Prefetching cookies from /index.cgi (api_version=%s)", self.api_version)
+ try:
cookieresponse = await self._request_json(
"GET",
f"{self.base_url}/index.cgi",
authenticated=True,
headers={
"Referer": f"{self.base_url}/login.cgi",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36",
},
)
- _LOGGER.error(
- "TESTv%s - Cookie response: %s", self.api_version, cookieresponse
- )
- if isinstance(cookieresponse, aiohttp.ClientResponse):
- _LOGGER.debug(
- "TESTv%s - Finalization redirect chain: %s",
- self.api_version,
- cookieresponse.history,
- )
- else:
- _LOGGER.debug(
- "TESTv%s - Finalization response is not a ClientResponse: %s",
- self.api_version,
- type(cookieresponse),
- )
+ _LOGGER.debug("Cookie prefetch response: %s (api_version=%s)", cookieresponse, self.api_version)
+ except (aiohttp.ClientError, TimeoutError, AirOSDeviceConnectionError) as err:
+ _LOGGER.debug("Cookie prefetch failed (non-fatal): %s", err) 🤖 Prompt for AI Agents
|
||||||||||||||
|
||||||||||||||
v6_simple_multipart_form_data = aiohttp.FormData() | ||||||||||||||
v6_simple_multipart_form_data.add_field("uri", "/index.cgi") | ||||||||||||||
v6_simple_multipart_form_data.add_field("username", self.username) | ||||||||||||||
v6_simple_multipart_form_data.add_field("password", self.password) | ||||||||||||||
|
||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
_LOGGER.debug( | ||||||||||||||
"TESTv%s !!!REDACT THIS!!!! Form payload: %s", | ||||||||||||||
self.api_version, | ||||||||||||||
v6_simple_multipart_form_data(), | ||||||||||||||
) | ||||||||||||||
Comment on lines
+481
to
+485
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CRITICAL: Form payload with password logged. Lines 434-438 log the form payload at debug level. Even though it says "REDACT THIS", the code still executes Apply this diff to remove the password logging: - _LOGGER.debug(
- "TESTv%s !!!REDACT THIS!!!! Form payload: %s",
- self.api_version,
- v6_simple_multipart_form_data(),
- )
+ _LOGGER.debug("Prepared v6 login form data (api_version=%s)", self.api_version) 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||
|
||||||||||||||
login_headers = { | ||||||||||||||
"Referer": self._login_urls["v6_login"], | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
_LOGGER.error("TESTv%s - start v6 attempts", self.api_version) | ||||||||||||||
# --- ATTEMPT B: Simple Payload (multipart/form-data) --- | ||||||||||||||
try: | ||||||||||||||
_LOGGER.error( | ||||||||||||||
"TESTv%s - Trying V6 POST to %s with SIMPLE multipart/form-data", | ||||||||||||||
self.api_version, | ||||||||||||||
self._login_urls["v6_login"], | ||||||||||||||
) | ||||||||||||||
await self._request_json( | ||||||||||||||
"POST", | ||||||||||||||
self._login_urls["v6_alternative"], | ||||||||||||||
form_data=payload, | ||||||||||||||
ct_form=True, | ||||||||||||||
self._login_urls["v6_login"], | ||||||||||||||
headers=login_headers, | ||||||||||||||
form_data=v6_simple_multipart_form_data, | ||||||||||||||
ct_form=False, | ||||||||||||||
ct_json=False, | ||||||||||||||
authenticated=False, | ||||||||||||||
allow_redirects=False, | ||||||||||||||
) | ||||||||||||||
except AirOSConnectionSetupError as err: | ||||||||||||||
raise AirOSConnectionSetupError( | ||||||||||||||
"Failed to login to default and alternate AirOS device urls" | ||||||||||||||
) from err | ||||||||||||||
except (AirOSUrlNotFoundError, AirOSConnectionSetupError) as err: | ||||||||||||||
_LOGGER.error( | ||||||||||||||
"TESTv%s - V6 simple multipart failed (%s) on %s. Error: %s", | ||||||||||||||
self.api_version, | ||||||||||||||
type(err).__name__, | ||||||||||||||
self._login_urls["v6_login"], | ||||||||||||||
err, | ||||||||||||||
) | ||||||||||||||
except AirOSConnectionAuthenticationError: | ||||||||||||||
_LOGGER.error( | ||||||||||||||
"TESTv%s - autherror during extended multipart", self.api_version | ||||||||||||||
) | ||||||||||||||
raise | ||||||||||||||
Comment on lines
+517
to
+521
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Message mismatch: says “extended multipart” inside simple attempt. Adjust text to match the attempt type. - _LOGGER.error(
- "TESTv%s - autherror during extended multipart", self.api_version
- )
+ _LOGGER.debug("v6 login auth error (simple multipart) (v%s)", self.api_version) 🤖 Prompt for AI Agents
|
||||||||||||||
else: | ||||||||||||||
_LOGGER.error("TESTv%s - returning from simple multipart", self.api_version) | ||||||||||||||
# Finalize session by visiting /index.cgi | ||||||||||||||
_LOGGER.error( | ||||||||||||||
"TESTv%s - Finalizing session with GET to /index.cgi", self.api_version | ||||||||||||||
) | ||||||||||||||
with contextlib.suppress(Exception): | ||||||||||||||
await self._request_json( | ||||||||||||||
"GET", | ||||||||||||||
f"{self.base_url}/index.cgi", | ||||||||||||||
headers={ | ||||||||||||||
"Referer": f"{self.base_url}/login.cgi", | ||||||||||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36", | ||||||||||||||
}, | ||||||||||||||
authenticated=True, | ||||||||||||||
allow_redirects=True, | ||||||||||||||
) | ||||||||||||||
return # Success | ||||||||||||||
Comment on lines
+522
to
+539
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Major: Overly broad exception suppression in finalization. Line 443 uses Apply this diff: else:
- _LOGGER.error("TESTv%s - returning from simple multipart", self.api_version)
+ _LOGGER.debug("v6 simple multipart succeeded (api_version=%s)", self.api_version)
# Finalize session by visiting /index.cgi
- _LOGGER.error(
- "TESTv%s - Finalizing session with GET to /index.cgi", self.api_version
- )
- with contextlib.suppress(Exception):
+ _LOGGER.debug("Finalizing v6 session (api_version=%s)", self.api_version)
+ try:
await self._request_json(
"GET",
f"{self.base_url}/index.cgi",
headers={
"Referer": f"{self.base_url}/login.cgi",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36",
},
authenticated=True,
allow_redirects=True,
)
+ except (aiohttp.ClientError, TimeoutError, AirOSDeviceConnectionError) as err:
+ _LOGGER.debug("Session finalization failed (non-fatal): %s", err)
return # Success 🤖 Prompt for AI Agents
|
||||||||||||||
|
||||||||||||||
async def status(self) -> AirOSDataModel: | ||||||||||||||
"""Retrieve status from the device.""" | ||||||||||||||
status_headers = { | ||||||||||||||
"Accept": "application/json, text/javascript, */*; q=0.01", | ||||||||||||||
"X-Requested-With": "XMLHttpRequest", | ||||||||||||||
} | ||||||||||||||
response = await self._request_json( | ||||||||||||||
"GET", self._status_cgi_url, authenticated=True | ||||||||||||||
"GET", self._status_cgi_url, authenticated=True, headers=status_headers | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
try: | ||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical: Passed session parameter ignored, causing resource leak.
The code comments out the intended use of the passed
session
parameter and creates a newClientSession
that is never closed. This breaks the API contract and causes resource leaks.Apply this diff to use the passed session:
If you need to configure the session differently, do so at the call site where the session is created, not here.
🤖 Prompt for AI Agents