You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
72 lines
3.1 KiB
72 lines
3.1 KiB
|
1 month ago
|
# src/network/ap_validator.py
|
||
|
|
"""AP hard-reject 규칙 (spec §5.1/§6/§7). 사용자 표시 메시지는 영어(웹 UI 일관성)."""
|
||
|
|
import ipaddress
|
||
|
|
|
||
|
|
AP_SSID_MAX = 32 # hostapd SSID 한계 (byte). STA(19, dpworldapp 버퍼)와 다름.
|
||
|
|
PSK_MIN, PSK_MAX = 8, 63 # WPA2-PSK passphrase (byte)
|
||
|
|
FORBIDDEN = ('"', "\\", "#") # '#' starts an inline comment in hostapd conf
|
||
|
|
AP_BANDS = frozenset({"auto", "2g", "5g"})
|
||
|
|
CHANNELS_2G = frozenset(range(1, 14))
|
||
|
|
CHANNELS_5G = frozenset({36, 40, 44, 48, 149, 153, 157, 161, 165})
|
||
|
|
|
||
|
|
def _blen(s):
|
||
|
|
return len(s.encode("utf-8"))
|
||
|
|
|
||
|
|
def _ip(s):
|
||
|
|
try:
|
||
|
|
return ipaddress.IPv4Address(s)
|
||
|
|
except (ValueError, TypeError):
|
||
|
|
return None
|
||
|
|
|
||
|
|
def validate_ap(intent, live_country, country_pending):
|
||
|
|
e = []
|
||
|
|
if not intent.get("ap_enabled"):
|
||
|
|
return e # 비활성이면 필드 검증 생략
|
||
|
|
ssid = str(intent.get("ap_ssid", ""))
|
||
|
|
if not ssid:
|
||
|
|
e.append("SSID is required.")
|
||
|
|
elif _blen(ssid) > AP_SSID_MAX:
|
||
|
|
e.append(f"SSID must be at most {AP_SSID_MAX} bytes.")
|
||
|
|
if any(c in ssid for c in FORBIDDEN):
|
||
|
|
e.append('SSID cannot contain ", \\, or #.')
|
||
|
|
psk = str(intent.get("ap_passphrase", ""))
|
||
|
|
if not (PSK_MIN <= _blen(psk) <= PSK_MAX):
|
||
|
|
e.append(f"Password must be {PSK_MIN}-{PSK_MAX} bytes.")
|
||
|
|
if any(c in psk for c in FORBIDDEN): # '#' truncates the hostapd WPA key
|
||
|
|
e.append('Password cannot contain ", \\, or #.')
|
||
|
|
if any(ord(c) < 0x20 for c in (ssid + psk)): # control chars (e.g. newline) → block hostapd conf injection
|
||
|
|
e.append("SSID/password cannot contain control characters (e.g. newline).")
|
||
|
|
band = str(intent.get("ap_band", "auto")).lower()
|
||
|
|
channel = intent.get("ap_channel", 0)
|
||
|
|
if band not in AP_BANDS:
|
||
|
|
e.append("AP band must be auto, 2g, or 5g.")
|
||
|
|
elif channel not in (0, None):
|
||
|
|
if band == "auto":
|
||
|
|
e.append("AP channel must be auto when AP band is auto.")
|
||
|
|
elif band == "2g" and channel not in CHANNELS_2G:
|
||
|
|
e.append("2.4 GHz AP channel is invalid.")
|
||
|
|
elif band == "5g" and channel not in CHANNELS_5G:
|
||
|
|
e.append("5 GHz AP channel is invalid.")
|
||
|
|
ap_ip = _ip(intent.get("ap_ip", ""))
|
||
|
|
s = _ip(intent.get("dhcp_start", "")); d = _ip(intent.get("dhcp_end", ""))
|
||
|
|
if not ap_ip:
|
||
|
|
e.append("AP IP address is invalid.")
|
||
|
|
if not s:
|
||
|
|
e.append("DHCP start address is invalid.")
|
||
|
|
if not d:
|
||
|
|
e.append("DHCP end address is invalid.")
|
||
|
|
if ap_ip and s and d:
|
||
|
|
net = ipaddress.IPv4Network(f"{ap_ip}/24", strict=False)
|
||
|
|
if s not in net or d not in net:
|
||
|
|
e.append("DHCP range is outside the AP subnet (/24).")
|
||
|
|
if int(s) > int(d):
|
||
|
|
e.append("DHCP start is greater than DHCP end.")
|
||
|
|
if s <= ap_ip <= d:
|
||
|
|
e.append("DHCP range overlaps the AP IP.")
|
||
|
|
lc = str(live_country or "").upper()
|
||
|
|
if country_pending:
|
||
|
|
e.append("Reboot required to apply the country code before enabling the AP.")
|
||
|
|
elif not (len(lc) == 2 and lc.isalpha()):
|
||
|
|
e.append("Cannot enable AP: no valid country regdomain.")
|
||
|
|
return e
|