# 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