#!/usr/bin/env python3
"""Speed up captive portal CNA on both live routers. Backup first. No secrets in output."""
from __future__ import annotations
import io
import re
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import paramiko
FONT_RE = re.compile(
r'[ \t]*]+(?:fonts\.googleapis\.com|fonts\.gstatic\.com)[^>]*>\s*',
re.I,
)
FAST_MARK = "wo-cna-fast"
FAST_CSS = (
'"
)
HUB_RSC = r"""
:put "HUB-CNA-FAST START"
/system backup save name=wo-before-cna-fast
/ip hotspot profile set [find name=hs-otantik] dns-name="" login-by=cookie,http-chap,http-pap
:if ([:len [/ipv6 nd find where interface=bridge-hotspot]] = 0) do={
:do { /ipv6 nd add interface=bridge-hotspot ra-lifetime=none advertise-dns=no advertise-mac-address=no comment=wo-hs-no-ra } on-error={
:do { /ipv6 nd add interface=bridge-hotspot ra-lifetime=0s advertise-dns=no comment=wo-hs-no-ra } on-error={}
}
} else={
:do { /ipv6 nd set [find interface=bridge-hotspot] ra-lifetime=none advertise-dns=no advertise-mac-address=no } on-error={
:do { /ipv6 nd set [find interface=bridge-hotspot] ra-lifetime=0s advertise-dns=no } on-error={}
}
}
:if ([:len [/ipv6 firewall filter find comment="wo-hs-no-ra"]] = 0) do={
:do { /ipv6 firewall filter add chain=output out-interface=bridge-hotspot protocol=icmpv6 icmp-options=134:0-255 action=drop comment=wo-hs-no-ra } on-error={}
}
:if ([:len [/ipv6 firewall filter find comment="wo-hs-no-v6-fwd"]] = 0) do={
:do { /ipv6 firewall filter add chain=forward in-interface=bridge-hotspot action=drop comment=wo-hs-no-v6-fwd } on-error={}
}
:do { /ip hotspot walled-garden remove [find dst-host="*.gstatic.com"] } on-error={}
:do { /ip hotspot walled-garden remove [find dst-host="www.gstatic.com"] } on-error={}
:put "HUB-CNA-FAST DONE"
/ip hotspot profile print where name=hs-otantik
/ipv6 nd print
/ip hotspot walled-garden print where dst-host~"gstatic"
"""
NKO_RSC = r"""
:put "NKO-CNA-FAST START"
/system backup save name=wo-before-cna-fast
/ip hotspot profile set [find name=hsprof1] dns-name="" login-by=cookie,http-chap,http-pap html-directory=hotspot
:do { /ip hotspot profile set [find name=hs-otantik] dns-name="" login-by=cookie,http-chap,http-pap } on-error={}
:if ([:len [/ipv6 nd find where interface=hs-bridge]] = 0) do={
:do { /ipv6 nd add interface=hs-bridge ra-lifetime=none advertise-dns=no advertise-mac-address=no comment=wo-hs-no-ra } on-error={
:do { /ipv6 nd add interface=hs-bridge ra-lifetime=0s advertise-dns=no comment=wo-hs-no-ra } on-error={}
}
} else={
:do { /ipv6 nd set [find interface=hs-bridge] ra-lifetime=none advertise-dns=no advertise-mac-address=no } on-error={
:do { /ipv6 nd set [find interface=hs-bridge] ra-lifetime=0s advertise-dns=no } on-error={}
}
}
:if ([:len [/ipv6 firewall filter find comment="wo-hs-no-ra"]] = 0) do={
:do { /ipv6 firewall filter add chain=output out-interface=hs-bridge protocol=icmpv6 icmp-options=134:0-255 action=drop comment=wo-hs-no-ra } on-error={}
}
:if ([:len [/ipv6 firewall filter find comment="wo-hs-no-v6-fwd"]] = 0) do={
:do { /ipv6 firewall filter add chain=forward in-interface=hs-bridge action=drop comment=wo-hs-no-v6-fwd } on-error={}
}
:do { /ip hotspot walled-garden remove [find dst-host="*.gstatic.com"] } on-error={}
:do { /ip hotspot walled-garden remove [find dst-host="www.gstatic.com"] } on-error={}
:put "NKO-CNA-FAST DONE"
/ip hotspot profile print where name=hsprof1
/ipv6 nd print
/ip hotspot walled-garden print where dst-host~"gstatic"
"""
def patch_html(raw: str) -> str:
html = FONT_RE.sub("", raw)
if FAST_MARK not in html:
if re.search(r"", html, re.I):
html = re.sub(r"", FAST_CSS + "", html, count=1, flags=re.I)
else:
html = FAST_CSS + html
return html
def sftp_patch_html(sftp: paramiko.SFTPClient, paths: list[str]) -> None:
for remote in paths:
try:
with sftp.file(remote, "r") as f:
raw = f.read()
if isinstance(raw, bytes):
text = raw.decode("utf-8", "replace")
else:
text = str(raw)
except Exception as e:
print("SKIP", remote, type(e).__name__)
continue
new = patch_html(text)
if new == text:
print("UNCHANGED", remote, len(text))
continue
bio = io.BytesIO(new.encode("utf-8"))
sftp.putfo(bio, remote)
print("PATCHED", remote, len(text), "->", len(new))
def apply_hub() -> None:
from mk_hub_exec import connect, import_rsc
print("==== HUB BACKUP+RSC ====")
print(import_rsc("wo-cna-fast.rsc", HUB_RSC, timeout=120))
mk = connect()
try:
sftp = mk.open_sftp()
sftp_patch_html(
sftp,
[
"hotspot/login.html",
"hotspot/alogin.html",
"hotspot/status.html",
"hotspot/ticket.html",
"hotspot/pay.html",
"hotspot/code.html",
"hotspot/error.html",
"hotspot/logout.html",
"hotspot/apropos.html",
"hotspot/services.html",
"hotspot/chat.html",
],
)
sftp.close()
finally:
mk.close()
from mk_hub_exec import run
print("==== HUB VERIFY ====")
print(
run(
r"""
:put [/ip hotspot profile get [find name=hs-otantik] dns-name]
:put [/ip hotspot profile get [find name=hs-otantik] login-by]
:put [/ip hotspot profile get [find name=hs-otantik] html-directory]
:put [/ip hotspot get [find name=hotspot-otantik] ip-of-dns-name]
/ipv6 nd print where comment=wo-hs-no-ra or interface=bridge-hotspot
:put ("gstatic-wg=" . [:len [/ip hotspot walled-garden find dst-host~"gstatic"]])
""",
timeout=40,
)
)
def apply_nko() -> None:
from mk_rb4011_exec import JUMP, MK
print("==== NKO BACKUP+RSC ====")
jump = paramiko.SSHClient()
jump.set_missing_host_key_policy(paramiko.AutoAddPolicy())
jump.connect(JUMP[0], username=JUMP[1], password=JUMP[2], timeout=25, allow_agent=False, look_for_keys=False)
chan = jump.get_transport().open_channel("direct-tcpip", (MK[0], 22), ("127.0.0.1", 0))
mk = paramiko.SSHClient()
mk.set_missing_host_key_policy(paramiko.AutoAddPolicy())
mk.connect(MK[0], username=MK[1], password=MK[2], sock=chan, timeout=25, allow_agent=False, look_for_keys=False)
try:
sftp = mk.open_sftp()
with sftp.file("wo-cna-fast.rsc", "w") as f:
f.write(NKO_RSC)
stdin, stdout, stderr = mk.exec_command("/import file-name=wo-cna-fast.rsc", timeout=120)
print(stdout.read().decode("utf-8", "replace"))
err = stderr.read().decode("utf-8", "replace")
if err.strip():
print("STDERR", err)
sftp_patch_html(
sftp,
[
"hotspot/login.html",
"hotspot/alogin.html",
"hotspot/status.html",
"hotspot/ticket.html",
"hotspot/pay.html",
"hotspot/code.html",
"hotspot/error.html",
"hotspot/logout.html",
"hotspot/apropos.html",
"hotspot/services.html",
"hotspot/chat.html",
"portal/login.html",
"portal/alogin.html",
"portal/status.html",
"portal/ticket.html",
"portal/pay.html",
],
)
sftp.close()
stdin, stdout, stderr = mk.exec_command(
r"""
:put [/ip hotspot profile get [find name=hsprof1] dns-name]
:put [/ip hotspot profile get [find name=hsprof1] login-by]
:put [/ip hotspot profile get [find name=hsprof1] html-directory]
:put [/ip hotspot get [find name=hotspot1] ip-of-dns-name]
/ipv6 nd print where comment=wo-hs-no-ra or interface=hs-bridge
:put ("gstatic-wg=" . [:len [/ip hotspot walled-garden find dst-host~"gstatic"]])
""",
timeout=40,
)
print("==== NKO VERIFY ====")
print(stdout.read().decode("utf-8", "replace"))
err = stderr.read().decode("utf-8", "replace")
if err.strip():
print("STDERR", err)
finally:
mk.close()
jump.close()
if __name__ == "__main__":
which = sys.argv[1] if len(sys.argv) > 1 else "both"
if which in ("hub", "both"):
apply_hub()
if which in ("nko", "both"):
apply_nko()