Skip to content

Commit 5018834

Browse files
committed
fix: add CM3000 auth diagnostics
1 parent fe7c9b2 commit 5018834

File tree

2 files changed

+94
-1
lines changed

2 files changed

+94
-1
lines changed

app/drivers/cm3000.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
"sessionstorage.getitem('privatekey')",
4040
"sessionstorage.getitem(\"privatekey\")",
4141
)
42+
_RE_TITLE = re.compile(r"<title[^>]*>(.*?)</title>", re.IGNORECASE | re.DOTALL)
43+
_RE_FORM_ACTION = re.compile(r"<form[^>]+action=['\"]([^'\"]+)['\"]", re.IGNORECASE)
4244

4345
# Fields per channel for each section (after the leading count value).
4446
_DS_QAM_FIELDS = 9 # num|lock|mod|chID|freq|power|snr|corrErr|uncorrErr
@@ -86,6 +88,9 @@ def login(self) -> None:
8688
raise RuntimeError("CM3000 authentication failed: connection refused after retry")
8789
except requests.RequestException as e:
8890
raise RuntimeError(f"CM3000 authentication failed: {e}")
91+
except RuntimeError as e:
92+
self._log_status_page_diagnostics(r.text, "login")
93+
raise e
8994

9095
def get_docsis_data(self) -> dict:
9196
"""Retrieve DOCSIS channel data from JavaScript on status page.
@@ -166,7 +171,11 @@ def _fetch_status_page(self) -> str:
166171
r.raise_for_status()
167172
except requests.RequestException as e:
168173
raise RuntimeError(f"CM3000 status page retrieval failed: {e}")
169-
self._ensure_status_page(r.text)
174+
try:
175+
self._ensure_status_page(r.text)
176+
except RuntimeError:
177+
self._log_status_page_diagnostics(r.text, "fetch")
178+
raise
170179
return r.text
171180

172181
@staticmethod
@@ -200,6 +209,59 @@ def _ensure_status_page(html: str) -> None:
200209
"CM3000 status page did not contain the expected DOCSIS data blocks"
201210
)
202211

212+
@staticmethod
213+
def _status_page_diagnostics(html: str) -> dict:
214+
"""Summarize the response shape for debugging failed CM3000 auth."""
215+
if not html:
216+
return {
217+
"length": 0,
218+
"title": "",
219+
"form_action": "",
220+
"login_markers": [],
221+
"has_sys_info": False,
222+
"has_channel_data": False,
223+
}
224+
225+
lower_html = html.lower()
226+
title_match = _RE_TITLE.search(html)
227+
form_match = _RE_FORM_ACTION.search(html)
228+
login_markers = [marker for marker in _LOGIN_MARKERS if marker in lower_html]
229+
has_sys_info = bool(CM3000Driver._extract_tag_value_list(html, "InitTagValue"))
230+
has_channel_data = any(
231+
CM3000Driver._extract_tag_value_list(html, function_name)
232+
for function_name in (
233+
"InitDsTableTagValue",
234+
"InitUsTableTagValue",
235+
"InitDsOfdmTableTagValue",
236+
"InitUsOfdmaTableTagValue",
237+
)
238+
)
239+
240+
return {
241+
"length": len(html),
242+
"title": (title_match.group(1).strip() if title_match else ""),
243+
"form_action": (form_match.group(1).strip() if form_match else ""),
244+
"login_markers": login_markers,
245+
"has_sys_info": has_sys_info,
246+
"has_channel_data": has_channel_data,
247+
}
248+
249+
@staticmethod
250+
def _log_status_page_diagnostics(html: str, context: str) -> None:
251+
"""Emit a compact debug summary of the CM3000 response page."""
252+
diag = CM3000Driver._status_page_diagnostics(html)
253+
log.debug(
254+
"CM3000 %s diagnostics: len=%s title=%r form_action=%r login_markers=%s "
255+
"has_sys_info=%s has_channel_data=%s",
256+
context,
257+
diag["length"],
258+
diag["title"],
259+
diag["form_action"],
260+
",".join(diag["login_markers"]) or "(none)",
261+
diag["has_sys_info"],
262+
diag["has_channel_data"],
263+
)
264+
203265
# -- Channel parsers --
204266

205267
def _parse_ds_qam(self, html: str) -> list:

tests/test_cm3000_driver.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,37 @@ def test_extracts_double_quoted_concatenated_assignment(self):
551551
)
552552

553553

554+
class TestDiagnostics:
555+
def test_status_page_diagnostics_for_valid_page(self):
556+
diag = CM3000Driver._status_page_diagnostics(STATUS_HTML)
557+
assert diag["title"] == "NETGEAR - Cable Modem CM3000"
558+
assert diag["form_action"] == ""
559+
assert diag["login_markers"] == []
560+
assert diag["has_sys_info"] is True
561+
assert diag["has_channel_data"] is True
562+
563+
def test_status_page_diagnostics_for_login_page(self):
564+
html = """
565+
<html><head><title>Login</title></head>
566+
<body>
567+
<script>
568+
if (sessionStorage.getItem('PrivateKey') === null) {
569+
window.location.replace('../Login.htm');
570+
}
571+
</script>
572+
<form action='/goform/Login'></form>
573+
</body></html>
574+
"""
575+
diag = CM3000Driver._status_page_diagnostics(html)
576+
assert diag["title"] == "Login"
577+
assert diag["form_action"] == "/goform/Login"
578+
assert "login.htm" in diag["login_markers"]
579+
assert "window.location.replace" in diag["login_markers"]
580+
assert "sessionstorage.getitem('privatekey')" in diag["login_markers"]
581+
assert diag["has_sys_info"] is False
582+
assert diag["has_channel_data"] is False
583+
584+
554585
# -- Collect cycle (cache reuse) --
555586

556587
class TestCollectCycle:

0 commit comments

Comments
 (0)