Skip to content

Commit 0507e91

Browse files
committed
Add timestamps and full UA/AJAX to prevent 204
1 parent 7c64369 commit 0507e91

File tree

1 file changed

+61
-22
lines changed

1 file changed

+61
-22
lines changed

airos/base.py

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from http.cookies import SimpleCookie
1010
import json
1111
import logging
12+
import time
1213
from typing import Any, Generic, TypeVar
1314
from urllib.parse import urlparse
1415

@@ -68,6 +69,8 @@ def __init__(
6869

6970
self.session = session
7071

72+
self.api_version: int = 8
73+
7174
self._use_json_for_login_post = False
7275
self._auth_cookie: str | None = None
7376
self._csrf_id: str | None = None
@@ -202,11 +205,15 @@ def _get_authenticated_headers(
202205
headers["Content-Type"] = "application/x-www-form-urlencoded"
203206

204207
if self._csrf_id: # pragma: no cover
205-
_LOGGER.error("TESTv6 - CSRF ID found %s", self._csrf_id)
208+
_LOGGER.error("TESTv%s - CSRF ID found %s", self.api_version, self._csrf_id)
206209
headers["X-CSRF-ID"] = self._csrf_id
207210

208211
if self._auth_cookie: # pragma: no cover
209-
_LOGGER.error("TESTv6 - auth_cookie found: AIROS_%s", self._auth_cookie)
212+
_LOGGER.error(
213+
"TESTv%s - auth_cookie found: AIROS_%s",
214+
self.api_version,
215+
self._auth_cookie,
216+
)
210217
headers["Cookie"] = f"AIROS_{self._auth_cookie}"
211218

212219
return headers
@@ -218,11 +225,16 @@ def _store_auth_data(self, response: aiohttp.ClientResponse) -> None:
218225
# Parse all Set-Cookie headers to ensure we don't miss AIROS_* cookie
219226
cookie = SimpleCookie()
220227
for set_cookie in response.headers.getall("Set-Cookie", []):
221-
_LOGGER.error("TESTv6 - regular cookie handling: %s", set_cookie)
228+
_LOGGER.error(
229+
"TESTv%s - regular cookie handling: %s", self.api_version, set_cookie
230+
)
222231
cookie.load(set_cookie)
223232
for key, morsel in cookie.items():
224233
_LOGGER.error(
225-
"TESTv6 - AIROS_cookie handling: %s with %s", key, morsel.value
234+
"TESTv%s - AIROS_cookie handling: %s with %s",
235+
self.api_version,
236+
key,
237+
morsel.value,
226238
)
227239
if key.startswith("AIROS_"):
228240
self._auth_cookie = morsel.key[6:] + "=" + morsel.value
@@ -251,21 +263,36 @@ async def _request_json(
251263
request_headers.update(headers)
252264

253265
# Potential XM fix - not sure, might have been login issue
254-
if url == self._status_cgi_url:
255-
request_headers["Referrer"] = f"{self.base_url}/login.cgi"
266+
if self.api_version == 6 and url.startswith(self._status_cgi_url):
267+
# Modified from login.cgi to index.cgi
268+
request_headers["Referrer"] = f"{self.base_url}/index.cgi"
256269
request_headers["Accept"] = "application/json, text/javascript, */*; q=0.01"
257270
request_headers["X-Requested-With"] = "XMLHttpRequest"
271+
# Added AJAX / UA
272+
request_headers["User-Agent"] = (
273+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
274+
)
275+
request_headers["Sec-Fetch-Dest"] = "empty"
276+
request_headers["Sec-Fetch-Mode"] = "cors"
277+
request_headers["Sec-Fetch-Site"] = "same-origin"
258278

259279
try:
260280
if (
261281
url not in self._login_urls.values()
262-
and url != "/"
282+
and url != f"{self.base_url}/"
263283
and not self.connected
264284
):
265285
_LOGGER.error("Not connected, login first")
266286
raise AirOSDeviceConnectionError from None
267287

268-
_LOGGER.error("TESTv6 - Trying with URL: %s", url)
288+
if self.api_version == 6 and url.startswith(self._status_cgi_url):
289+
_LOGGER.error(
290+
"TESTv%s - adding timestamp to status url!", self.api_version
291+
)
292+
timestamp = int(time.time() * 1000)
293+
url = f"{self._status_cgi_url}?_={timestamp}"
294+
295+
_LOGGER.error("TESTv%s - Trying with URL: %s", self.api_version, url)
269296
async with self.session.request(
270297
method,
271298
url,
@@ -274,10 +301,13 @@ async def _request_json(
274301
headers=request_headers, # Pass the constructed headers
275302
allow_redirects=allow_redirects,
276303
) as response:
277-
_LOGGER.error("TESTv6 - Response code: %s", response.status)
304+
_LOGGER.error(
305+
"TESTv%s - Response code: %s", self.api_version, response.status
306+
)
278307

279308
# v6 responds with a 302 redirect and empty body
280309
if url != self._login_urls["v6_login"]:
310+
self.api_version = 6
281311
response.raise_for_status()
282312

283313
response_text = await response.text()
@@ -288,9 +318,9 @@ async def _request_json(
288318
self._store_auth_data(response)
289319
self.connected = True
290320

291-
_LOGGER.error("TESTv6 - response: %s", response_text)
321+
_LOGGER.error("TESTv%s - response: %s", self.api_version, response_text)
292322
# V6 responds with empty body on login, not JSON
293-
if url == self._login_urls["v6_login"]:
323+
if url.startswith(self._login_urls["v6_login"]):
294324
self._store_auth_data(response)
295325
self.connected = True
296326
return {}
@@ -319,27 +349,32 @@ async def login(self) -> None:
319349
"""Login to AirOS device."""
320350
payload = {"username": self.username, "password": self.password}
321351
try:
322-
_LOGGER.error("TESTv6 - Trying default v8 login URL")
352+
_LOGGER.error("TESTv%s - Trying default v8 login URL", self.api_version)
323353
await self._request_json(
324354
"POST", self._login_urls["default"], json_data=payload
325355
)
326356
except AirOSUrlNotFoundError:
327-
_LOGGER.error("TESTv6 - gives URL not found, trying alternative v6 URL")
357+
_LOGGER.error(
358+
"TESTv%s - gives URL not found, trying alternative v6 URL",
359+
self.api_version,
360+
)
328361
# Try next URL
329362
except AirOSConnectionSetupError as err:
330-
_LOGGER.error("TESTv6 - failed to login to v8 URL")
363+
_LOGGER.error("TESTv%s - failed to login to v8 URL", self.api_version)
331364
raise AirOSConnectionSetupError("Failed to login to AirOS device") from err
332365
else:
333-
_LOGGER.error("TESTv6 - returning from v8 login")
366+
_LOGGER.error("TESTv%s - returning from v8 login", self.api_version)
334367
return
335368

336369
# Start of v6, go for cookies
337-
_LOGGER.error("TESTv6 - Trying to get / first for cookies")
370+
_LOGGER.error("TESTv%s - Trying to get / first for cookies", self.api_version)
338371
with contextlib.suppress(Exception):
339372
cookieresponse = await self._request_json(
340373
"GET", f"{self.base_url}/", authenticated=True
341374
)
342-
_LOGGER.error("TESTv6 - Cookie response: %s", cookieresponse)
375+
_LOGGER.error(
376+
"TESTv%s - Cookie response: %s", self.api_version, cookieresponse
377+
)
343378

344379
v6_simple_multipart_form_data = aiohttp.FormData()
345380
v6_simple_multipart_form_data.add_field("uri", "/index.cgi")
@@ -350,11 +385,12 @@ async def login(self) -> None:
350385
"Referer": self._login_urls["v6_login"],
351386
}
352387

353-
_LOGGER.error("TESTv6 - start v6 attempts")
388+
_LOGGER.error("TESTv%s - start v6 attempts", self.api_version)
354389
# --- ATTEMPT B: Simple Payload (multipart/form-data) ---
355390
try:
356391
_LOGGER.error(
357-
"TESTv6 - Trying V6 POST to %s with SIMPLE multipart/form-data",
392+
"TESTv%s - Trying V6 POST to %s with SIMPLE multipart/form-data",
393+
self.api_version,
358394
self._login_urls["v6_login"],
359395
)
360396
await self._request_json(
@@ -367,16 +403,19 @@ async def login(self) -> None:
367403
)
368404
except (AirOSUrlNotFoundError, AirOSConnectionSetupError) as err:
369405
_LOGGER.error(
370-
"TESTv6 - V6 simple multipart failed (%s) on %s. Error: %s",
406+
"TESTv%s - V6 simple multipart failed (%s) on %s. Error: %s",
407+
self.api_version,
371408
type(err).__name__,
372409
self._login_urls["v6_login"],
373410
err,
374411
)
375412
except AirOSConnectionAuthenticationError:
376-
_LOGGER.error("TESTv6 - autherror during extended multipart")
413+
_LOGGER.error(
414+
"TESTv%s - autherror during extended multipart", self.api_version
415+
)
377416
raise
378417
else:
379-
_LOGGER.error("TESTv6 - returning from simple multipart")
418+
_LOGGER.error("TESTv%s - returning from simple multipart", self.api_version)
380419
return # Success
381420

382421
async def status(self) -> AirOSDataModel:

0 commit comments

Comments
 (0)