From e01c91a1a3691c22e2ab6c7ab37283d6bb242672 Mon Sep 17 00:00:00 2001 From: fengfeng Date: Mon, 9 Feb 2026 13:06:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=85=A8=E5=B1=80=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E7=BC=96=E8=BE=91=E7=AA=97=E5=8F=A3=E5=92=8C=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO.md | 5 +-- src/App.tsx | 2 +- src/components/NodeEditor.tsx | 53 ++++++++++++++++++++++++++++--- src/components/SettingsEditor.tsx | 8 +++-- src/utils/iputils.ts | 29 +++++++++++------ 5 files changed, 78 insertions(+), 19 deletions(-) diff --git a/TODO.md b/TODO.md index 5d4e075..56bd42c 100644 --- a/TODO.md +++ b/TODO.md @@ -2,12 +2,13 @@ - [x] 完成节点的编辑窗口 - [x] 完成边的编辑窗口 -- [ ] 完成全局设置编辑窗口 -- [ ] 完成参数校验以及参数联动逻辑 +- [x] 完成全局设置编辑窗口 +- [x] 完成参数校验以及参数联动逻辑 - [ ] 实现配置生成逻辑,并验证有效 - [ ] 实现子网路由功能,并验证有效 - [ ] 实现配置保存和加载功能 - [ ] 实现加密功能(完全加密和只加密私钥) +- [ ] 添加测试用例 - [ ] 完成! diff --git a/src/App.tsx b/src/App.tsx index c68e263..43656c2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -109,7 +109,7 @@ function FlowContent(): ReactNode { const handleAddNode = (): void => { const result = generateNodeData(nodes.length); - if(result == null) return; + if(!result) return; const newNode: AppNode = { id: result.id, position: { x: 0, y: 0 }, diff --git a/src/components/NodeEditor.tsx b/src/components/NodeEditor.tsx index eb09584..8c42a77 100644 --- a/src/components/NodeEditor.tsx +++ b/src/components/NodeEditor.tsx @@ -1,6 +1,7 @@ import { useState, ReactNode } from 'react'; import { NodeData, Settings, NodeDataUpdate } from '../types/graph'; import { generateWireGuardPrivateKey } from '../utils/wireguardConfig' +import { IPNetwork} from '../utils/iputils' import './FormEditor.css'; import Folder from './Folder' @@ -11,6 +12,43 @@ interface NodeEditorProps { onClose: () => void; } + +class Validation { + constructor( + public readonly isValid: boolean, + public readonly errors: string[], + ) {} +} + +function Validate(updateData : NodeDataUpdate, settings : Settings) : Validation { + const errors: string[] = []; + + const ipv4Subnet = settings.ipv4Subnet; + const ipv4Address = updateData.ipv4Address; + if(ipv4Subnet) { + const cidr = IPNetwork.parse(ipv4Subnet); + if(!ipv4Address) { + errors.push("需要设置IPv4地址"); + } else if(!cidr.contains(IPNetwork.parse(`${ipv4Address}/32`))) { + errors.push("IPv4不在子网范围中"); + } + } + + const ipv6Subnet = settings.ipv6Subnet; + const ipv6Address = updateData.ipv6Address; + if(ipv6Subnet) { + const cidr = IPNetwork.parse(ipv6Subnet); + if(!ipv6Address) { + errors.push("需要设置IPv6地址"); + } else if(!cidr.contains(IPNetwork.parse(`${ipv6Address}/128`))) { + errors.push("IPv6不在子网范围中"); + } + } + + const isValid : boolean = errors.length === 0; + return new Validation(isValid, errors); +} + export default function NodeEditor({ node, settings, @@ -33,9 +71,7 @@ export default function NodeEditor({ const [notes, setNotes] = useState(node.notes) const handleSave = (): void => { - // todo: 校验 - setErrors([]); - onUpdate({ + const updateData : NodeDataUpdate = { label: label, privateKey: privateKey, ipv4Address: ipv4Address, @@ -47,7 +83,16 @@ export default function NodeEditor({ listenPort: listenPort, dnsServers: dnsServers, notes: notes - }); + } + + const validation = Validate(updateData, settings); + if(!validation.isValid) { + setErrors(validation.errors); + return ; + } + + setErrors([]); + onUpdate(updateData); onClose(); }; diff --git a/src/components/SettingsEditor.tsx b/src/components/SettingsEditor.tsx index 7d570a8..45ae66f 100644 --- a/src/components/SettingsEditor.tsx +++ b/src/components/SettingsEditor.tsx @@ -26,13 +26,17 @@ export default function SettingsEditor({ if(ipv4Subnet) { const result = IPNetwork.parse(ipv4Subnet) if(!result.isValid) { - errorInfo.push(result.error ?? "ipv4子网不合法") + errorInfo.push("IPv4子网:" + (result.error ?? "ipv4子网不合法")) + } else if(result.version != 'IPv4') { + errorInfo.push("IPv4子网:" + "非IPv4 CIDR"); } } if(ipv6Subnet) { const result = IPNetwork.parse(ipv6Subnet) if(!result.isValid) { - errorInfo.push(result.error ?? "ipv6子网不合法") + errorInfo.push("IPv6子网:" + (result.error ?? "子网不合法")); + } else if(result.version != 'IPv6') { + errorInfo.push("IPv6子网:" + "非IPv6 CIDR"); } } if(errorInfo.length > 0) { diff --git a/src/utils/iputils.ts b/src/utils/iputils.ts index f20c747..d1d451f 100644 --- a/src/utils/iputils.ts +++ b/src/utils/iputils.ts @@ -1,18 +1,27 @@ export type IPVersion = 'IPv4' | 'IPv6' | 'invalid'; -export interface IPResult { - isValid: boolean; - version: IPVersion; - binary: string; - mask: number; - error?: string; +export class CIDR { + constructor( + public isValid: boolean, + public version: IPVersion, + public binary: string, + public mask: number, + public error?: string + ) {} + + contains(cidr: CIDR) : boolean { + if(!cidr.isValid || !this.isValid) return false; + if(this.version !== cidr.version) return false; + if(this.mask > cidr.mask) return false; + return this.binary.slice(0, this.mask) === cidr.binary.slice(0, this.mask); + } } export class IPNetwork { /** * 解析 CIDR,返回结果对象(不抛出异常) */ - static parse(cidr: string): IPResult { + static parse(cidr: string): CIDR { const parts = cidr.split('/'); if (parts.length !== 2) { return this.invalid('格式错误,缺少掩码 (如 /24)'); @@ -34,7 +43,7 @@ export class IPNetwork { const binary = version === 'IPv4' ? this.ipv4ToBinary(ip) : this.ipv6ToBinary(ip); if (!binary) return this.invalid('IP 地址数值非法'); - return { isValid: true, version, binary, mask }; + return new CIDR(true, version, binary, mask); } catch (e) { return this.invalid('解析过程中出错'); } @@ -84,8 +93,8 @@ export class IPNetwork { } } - private static invalid(msg: string): IPResult { - return { isValid: false, version: 'invalid', binary: '', mask: -1, error: msg }; + private static invalid(msg: string): CIDR { + return new CIDR(false, 'invalid', '', -1, msg); } /**