release: Mark VIII 1.1
Wireless client mode (connect to WiFi as client): - settings/wifi/client API: state, scan, connect, disconnect, route - Internet Connection topbar dialog and functional Settings > Networking card - routing toggle syncing UCI flag and daemon state - fix trailing-slash hash routes (View not available) - hidden-SSID filtering, encryption classification (Open/WPA2/WPA3/mixed) - tests for client state, scan parsing, connect/disconnect, routing
This commit is contained in:
@@ -1963,11 +1963,32 @@ def _proxy_json(method, path, body=None):
|
||||
return 200, (data if isinstance(data, dict) else {'ok': True})
|
||||
|
||||
|
||||
def _payload_detail(data):
|
||||
if isinstance(data, dict):
|
||||
text = data.get('error') or data.get('detail')
|
||||
if isinstance(text, str):
|
||||
return text
|
||||
return json.dumps(data)
|
||||
if isinstance(data, bytes):
|
||||
data = data.decode('utf-8', 'replace')
|
||||
if isinstance(data, str):
|
||||
try:
|
||||
parsed = json.loads(data)
|
||||
except Exception:
|
||||
return data
|
||||
if isinstance(parsed, dict):
|
||||
text = parsed.get('error') or parsed.get('detail')
|
||||
if isinstance(text, str):
|
||||
return text
|
||||
return data
|
||||
return str(data) if data is not None else ''
|
||||
|
||||
|
||||
def _payload_daemon(method, path, body=None):
|
||||
status, data = daemon_call(method, path, body=body, token=current_token(), timeout=45)
|
||||
if status != 200:
|
||||
return (502 if status == 0 else status), {
|
||||
'error': 'Pager payload service failed', 'detail': data}
|
||||
'error': 'Pager payload service failed', 'detail': _payload_detail(data)}
|
||||
if not isinstance(data, (dict, list)):
|
||||
return 502, {'error': 'Pager payload service returned an invalid response'}
|
||||
return 200, data
|
||||
@@ -2413,6 +2434,184 @@ def h_settings_management_wifi(ctx):
|
||||
}
|
||||
|
||||
|
||||
WIFI_CLIENT_ENCRYPTIONS = {
|
||||
'none': 'none', 'open': 'none',
|
||||
'wpa2': 'psk2', 'psk2': 'psk2',
|
||||
'wpa3': 'sae', 'sae': 'sae',
|
||||
'wpa2wpa3': 'sae-mixed', 'sae-mixed': 'sae-mixed'
|
||||
}
|
||||
|
||||
|
||||
def _freq_to_channel(freq):
|
||||
if not freq:
|
||||
return None
|
||||
if freq < 2484:
|
||||
return int((freq - 2412) / 5 + 1)
|
||||
if freq == 2484:
|
||||
return 14
|
||||
return int((freq - 5000) / 5)
|
||||
|
||||
|
||||
def _wifi_client_state():
|
||||
cfg = _uci_wifi_iface('wlan0cli')
|
||||
state = {
|
||||
'enabled': cfg.get('disabled', '1') != '1',
|
||||
'connected': False,
|
||||
'ssid': cfg.get('ssid') or '',
|
||||
'connected_ssid': '',
|
||||
'ip': '',
|
||||
'signal': None,
|
||||
'freq': None,
|
||||
'routed': cfg.get('routed') == '1',
|
||||
'has_password': bool(cfg.get('key'))
|
||||
}
|
||||
rc, out, err = device_run(['iw', 'dev', 'wlan0cli', 'link'], timeout=10)
|
||||
for line in out.splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith('Connected to'):
|
||||
state['connected'] = True
|
||||
elif line.startswith('SSID:'):
|
||||
state['connected_ssid'] = line.split(':', 1)[1].strip().strip('"')
|
||||
elif line.startswith('signal:'):
|
||||
try:
|
||||
state['signal'] = int(float(line.split(':', 1)[1].split()[0]))
|
||||
except (ValueError, IndexError):
|
||||
state['signal'] = None
|
||||
elif line.startswith('freq:'):
|
||||
try:
|
||||
state['freq'] = int(float(line.split(':', 1)[1].split()[0]))
|
||||
except (ValueError, IndexError):
|
||||
state['freq'] = None
|
||||
if not state['connected']:
|
||||
state['connected_ssid'] = ''
|
||||
_, addr_out, _ = device_run(['ip', '-4', 'addr', 'show', 'dev', 'wlan0cli'], timeout=10)
|
||||
for line in addr_out.splitlines():
|
||||
m = re.search(r'inet\s+(\d+\.\d+\.\d+\.\d+)', line)
|
||||
if m:
|
||||
state['ip'] = m.group(1)
|
||||
break
|
||||
return state
|
||||
|
||||
|
||||
def h_settings_wifi_client(ctx):
|
||||
return 200, _wifi_client_state()
|
||||
|
||||
|
||||
def _parse_wifi_scan(out):
|
||||
networks = []
|
||||
for block in out.split('BSS '):
|
||||
block = block.strip()
|
||||
if not block:
|
||||
continue
|
||||
m = re.match(r'([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})\(on', block)
|
||||
bss = m.group(1).upper() if m else ''
|
||||
freq = None
|
||||
m = re.search(r'freq:\s*([\d.]+)', block)
|
||||
if m:
|
||||
try:
|
||||
freq = int(float(m.group(1)))
|
||||
except ValueError:
|
||||
freq = None
|
||||
signal = None
|
||||
m = re.search(r'signal:\s*(-?\d+(?:\.\d+)?)', block)
|
||||
if m:
|
||||
try:
|
||||
signal = int(float(m.group(1)))
|
||||
except ValueError:
|
||||
signal = None
|
||||
ssid = ''
|
||||
m = re.search(r'SSID:\s*([^\n]*)', block)
|
||||
if m:
|
||||
ssid = m.group(1).strip().strip('"')
|
||||
if (not ssid or all(ord(c) < 32 or c == '\ufffd' for c in ssid)
|
||||
or re.match(r'^(\\x[0-9A-Fa-f]{2})+$', ssid)):
|
||||
ssid = ''
|
||||
if not ssid:
|
||||
continue
|
||||
auth = ''
|
||||
m = re.search(r'Authentication suites:\s*([^\n]+)', block)
|
||||
if m:
|
||||
auth = m.group(1).strip()
|
||||
if 'RSN:' in block and 'WPA:' in block:
|
||||
encryption = 'WPA/WPA2'
|
||||
elif 'RSN:' in block:
|
||||
if 'SAE' in auth and 'PSK' in auth:
|
||||
encryption = 'WPA2/WPA3'
|
||||
elif 'SAE' in auth:
|
||||
encryption = 'WPA3'
|
||||
else:
|
||||
encryption = 'WPA2'
|
||||
elif 'WPA:' in block:
|
||||
encryption = 'WPA'
|
||||
else:
|
||||
encryption = 'Open'
|
||||
networks.append({'bssid': bss, 'ssid': ssid, 'freq': freq,
|
||||
'channel': _freq_to_channel(freq), 'signal': signal,
|
||||
'encryption': encryption})
|
||||
by_ssid = {}
|
||||
for net in networks:
|
||||
key = net['ssid'] or net['bssid']
|
||||
current = by_ssid.get(key)
|
||||
if current is None or (net['signal'] or -200) > (current['signal'] or -200):
|
||||
by_ssid[key] = net
|
||||
return sorted(by_ssid.values(),
|
||||
key=lambda n: n['signal'] if n['signal'] is not None else -200,
|
||||
reverse=True)
|
||||
|
||||
|
||||
def h_settings_wifi_client_scan(ctx):
|
||||
rc, out, err = device_run(['iw', 'dev', 'wlan0', 'scan'], timeout=25)
|
||||
if rc != 0:
|
||||
return 502, {'error': err or out or 'scan failed'}
|
||||
return 200, {'networks': _parse_wifi_scan(out)}
|
||||
|
||||
|
||||
def h_settings_wifi_client_connect(ctx):
|
||||
body = ctx.body or {}
|
||||
ssid = (body.get('ssid') or '').strip()
|
||||
if not ssid:
|
||||
return 400, {'error': 'SSID is required'}
|
||||
encryption = (body.get('encryption') or 'wpa2').strip().lower()
|
||||
if encryption not in WIFI_CLIENT_ENCRYPTIONS:
|
||||
return 400, {'error': 'unsupported encryption type'}
|
||||
enc = WIFI_CLIENT_ENCRYPTIONS[encryption]
|
||||
password = body.get('password') or ''
|
||||
if enc != 'none' and len(password) < 8:
|
||||
return 400, {'error': 'password must be at least 8 characters'}
|
||||
routed = '1' if body.get('routed') else '0'
|
||||
device_run(['uci', 'set', 'wireless.wlan0cli.ssid=%s' % ssid])
|
||||
device_run(['uci', 'set', 'wireless.wlan0cli.encryption=%s' % enc])
|
||||
device_run(['uci', 'set', 'wireless.wlan0cli.disabled=0'])
|
||||
device_run(['uci', 'set', 'wireless.wlan0cli.routed=%s' % routed])
|
||||
if enc == 'none':
|
||||
device_run(['uci', 'delete', 'wireless.wlan0cli.key'])
|
||||
else:
|
||||
device_run(['uci', 'set', 'wireless.wlan0cli.key=%s' % password])
|
||||
device_run(['uci', 'commit', 'wireless'])
|
||||
daemon_sock_call('PUT', '/api/settings/wifi/set_client_route', {'routed': body.get('routed') or False})
|
||||
rc, out, err = device_run(['wifi', 'reload'], timeout=45)
|
||||
if rc != 0:
|
||||
return 502, {'error': err or out or 'wireless reload failed'}
|
||||
return 200, _wifi_client_state()
|
||||
|
||||
|
||||
def h_settings_wifi_client_disconnect(ctx):
|
||||
device_run(['uci', 'set', 'wireless.wlan0cli.disabled=1'])
|
||||
device_run(['uci', 'commit', 'wireless'])
|
||||
rc, out, err = device_run(['wifi', 'reload'], timeout=45)
|
||||
if rc != 0:
|
||||
return 502, {'error': err or out or 'wireless reload failed'}
|
||||
return 200, _wifi_client_state()
|
||||
|
||||
|
||||
def h_settings_wifi_client_route(ctx):
|
||||
routed = bool((ctx.body or {}).get('routed'))
|
||||
device_run(['uci', 'set', 'wireless.wlan0cli.routed=%d' % (1 if routed else 0)])
|
||||
device_run(['uci', 'commit', 'wireless'])
|
||||
daemon_sock_call('PUT', '/api/settings/wifi/set_client_route', {'routed': routed})
|
||||
return 200, _wifi_client_state()
|
||||
|
||||
|
||||
PAGER_LED_COLORS = ('red', 'green', 'blue', 'yellow', 'cyan', 'magenta', 'white')
|
||||
|
||||
|
||||
@@ -2565,6 +2764,11 @@ ROUTER.add('GET', r'/api/settings/usb', h_settings_usb)
|
||||
ROUTER.add('GET', r'/api/settings/network', h_settings_network)
|
||||
ROUTER.add('GET', r'/api/settings/wifi/management', h_settings_management_wifi)
|
||||
ROUTER.add('POST', r'/api/settings/wifi/management', h_settings_management_wifi)
|
||||
ROUTER.add('GET', r'/api/settings/wifi/client', h_settings_wifi_client)
|
||||
ROUTER.add('POST', r'/api/settings/wifi/client/scan', h_settings_wifi_client_scan)
|
||||
ROUTER.add('POST', r'/api/settings/wifi/client/connect', h_settings_wifi_client_connect)
|
||||
ROUTER.add('POST', r'/api/settings/wifi/client/disconnect', h_settings_wifi_client_disconnect)
|
||||
ROUTER.add('POST', r'/api/settings/wifi/client/route', h_settings_wifi_client_route)
|
||||
ROUTER.add('GET', r'/api/settings/hardware', h_settings_hardware)
|
||||
ROUTER.add('POST', r'/api/settings/hardware', h_settings_hardware)
|
||||
ROUTER.add('GET', r'/api/settings/advanced', h_settings_advanced)
|
||||
|
||||
Reference in New Issue
Block a user