python-netgear-switch-library 0.0.post353__py3-none-any.whl → 0.0.post355__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- netgear_switch/_version.py +2 -2
- netgear_switch/capabilities.py +17 -1
- netgear_switch/http_write.py +23 -0
- netgear_switch/protocols/http/endpoints.py +21 -0
- netgear_switch/virtual/web.py +32 -0
- {python_netgear_switch_library-0.0.post353.dist-info → python_netgear_switch_library-0.0.post355.dist-info}/METADATA +1 -1
- {python_netgear_switch_library-0.0.post353.dist-info → python_netgear_switch_library-0.0.post355.dist-info}/RECORD +10 -10
- {python_netgear_switch_library-0.0.post353.dist-info → python_netgear_switch_library-0.0.post355.dist-info}/WHEEL +0 -0
- {python_netgear_switch_library-0.0.post353.dist-info → python_netgear_switch_library-0.0.post355.dist-info}/entry_points.txt +0 -0
- {python_netgear_switch_library-0.0.post353.dist-info → python_netgear_switch_library-0.0.post355.dist-info}/licenses/LICENSE +0 -0
netgear_switch/_version.py
CHANGED
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 0, '
|
|
21
|
+
__version__ = version = '0.0.post355'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 0, 'post355')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
netgear_switch/capabilities.py
CHANGED
|
@@ -101,6 +101,11 @@ class Operation:
|
|
|
101
101
|
|
|
102
102
|
_CLI_BACKENDS = frozenset({Backend.SSH, Backend.TELNET, Backend.CONSOLE})
|
|
103
103
|
|
|
104
|
+
#: HTTP writes implemented by scraping the Plus dialect's CSRF token. A dialect
|
|
105
|
+
#: without that token cannot serve any of them -- see
|
|
106
|
+
#: ``endpoints.dialect_has_csrf_hash`` for the measurement.
|
|
107
|
+
_CSRF_HTTP_WRITES = frozenset({"create_vlan", "delete_vlan"})
|
|
108
|
+
|
|
104
109
|
READ_OPERATIONS: tuple[Operation, ...] = (
|
|
105
110
|
Operation("get_ports", OperationKind.READ, "Per-port link/admin status"),
|
|
106
111
|
Operation("get_stats", OperationKind.READ, "Per-port octet/packet counters"),
|
|
@@ -351,7 +356,7 @@ def _http_path_for(spec: HttpModelSpec, op: Operation) -> str | None:
|
|
|
351
356
|
|
|
352
357
|
def _http_support(model: SwitchModel, op: Operation) -> tuple[Support, str]:
|
|
353
358
|
from .http_write import CERT_UPLOAD_KNOWN_UNIMPLEMENTED
|
|
354
|
-
from .protocols.http.endpoints import http_spec
|
|
359
|
+
from .protocols.http.endpoints import dialect_has_csrf_hash, http_spec
|
|
355
360
|
|
|
356
361
|
spec = http_spec(model)
|
|
357
362
|
if not spec.reads_verified:
|
|
@@ -374,6 +379,17 @@ def _http_support(model: SwitchModel, op: Operation) -> tuple[Support, str]:
|
|
|
374
379
|
f"this model takes a certificate by {mechanism}, not over the "
|
|
375
380
|
"web UI -- use upload_certificate_scp",
|
|
376
381
|
)
|
|
382
|
+
if op.name in _CSRF_HTTP_WRITES and not dialect_has_csrf_hash(spec.html_dialect):
|
|
383
|
+
# These writers scrape an <input name="hash"> before posting, and this
|
|
384
|
+
# dialect's pages do not carry one -- MEASURED on gsm7252ps and
|
|
385
|
+
# gs110emx, see endpoints.dialect_has_csrf_hash. Driving them raises
|
|
386
|
+
# HttpUnexpectedPageError on real hardware, so claiming support here
|
|
387
|
+
# would publish a support table that contradicts the device.
|
|
388
|
+
return (
|
|
389
|
+
Support.UNSUPPORTED,
|
|
390
|
+
f"model {model.key!r} web UI carries no CSRF 'hash' token, which "
|
|
391
|
+
f"the HTTP {op.name} writer requires",
|
|
392
|
+
)
|
|
377
393
|
path = _http_path_for(spec, op)
|
|
378
394
|
if path is None:
|
|
379
395
|
return (
|
netgear_switch/http_write.py
CHANGED
|
@@ -268,6 +268,25 @@ def _check_goahead_upload_response(text: str) -> None:
|
|
|
268
268
|
)
|
|
269
269
|
|
|
270
270
|
|
|
271
|
+
def _require_csrf_dialect(spec: HttpModelSpec, model_key: str, op: str) -> None:
|
|
272
|
+
"""Refuse a hash-scraping write on a dialect whose pages carry no token.
|
|
273
|
+
|
|
274
|
+
This is a CAPABILITY refusal, not a surprise, so it raises
|
|
275
|
+
UnsupportedCapabilityError rather than HttpUnexpectedPageError: the writer
|
|
276
|
+
is built on the Plus form, and MEASURED probes of gsm7252ps and gs110emx
|
|
277
|
+
found no ``<input name="hash">`` anywhere in their write pages. Saying so by
|
|
278
|
+
type is what lets the facade, the capability table and the caller agree --
|
|
279
|
+
and what stops a bare "unexpected page" from reading like a transient fault.
|
|
280
|
+
"""
|
|
281
|
+
from .protocols.http.endpoints import dialect_has_csrf_hash
|
|
282
|
+
|
|
283
|
+
if not dialect_has_csrf_hash(spec.html_dialect):
|
|
284
|
+
raise UnsupportedCapabilityError(
|
|
285
|
+
f"model {model_key!r} web UI carries no CSRF 'hash' token, which "
|
|
286
|
+
f"the HTTP {op} writer requires"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
271
290
|
def _csrf(html: str) -> str:
|
|
272
291
|
token = parse.parse_csrf_hash(html)
|
|
273
292
|
if token is None:
|
|
@@ -747,6 +766,7 @@ class HttpWriter:
|
|
|
747
766
|
return shown
|
|
748
767
|
|
|
749
768
|
def create_vlan(self, vlan: int, name: str, *, force: bool = False) -> None:
|
|
769
|
+
_require_csrf_dialect(self._spec, self.model.key, "create_vlan")
|
|
750
770
|
del name, force # web UI 8021qCf.cgi has no VLAN-name field (GROUNDED).
|
|
751
771
|
path = _require_path(self.model.key, self._spec.vlan_config_path, "VLAN config")
|
|
752
772
|
page = self.session.get_page(path)
|
|
@@ -760,6 +780,7 @@ class HttpWriter:
|
|
|
760
780
|
)
|
|
761
781
|
|
|
762
782
|
def delete_vlan(self, vlan: int, *, force: bool = False) -> None:
|
|
783
|
+
_require_csrf_dialect(self._spec, self.model.key, "delete_vlan")
|
|
763
784
|
del force # VLAN delete disruptiveness is guarded per-member elsewhere.
|
|
764
785
|
path = _require_path(self.model.key, self._spec.vlan_config_path, "VLAN config")
|
|
765
786
|
page = self.session.get_page(path)
|
|
@@ -1215,6 +1236,7 @@ class AsyncHttpWriter:
|
|
|
1215
1236
|
return shown
|
|
1216
1237
|
|
|
1217
1238
|
async def create_vlan(self, vlan: int, name: str, *, force: bool = False) -> None:
|
|
1239
|
+
_require_csrf_dialect(self._spec, self.model.key, "create_vlan")
|
|
1218
1240
|
del name, force
|
|
1219
1241
|
path = _require_path(self.model.key, self._spec.vlan_config_path, "VLAN config")
|
|
1220
1242
|
page = await self.session.get_page(path)
|
|
@@ -1228,6 +1250,7 @@ class AsyncHttpWriter:
|
|
|
1228
1250
|
)
|
|
1229
1251
|
|
|
1230
1252
|
async def delete_vlan(self, vlan: int, *, force: bool = False) -> None:
|
|
1253
|
+
_require_csrf_dialect(self._spec, self.model.key, "delete_vlan")
|
|
1231
1254
|
del force
|
|
1232
1255
|
path = _require_path(self.model.key, self._spec.vlan_config_path, "VLAN config")
|
|
1233
1256
|
page = await self.session.get_page(path)
|
|
@@ -140,6 +140,27 @@ class HtmlDialect(enum.Enum):
|
|
|
140
140
|
GOAHEAD_XML = "goahead_xml"
|
|
141
141
|
|
|
142
142
|
|
|
143
|
+
def dialect_has_csrf_hash(dialect: HtmlDialect) -> bool:
|
|
144
|
+
"""Whether this dialect's pages carry an ``<input name="hash">`` CSRF token.
|
|
145
|
+
|
|
146
|
+
``http_write`` scrapes that token before every form post, so a dialect
|
|
147
|
+
without one cannot be driven by those writers at all.
|
|
148
|
+
|
|
149
|
+
MEASURED 2026-08-02, not inferred. Live probes found NO hash on any write
|
|
150
|
+
page of gsm7252ps (10.1.5.22: vlanStatus, poeInterfaceConfiguration,
|
|
151
|
+
portPvidConfiguration, vlan_port_cfg, portsConfiguration) nor of gs110emx
|
|
152
|
+
(10.1.5.25: Cf8021q, vlan_pvidsetting). Only the Plus ``.cgi`` pages have
|
|
153
|
+
it -- the surface ``HttpWriter`` was originally written against.
|
|
154
|
+
|
|
155
|
+
ONE definition, read by the capability oracle AND by the virtual switch, so
|
|
156
|
+
the mock can never emit a token the hardware lacks. That divergence is
|
|
157
|
+
exactly how HTTP ``create_vlan`` passed the entire test suite while failing
|
|
158
|
+
on all four FASTPATH switches.
|
|
159
|
+
"""
|
|
160
|
+
return dialect in {HtmlDialect.STANDARD, HtmlDialect.GS105PE}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
143
164
|
@dataclass(frozen=True)
|
|
144
165
|
class XuiMgmtIpFields:
|
|
145
166
|
"""Which fields of a FASTPATH XUI management-IP page carry what.
|
netgear_switch/virtual/web.py
CHANGED
|
@@ -39,8 +39,40 @@ def _hash_input() -> str:
|
|
|
39
39
|
return f'<input type="hidden" name="hash" value="{_HASH}">'
|
|
40
40
|
|
|
41
41
|
|
|
42
|
+
def _has_csrf_hash(spec: HttpModelSpec) -> bool:
|
|
43
|
+
"""Whether this model's real web UI carries an ``<input name="hash">``.
|
|
44
|
+
|
|
45
|
+
ONLY the Plus ``.cgi`` dialects do. MEASURED 2026-08-02: a live capture of
|
|
46
|
+
the FASTPATH ``vlanStatus.html`` on gsm7252ps (10.1.5.22, committed as
|
|
47
|
+
``tests/fixtures/http/gsm7252ps_vlan_status_live.html``) carries no ``hash``
|
|
48
|
+
input anywhere -- only ``applet_port``, ``applet_unit``, ``dbgopt`` and the
|
|
49
|
+
XUI cell hiddens.
|
|
50
|
+
|
|
51
|
+
This mock used to emit the token on EVERY page regardless of dialect, and
|
|
52
|
+
that is how ``HttpWriter.create_vlan`` -- which scrapes the token and is
|
|
53
|
+
written for the Plus form -- passed here while failing on real FASTPATH
|
|
54
|
+
hardware with "no CSRF 'hash' token on page before write". The mock and the
|
|
55
|
+
writer agreed with each other while both disagreed with the device, which
|
|
56
|
+
is exactly the failure principle 5 exists to prevent. Emitting it only
|
|
57
|
+
where the hardware does makes the mock refuse the same write the switch
|
|
58
|
+
refuses.
|
|
59
|
+
"""
|
|
60
|
+
from ..protocols.http.endpoints import dialect_has_csrf_hash
|
|
61
|
+
|
|
62
|
+
return dialect_has_csrf_hash(spec.html_dialect)
|
|
63
|
+
|
|
64
|
+
|
|
42
65
|
def render_page(
|
|
43
66
|
state: VirtualSwitchState, spec: HttpModelSpec, path: str, form: dict[str, str]
|
|
67
|
+
) -> str:
|
|
68
|
+
html = _render_page_body(state, spec, path, form)
|
|
69
|
+
if not _has_csrf_hash(spec):
|
|
70
|
+
html = html.replace(_hash_input(), "")
|
|
71
|
+
return html
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _render_page_body(
|
|
75
|
+
state: VirtualSwitchState, spec: HttpModelSpec, path: str, form: dict[str, str]
|
|
44
76
|
) -> str:
|
|
45
77
|
if path == spec.dashboard_path:
|
|
46
78
|
return _render_dashboard(state)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-netgear-switch-library
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.post355
|
|
4
4
|
Summary: Python library and CLI to query and control Netgear switches over SNMP, NSDP and HTTP.
|
|
5
5
|
Author-email: Tim Ansell <me@mith.ro>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
netgear_switch/__init__.py,sha256=3P68qR3Rcf9zl5u4_SMkmloD8MnzZi6VWv7n4AMb_-4,5835
|
|
2
2
|
netgear_switch/_dispatch.py,sha256=JO30TCPjE_1Y10SIGEkQybPT5EQB5z0LGfyT5YDQYMc,12615
|
|
3
|
-
netgear_switch/_version.py,sha256=
|
|
3
|
+
netgear_switch/_version.py,sha256=zWbegVLGRk_tDrqYbjadt8rHBhj6Vv_pjARz9Mr8w0g,534
|
|
4
4
|
netgear_switch/aio_api.py,sha256=5b7a6LPT_LkMmcRdCzyPRhiE9nte9TseOFCcmlTj-5o,29561
|
|
5
|
-
netgear_switch/capabilities.py,sha256=
|
|
5
|
+
netgear_switch/capabilities.py,sha256=ZG9QX0k0zZTelaat70Kfn_bW3UZ9Dk4Pfq49CiqzdAc,21524
|
|
6
6
|
netgear_switch/cli_read.py,sha256=EpbFLYPSA_tErwMfqHJBOjonhUVvPpS-pmlnYGQ73W0,6477
|
|
7
7
|
netgear_switch/cli_write.py,sha256=NGcitE5ZRfFSohOn38mLUcFsshofJNfI9XxcbQsd5Ss,33156
|
|
8
8
|
netgear_switch/config.py,sha256=z2vV1LqsLTb83ekEJwy_nnkeUlxcknicmkmKfILH5kc,5787
|
|
9
9
|
netgear_switch/errors.py,sha256=zb9fFU71o69NXBhH51OOe5tXVp1UigMd2U64TN5cTW0,2203
|
|
10
10
|
netgear_switch/http_read.py,sha256=LsgticMEeFdeSZgQwx16RSYc81i4wlHoe6tm_wStefg,39779
|
|
11
|
-
netgear_switch/http_write.py,sha256=
|
|
11
|
+
netgear_switch/http_write.py,sha256=dcMhpJ-qKJh-bE1fS0B06JoDeZpDhH9WXmcAH3jsqbU,62884
|
|
12
12
|
netgear_switch/models.py,sha256=5esfLwV47kM_8kyVeVAOihhS9nxB0CHj0kaRbd64388,8557
|
|
13
13
|
netgear_switch/nsdp_read.py,sha256=CXJ4FrIWvEnk1slERgJbZRD2G189vgE6t0YclrCMoH4,15467
|
|
14
14
|
netgear_switch/nsdp_write.py,sha256=NaMvrGbDqiej5gk27pS24UWY_T2WdamwK1kHX65e884,21366
|
|
@@ -31,7 +31,7 @@ netgear_switch/protocols/cli/commands.py,sha256=88Dt506mDAMUEW6fpmntjarAxnHyjVLJ
|
|
|
31
31
|
netgear_switch/protocols/cli/parse.py,sha256=dNxnzy49IsPrO5oYXWT7NLYsXZySRz0S_0IZUzwU0D8,38864
|
|
32
32
|
netgear_switch/protocols/http/__init__.py,sha256=n_iSvNTdSqeEE8U2hbk5SYSNXrWHDYzlqok89FVnK10,85
|
|
33
33
|
netgear_switch/protocols/http/crypt.py,sha256=zwFqE9ff49-j-qlvo7k-ydcG3z1l2SNJ8WLel65-w58,976
|
|
34
|
-
netgear_switch/protocols/http/endpoints.py,sha256=
|
|
34
|
+
netgear_switch/protocols/http/endpoints.py,sha256=mNvzll3pQLZC5rKUOLcf994v7LR8nKJ288MrYd9akVQ,53466
|
|
35
35
|
netgear_switch/protocols/http/forms.py,sha256=a218L58_D62pgYACtlVzGpej88dQLEfTXsQqM7K3Pww,11228
|
|
36
36
|
netgear_switch/protocols/http/parse.py,sha256=1qwGOX0l1kFxvwFWZXAFWoTnT4iM0qorIuJanof0qSk,116681
|
|
37
37
|
netgear_switch/protocols/http/session.py,sha256=kJto8197Q3_ArIr3DnwapmVPZP--zG-T17uCxxqyiHM,1766
|
|
@@ -67,7 +67,7 @@ netgear_switch/virtual/cli_fastpath.py,sha256=Mk1ER8_PRp2apT-l85Imnd8fPhQcM05RaM
|
|
|
67
67
|
netgear_switch/virtual/seed.py,sha256=xoaaQbgVQsN1lAzq7D9yPxQzRAs3B8RFOrMn-ikmOz4,96821
|
|
68
68
|
netgear_switch/virtual/server.py,sha256=L6pkdsK48nVhSOVx-TCNLQTtjTJkud-xi-EuRh5CbiQ,9335
|
|
69
69
|
netgear_switch/virtual/state.py,sha256=I0U9n3rqBQaTqwfoaKDdEb6D3Zt45s_ha6eBxBnCLdc,74611
|
|
70
|
-
netgear_switch/virtual/web.py,sha256=
|
|
70
|
+
netgear_switch/virtual/web.py,sha256=hMrDQgVP2DDQO-2ebE1z4NXSuCvZozzookRSfkUvx-k,8737
|
|
71
71
|
netgear_switch/virtual/web_fastpath_vlan.py,sha256=W5TsIrjRzt57jno-YAjXUiuXW_6IwIzTzPqs1N2O_Pw,19443
|
|
72
72
|
netgear_switch/virtual/web_fastpath_xui.py,sha256=B2K3c_CJ5dzOzCw8ELIdMiUyCj1wmw_psp7luNh70s0,11208
|
|
73
73
|
netgear_switch/virtual/web_gs105pe.py,sha256=4BJ7xEkTdnQiI5r20doFv-ErLEr5BDLMq4fmCayqMEs,7568
|
|
@@ -84,8 +84,8 @@ netgear_switch/virtual/faces/mibview.py,sha256=g4EsSfXO6ojyPDT633lEkNVmPcqzU0Vdb
|
|
|
84
84
|
netgear_switch/virtual/faces/nsdp.py,sha256=WX9w1YWArUvm4-L2Y7QI8njvBRD4t0TVFUL2V0yuQmI,10395
|
|
85
85
|
netgear_switch/virtual/faces/snmp.py,sha256=y8N3rDwO3AuHWi9Ke10WLG1yzZITpn363PunIG_ZuSk,21252
|
|
86
86
|
netgear_switch/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
87
|
-
python_netgear_switch_library-0.0.
|
|
88
|
-
python_netgear_switch_library-0.0.
|
|
89
|
-
python_netgear_switch_library-0.0.
|
|
90
|
-
python_netgear_switch_library-0.0.
|
|
91
|
-
python_netgear_switch_library-0.0.
|
|
87
|
+
python_netgear_switch_library-0.0.post355.dist-info/METADATA,sha256=TIr7LAfmhFmpnN_MfzW95BARs64CxWKGG0a60Cywk24,6682
|
|
88
|
+
python_netgear_switch_library-0.0.post355.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
89
|
+
python_netgear_switch_library-0.0.post355.dist-info/entry_points.txt,sha256=mf6HFDGVV0KIDI0dHkh0BNcCjHkSg1lhE70xTzB47WQ,96
|
|
90
|
+
python_netgear_switch_library-0.0.post355.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
91
|
+
python_netgear_switch_library-0.0.post355.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|