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.
 
 
 
 
 
 

71 lines
3.1 KiB

# 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