Source code for netgear_switch.virtual.web_gs105pe
"""GS105PE web-UI page renderers driven by ``VirtualSwitchState``.
Structurally faithful to the REAL captured pages (see
``tests/fixtures/http/gs105pe_*.html``) -- same row markers, same column
order, and critically the same two quirks real firmware has, so the mock
exercises exactly the code paths real hardware does:
- ``portStatistics.cgi`` leaves the first counter's ``<td>`` EMPTY and carries
every counter as a hidden ``(hi, lo)`` 32-bit pair (see
``protocols/http/parse.py``'s ``parse_gs105pe_stats``), and writes its rows as
``<tr class="portID" name="portID">`` -- an extra attribute the other pages
lack.
- ``8021qMembe.cgi`` carries a per-page CSRF ``hash`` and marks the currently
selected VLAN with ``<option ... selected>``; the reader reuses that page for
the selected VLAN rather than re-POSTing it (which makes real hardware drop
the connection).
Byte-for-byte fidelity to the capture is deliberately NOT attempted here (that
is what the fixture-driven parser tests in ``tests/test_http_read.py`` prove);
this face's job is to serve the SAME STATE the NSDP face serves, so the
HTTP<->NSDP cross-verification is meaningful.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .state import VirtualSwitchState
# The mock's fixed CSRF token. Real firmware regenerates this per page load;
# the reader only has to scrape it and echo it back, so a constant is enough
# to exercise that round trip.
VIRTUAL_CSRF_HASH = "18007"
def _speed_text(mbps: int) -> str:
"""State speed (Mbps) -> status.cgi speed text. Inverse of
``parse._speed_text_to_mbps`` so a state -> page -> reader round trip
reproduces the state value exactly."""
if mbps >= 1000 and mbps % 1000 == 0:
return f"{mbps // 1000}G"
return f"{mbps}M"
[docs]
def render_status(state: VirtualSwitchState) -> str:
"""GET /status.cgi -- per-port link + speed, the columns
``parse_gs105pe_port_status`` reads ([1]=port, [2]=link, [4]=speed)."""
rows = "".join(
'<tr class="portID">\n'
'<td class="def firstCol def_center"><input type="checkbox"></td>\n'
f'<td class="def" sel="text">{port}</td>\n'
f'<td class="def" sel="text">{"Up" if sim.link else "Down"}</td>\n'
'<td class="def" sel="text">Auto</td>\n'
f'<td class="def" sel="text">'
f"{_speed_text(sim.speed) if sim.link else 'No Speed'}</td>\n"
'<td class="def" sel="text">Disable</td>\n'
'<td class="def" sel="text">16349</td>\n'
for port, sim in sorted(state.ports.items())
)
return (
"<html><body><form method=post action=status.cgi>"
'<table id="tbl1">\n' + rows + "</table></form></body></html>\n"
)
[docs]
def render_port_statistics(state: VirtualSwitchState) -> str:
"""GET /portStatistics.cgi -- reproduces the real page's quirks: rows are
``<tr class="portID" name="portID">``, the Bytes-Received cell is rendered
EMPTY, and each counter is a hidden ``(hi, lo)`` 32-bit pair."""
def halves(value: int) -> str:
return (
f'<input type="hidden" value="{value >> 32}">\n'
f'<input type="hidden" value="{value & 0xFFFFFFFF}">\n'
)
rows = ""
for port, sim in sorted(state.ports.items()):
rx, tx, crc = sim.rx_octets or 0, sim.tx_octets or 0, sim.rx_errors or 0
rows += (
'<tr class="portID" name="portID">\n'
f'<td class="def firstCol" sel="text">{port}</td>\n'
'<td class="def" sel="text">\n</td>\n'
+ halves(rx)
+ f'<td class="def" sel="text">{tx}\n</td>\n'
+ halves(tx)
+ f'<td class="def" sel="text">{crc}\n</td>\n'
+ halves(crc)
)
return (
"<html><body><form method=post action=portStatistics.cgi>"
'<table id="tbl1">\n' + rows + "</table></form></body></html>\n"
)
[docs]
def render_pvid(state: VirtualSwitchState) -> str:
"""GET /portPVID.cgi -- [1]=port, [2]=PVID."""
rows = "".join(
'<tr class="portID">\n'
'<td class="def firstCol def_center"><input type="checkbox"></td>\n'
f'<td class="def" sel="text">{port}</td>\n'
f'<td class="def" sel="input">{pvid}</td>\n'
for port, pvid in sorted(state.pvids.items())
)
return (
"<html><body><form method=post action=portPVID.cgi>"
'<table id="tbl1">\n' + rows + "</table></form></body></html>\n"
)
[docs]
def render_vlan_config(state: VirtualSwitchState) -> str:
"""GET /8021qCf.cgi -- the VLAN list as ``vlanckN`` checkboxes (the shape
gs305ep's ``parse_vlan_ids`` reads, which gs105pe shares)."""
boxes = "".join(
f'<input type="checkbox" name="vlanck{i}" value="{vid}">\n'
for i, vid in enumerate(sorted(state.vlans), start=1)
)
return (
"<html><body><form method=post action=8021qCf.cgi>"
f'<input type="hidden" name="hash" value="{VIRTUAL_CSRF_HASH}">\n'
+ boxes
+ "</form></body></html>\n"
)
[docs]
def render_vlan_membership(state: VirtualSwitchState, selected_vid: int) -> str:
"""GET/POST /8021qMembe.cgi -- the per-port ``hiddenMem`` wire codes
(1=untagged, 2=tagged, 3=excluded) for ``selected_vid``, plus the CSRF
``hash`` and a ``<option ... selected>`` marking which VLAN is shown."""
from ..registry import get_model
port_count = get_model(state.model_key).port_count
vsim = state.vlans.get(selected_vid)
codes = ""
for port in range(1, port_count + 1):
if vsim is not None and port in vsim.untagged:
codes += "1"
elif vsim is not None and port in vsim.member:
codes += "2"
else:
codes += "3"
options = "".join(
f'<option value="{vid}"{" selected" if vid == selected_vid else ""}>'
f"{vid}</option>\n"
for vid in sorted(state.vlans)
)
return (
"<html><body><form method=post action=8021qMembe.cgi>"
f'<input type="hidden" name="hash" value="{VIRTUAL_CSRF_HASH}">\n'
f'<select name="VLAN_ID" id="vlanIdOption">\n{options}</select>\n'
f'<input name="hiddenMem" id="hiddenMem" value="{codes}" type="hidden">\n'
"</form></body></html>\n"
)
[docs]
def render_switch_info(state: VirtualSwitchState) -> str:
"""GET /switch_info.cgi -- device identity + mgmt-IP, in the labelled-cell
and lowercase-input shape ``parse_gs105pe_sysinfo`` reads."""
mac = ":".join(f"{b:02X}" for b in state.nsdp_mac)
dhcp_selected = state.mgmt.mode == "dhcp"
return (
"<html><body><form method=post action=switch_info.cgi>"
"<table>\n"
f"<tr><td>Product Name</td><td>{state.model_name or 'GS105PE'}</td></tr>\n"
f"<tr><td>Serial Number</td><td>{state.serial}</td></tr>\n"
f"<tr><td>MAC Address</td><td>{mac}</td></tr>\n"
f"<tr><td>Firmware Version</td><td>{state.firmware}</td></tr>\n"
"</table>\n"
f'<input type="text" name="switch_name" value="{state.hostname}">\n'
'<select name="dhcpMode" id="dhcpMode">\n'
f'<option value="0"{"" if dhcp_selected else " selected"}>Disable</option>\n'
f'<option value="1"{" selected" if dhcp_selected else ""}>Enable</option>\n'
"</select>\n"
f'<input type="text" name="ip_address" value="{state.mgmt.address}">\n'
f'<input type="text" name="subnet_mask" value="{state.mgmt.netmask}">\n'
f'<input type="text" name="gateway_address" value="{state.mgmt.gateway}">\n'
"</form></body></html>\n"
)