mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-09-11 00:22:56 -05:00
feat: Implement AP password authentication for the WiFi configuration page
This commit is contained in:
@@ -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())
|
||||
|
||||
|
||||
@@ -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", "")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -46,7 +46,14 @@ const zh = {
|
||||
failed: '操作失败,请重试。',
|
||||
invalidMode: '当前模式不支持配置网络。请先前往设备启用 Wi-Fi 配置模式。',
|
||||
confirmBtn: '确定',
|
||||
finishBtn: '完成'
|
||||
finishBtn: '完成',
|
||||
ap: {
|
||||
authTitle: '身份验证',
|
||||
authDescription: '请输入 AP 密码以继续',
|
||||
authFailed: '密码错误',
|
||||
passPlaceholder: 'AP 密码',
|
||||
verifyBtn: '验证',
|
||||
}
|
||||
},
|
||||
screen: {
|
||||
scale: '缩放',
|
||||
|
||||
@@ -58,11 +58,12 @@ class Http {
|
||||
});
|
||||
}
|
||||
|
||||
public post(url: string, data?: any): Promise<Response> {
|
||||
public post(url: string, data?: any, config?: AxiosRequestConfig): Promise<Response> {
|
||||
return this.instance.request({
|
||||
method: 'post',
|
||||
url,
|
||||
data
|
||||
data,
|
||||
...config
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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<State>('');
|
||||
const [apPassword, setApPassword] = useState<string>('');
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||
const [verifying, setVerifying] = useState<boolean>(false);
|
||||
const [verifyState, setVerifyState] = useState<VerifyState>('');
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Head title={t('head.wifi')} />
|
||||
|
||||
<div className="flex h-screen w-screen flex-col items-center justify-center">
|
||||
<Form
|
||||
style={{ minWidth: 300, maxWidth: 500 }}
|
||||
initialValues={{ remember: true }}
|
||||
onFinish={onVerifyFinish}
|
||||
>
|
||||
<div className="flex flex-col space-y-1 pb-5">
|
||||
<span className="text-center text-2xl font-semibold text-red-500">
|
||||
{t('wifi.ap.authTitle')}
|
||||
</span>
|
||||
<span className="text-center text-neutral-400">{t('wifi.ap.authDescription')}</span>
|
||||
</div>
|
||||
|
||||
<Form.Item name="apPassword">
|
||||
<Input.Password prefix={<KeyOutlined />} placeholder={t('wifi.ap.passPlaceholder')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button className="w-full" htmlType="submit" type="primary" loading={verifying}>
|
||||
{t('wifi.ap.verifyBtn')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<div className="flex max-w-[500px] justify-center px-5 pt-3 md:px-10">
|
||||
{verifyState === 'failed' && (
|
||||
<span className="text-sm text-red-500">{t('wifi.ap.authFailed')}</span>
|
||||
)}
|
||||
{verifyState === 'denied' && (
|
||||
<span className="text-sm text-red-500">{t('wifi.invalidMode')}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title={t('head.wifi')} />
|
||||
|
||||
@@ -44,6 +44,7 @@ export const router = createHashRouter([
|
||||
},
|
||||
{
|
||||
path: '/wifi',
|
||||
caseSensitive: false,
|
||||
lazy: async () => {
|
||||
const { Wifi } = await import('./pages/wifi');
|
||||
return { Component: Wifi };
|
||||
|
||||
Reference in New Issue
Block a user