From 10a512ec962b1df692ba617b8df076844e816eba Mon Sep 17 00:00:00 2001 From: wenjie Date: Thu, 12 Mar 2026 15:21:18 +0800 Subject: [PATCH] feat: Implement AP password authentication for the WiFi configuration page --- server/router/network.go | 3 +- server/service/network/wifi.go | 41 +++++++++++++++- web/src/api/network.ts | 21 +++++++- web/src/i18n/locales/en.ts | 9 +++- web/src/i18n/locales/zh.ts | 9 +++- web/src/lib/http.ts | 5 +- web/src/pages/wifi/index.tsx | 88 ++++++++++++++++++++++++++++++++-- web/src/router.tsx | 1 + 8 files changed, 166 insertions(+), 11 deletions(-) diff --git a/server/router/network.go b/server/router/network.go index af4e722..45937ec 100644 --- a/server/router/network.go +++ b/server/router/network.go @@ -10,7 +10,8 @@ import ( func networkRouter(r *gin.Engine) { service := network.NewService() - r.POST("/api/network/wifi", service.ConnectWifiNoAuth) // connect Wi-Fi without auth (only available in ap mode) + r.POST("/api/network/wifi", service.ConnectWifiNoAuth) // connect Wi-Fi without auth (only available in ap mode) + r.POST("/api/network/wifi/verify", service.VerifyApLogin) // verify ap login api := r.Group("/api").Use(middleware.CheckToken()) diff --git a/server/service/network/wifi.go b/server/service/network/wifi.go index f3eb973..d163d34 100644 --- a/server/service/network/wifi.go +++ b/server/service/network/wifi.go @@ -1,6 +1,7 @@ package network import ( + "crypto/subtle" "fmt" "os" "os/exec" @@ -21,6 +22,7 @@ const ( WiFiConnect = "/kvmapp/kvm/wifi_try_connect" WiFiStateFile = "/kvmapp/kvm/wifi_state" WiFiScript = "/etc/init.d/S30wifi" + WiFiApPassFile = "/kvmapp/kvm/ap.pass" ) func (s *Service) GetWifi(c *gin.Context) { @@ -62,6 +64,15 @@ func (s *Service) ConnectWifiNoAuth(c *gin.Context) { return } + // Verify AP Password + apKey := c.GetHeader("X-AP-Key") + expectedPass := getApPassword() + if apKey == "" || expectedPass == "" || subtle.ConstantTimeCompare([]byte(apKey), []byte(expectedPass)) != 1 { + time.Sleep(2 * time.Second) + rsp.ErrRsp(c, -4, "unauthorized") + return + } + if err := proto.ParseFormRequest(c, &req); err != nil { time.Sleep(1 * time.Second) rsp.ErrRsp(c, -2, "invalid parameters") @@ -79,6 +90,26 @@ func (s *Service) ConnectWifiNoAuth(c *gin.Context) { log.Debugf("set wifi ap mode successfully") } +func (s *Service) VerifyApLogin(c *gin.Context) { + var rsp proto.Response + + if !isSupported() || !isAPMode() { + time.Sleep(2 * time.Second) + rsp.ErrRsp(c, -1, "invalid mode") + return + } + + apKey := c.GetHeader("X-AP-Key") + expectedPass := getApPassword() + if apKey == "" || expectedPass == "" || subtle.ConstantTimeCompare([]byte(apKey), []byte(expectedPass)) != 1 { + time.Sleep(2 * time.Second) + rsp.ErrRsp(c, -4, "unauthorized") + return + } + + rsp.OkRsp(c) +} + func (s *Service) ConnectWifi(c *gin.Context) { var req proto.ConnectWifiReq var rsp proto.Response @@ -111,7 +142,6 @@ func (s *Service) ConnectWifi(c *gin.Context) { } rsp.ErrRsp(c, -3, "failed to get wifi status") - return } func (s *Service) DisconnectWifi(c *gin.Context) { @@ -182,3 +212,12 @@ func getWiFiSsid() string { return strings.ReplaceAll(string(ssidByte), "\n", "") } + +func getApPassword() string { + passByte, err := os.ReadFile(WiFiApPassFile) + if err != nil { + return "" + } + + return strings.ReplaceAll(string(passByte), "\n", "") +} diff --git a/web/src/api/network.ts b/web/src/api/network.ts index 4044274..3174652 100644 --- a/web/src/api/network.ts +++ b/web/src/api/network.ts @@ -31,12 +31,29 @@ export function getWiFi() { } // connect wifi without auth (only available in wifi configuration mode) -export function connectWifiNoAuth(ssid: string, password: string) { +export function connectWifiNoAuth(ssid: string, password: string, apPassword?: string) { const data = { ssid, password }; - return http.post('/api/network/wifi', data); + return http.post('/api/network/wifi', data, { + headers: { + 'X-AP-Key': apPassword || '' + } + }); +} + +// verify ap login +export function verifyApLogin(apPassword: string) { + return http.post( + '/api/network/wifi/verify', + {}, + { + headers: { + 'X-AP-Key': apPassword || '' + } + } + ); } // connect wifi diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index 2969f36..8bc71f3 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -48,7 +48,14 @@ const en = { invalidMode: 'The current mode does not support network setup. Please go to your device and enable Wi-Fi configuration mode.', confirmBtn: 'Ok', - finishBtn: 'Finished' + finishBtn: 'Finished', + ap: { + authTitle: 'Authentication Required', + authDescription: 'Please enter the AP password to continue', + authFailed: 'Invalid AP password', + passPlaceholder: 'AP password', + verifyBtn: 'Verify', + } }, screen: { scale: 'Scale', diff --git a/web/src/i18n/locales/zh.ts b/web/src/i18n/locales/zh.ts index 13eed26..27f86c5 100644 --- a/web/src/i18n/locales/zh.ts +++ b/web/src/i18n/locales/zh.ts @@ -46,7 +46,14 @@ const zh = { failed: '操作失败,请重试。', invalidMode: '当前模式不支持配置网络。请先前往设备启用 Wi-Fi 配置模式。', confirmBtn: '确定', - finishBtn: '完成' + finishBtn: '完成', + ap: { + authTitle: '身份验证', + authDescription: '请输入 AP 密码以继续', + authFailed: '密码错误', + passPlaceholder: 'AP 密码', + verifyBtn: '验证', + } }, screen: { scale: '缩放', diff --git a/web/src/lib/http.ts b/web/src/lib/http.ts index e7d9049..38802d9 100644 --- a/web/src/lib/http.ts +++ b/web/src/lib/http.ts @@ -58,11 +58,12 @@ class Http { }); } - public post(url: string, data?: any): Promise { + public post(url: string, data?: any, config?: AxiosRequestConfig): Promise { return this.instance.request({ method: 'post', url, - data + data, + ...config }); } diff --git a/web/src/pages/wifi/index.tsx b/web/src/pages/wifi/index.tsx index 3a7840c..f131992 100644 --- a/web/src/pages/wifi/index.tsx +++ b/web/src/pages/wifi/index.tsx @@ -1,17 +1,56 @@ -import { useState } from 'react'; -import { CheckOutlined, LockOutlined, WifiOutlined } from '@ant-design/icons'; +import { useEffect, useState } from 'react'; +import { CheckOutlined, KeyOutlined, LockOutlined, WifiOutlined } from '@ant-design/icons'; import { Button, Form, Input } from 'antd'; import { useTranslation } from 'react-i18next'; +import { useSearchParams } from 'react-router-dom'; import * as api from '@/api/network.ts'; import { Head } from '@/components/head.tsx'; type State = '' | 'loading' | 'success' | 'failed' | 'denied'; +type VerifyState = '' | 'failed' | 'denied'; export const Wifi = () => { const { t } = useTranslation(); + const [searchParams] = useSearchParams(); const [state, setState] = useState(''); + const [apPassword, setApPassword] = useState(''); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [verifying, setVerifying] = useState(false); + const [verifyState, setVerifyState] = useState(''); + + useEffect(() => { + const pass = searchParams.get('p') || searchParams.get('P'); + if (pass) { + verifyPassword(pass); + } + }, []); + + async function verifyPassword(password: string) { + if (verifying) return; + setVerifying(true); + setVerifyState(''); + + try { + const rsp = await api.verifyApLogin(password); + if (rsp?.code === 0) { + setApPassword(password); + setIsAuthenticated(true); + } else { + setVerifyState(rsp?.code === -1 ? 'denied' : 'failed'); + } + } catch (err) { + console.error(err); + setVerifyState('failed'); + } + setVerifying(false); + } + + async function onVerifyFinish(values: any) { + if (!values.apPassword) return; + await verifyPassword(values.apPassword); + } async function connect(values: any) { if (!values.ssid || !values.password) return; @@ -20,13 +59,14 @@ export const Wifi = () => { setState('loading'); try { - const rsp = await api.connectWifiNoAuth(values.ssid, values.password); + const rsp = await api.connectWifiNoAuth(values.ssid, values.password, apPassword); switch (rsp?.code) { case 0: setState('success'); return; case -1: + case -4: setState('denied'); return; case -2: @@ -41,6 +81,48 @@ export const Wifi = () => { setState('success'); } + if (!isAuthenticated) { + return ( + <> + + +
+
+
+ + {t('wifi.ap.authTitle')} + + {t('wifi.ap.authDescription')} +
+ + + } placeholder={t('wifi.ap.passPlaceholder')} /> + + + + + +
+ +
+ {verifyState === 'failed' && ( + {t('wifi.ap.authFailed')} + )} + {verifyState === 'denied' && ( + {t('wifi.invalidMode')} + )} +
+
+ + ); + } + return ( <> diff --git a/web/src/router.tsx b/web/src/router.tsx index a7135e5..a36709b 100644 --- a/web/src/router.tsx +++ b/web/src/router.tsx @@ -44,6 +44,7 @@ export const router = createHashRouter([ }, { path: '/wifi', + caseSensitive: false, lazy: async () => { const { Wifi } = await import('./pages/wifi'); return { Component: Wifi };