mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2026-02-10 11:40:28 +08:00
refactor: external controller & rule editor
* update external controller labels for clarity * add generate and toggle visibility for external controller secret * add validation for external controller and rule payloads * enhance rule payload validation UI with prominent error styling
This commit is contained in:
parent
8e6bfb0bd1
commit
1c17cfb683
@ -48,6 +48,7 @@
|
||||
"js-yaml": "^4.1.1",
|
||||
"plist": "^3.1.0",
|
||||
"sysproxy-rs": "file:src/native/sysproxy",
|
||||
"validator": "^13.15.26",
|
||||
"webdav": "^5.8.0",
|
||||
"ws": "^8.18.3",
|
||||
"yaml": "^2.8.1"
|
||||
@ -68,6 +69,7 @@
|
||||
"@types/pubsub-js": "^1.8.6",
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/validator": "^13.15.10",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.52.0",
|
||||
"@typescript-eslint/parser": "^8.52.0",
|
||||
|
||||
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
@ -53,6 +53,9 @@ importers:
|
||||
sysproxy-rs:
|
||||
specifier: file:src/native/sysproxy
|
||||
version: file:src/native/sysproxy
|
||||
validator:
|
||||
specifier: ^13.15.26
|
||||
version: 13.15.26
|
||||
webdav:
|
||||
specifier: ^5.8.0
|
||||
version: 5.8.0
|
||||
@ -108,6 +111,9 @@ importers:
|
||||
'@types/react-dom':
|
||||
specifier: ^19.2.3
|
||||
version: 19.2.3(@types/react@19.2.7)
|
||||
'@types/validator':
|
||||
specifier: ^13.15.10
|
||||
version: 13.15.10
|
||||
'@types/ws':
|
||||
specifier: ^8.18.1
|
||||
version: 8.18.1
|
||||
@ -2239,6 +2245,9 @@ packages:
|
||||
'@types/unist@3.0.3':
|
||||
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
||||
|
||||
'@types/validator@13.15.10':
|
||||
resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==}
|
||||
|
||||
'@types/verror@1.10.11':
|
||||
resolution: {integrity: sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==}
|
||||
|
||||
@ -5093,6 +5102,10 @@ packages:
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
validator@13.15.26:
|
||||
resolution: {integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
vary@1.1.2:
|
||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@ -7986,6 +7999,8 @@ snapshots:
|
||||
|
||||
'@types/unist@3.0.3': {}
|
||||
|
||||
'@types/validator@13.15.10': {}
|
||||
|
||||
'@types/verror@1.10.11':
|
||||
optional: true
|
||||
|
||||
@ -11494,6 +11509,8 @@ snapshots:
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
validator@13.15.26: {}
|
||||
|
||||
vary@1.1.2: {}
|
||||
|
||||
verror@1.10.1:
|
||||
|
||||
@ -31,6 +31,33 @@ import { IoMdTrash, IoMdArrowUp, IoMdArrowDown, IoMdUndo } from 'react-icons/io'
|
||||
import { MdVerticalAlignTop, MdVerticalAlignBottom } from 'react-icons/md'
|
||||
import { platform } from '@renderer/utils/init'
|
||||
import { toast } from '@renderer/components/base/toast'
|
||||
import {
|
||||
domainValidator,
|
||||
domainSuffixValidator,
|
||||
domainKeywordValidator,
|
||||
domainRegexValidator,
|
||||
domainWildcardValidator,
|
||||
geositeValidator,
|
||||
geoipValidator,
|
||||
asnValidator,
|
||||
uidValidator,
|
||||
dscpValidator,
|
||||
networkValidator,
|
||||
processPathValidator,
|
||||
processPathWildcardValidator,
|
||||
processPathRegexValidator,
|
||||
processNameValidator,
|
||||
processNameWildcardValidator,
|
||||
processNameRegexValidator,
|
||||
inTypeValidator,
|
||||
inUserValidator,
|
||||
inNameValidator,
|
||||
ruleSetValidator,
|
||||
logicRuleValidator,
|
||||
subRuleValidator,
|
||||
portRangeValidator,
|
||||
ipCIDRValidator
|
||||
} from '@renderer/utils/validate'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
@ -45,53 +72,6 @@ interface RuleItem {
|
||||
offset?: number
|
||||
}
|
||||
|
||||
const domainValidator = (value: string): boolean => {
|
||||
if (value.length > 253 || value.length < 2) return false
|
||||
|
||||
return (
|
||||
new RegExp('^(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\\.)+[a-zA-Z]{2,}$').test(
|
||||
value
|
||||
) || ['localhost', 'local', 'localdomain'].includes(value.toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
const domainSuffixValidator = (value: string): boolean => {
|
||||
return new RegExp(
|
||||
'^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.[a-zA-Z]{2,}$'
|
||||
).test(value)
|
||||
}
|
||||
|
||||
const domainKeywordValidator = (value: string): boolean => {
|
||||
return value.length > 0 && !value.includes(',') && !value.includes(' ')
|
||||
}
|
||||
|
||||
const domainRegexValidator = (value: string): boolean => {
|
||||
try {
|
||||
new RegExp(value)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const portValidator = (value: string): boolean => {
|
||||
return new RegExp(
|
||||
'^(?:[1-9]\\d{0,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$'
|
||||
).test(value)
|
||||
}
|
||||
|
||||
const ipv4CIDRValidator = (value: string): boolean => {
|
||||
return new RegExp(
|
||||
'^(?:(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))\\.){3}(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))(?:\\/(?:[12]?[0-9]|3[0-2]))$'
|
||||
).test(value)
|
||||
}
|
||||
|
||||
const ipv6CIDRValidator = (value: string): boolean => {
|
||||
return new RegExp(
|
||||
'^([0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4}){7}|::|:(?::[0-9a-fA-F]{1,4}){1,6}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,5}|(?:[0-9a-fA-F]{1,4}:){2}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){3}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){4}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){5}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,6}:)\\/(?:12[0-8]|1[01][0-9]|[1-9]?[0-9])$'
|
||||
).test(value)
|
||||
}
|
||||
|
||||
// 内置路由规则 https://wiki.metacubex.one/config/rules/
|
||||
const ruleDefinitionsMap = new Map<
|
||||
string,
|
||||
@ -136,11 +116,20 @@ const ruleDefinitionsMap = new Map<
|
||||
validator: (value) => domainRegexValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
'DOMAIN-WILDCARD',
|
||||
{
|
||||
name: 'DOMAIN-WILDCARD',
|
||||
example: '*.google.com',
|
||||
validator: (value) => domainWildcardValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
'GEOSITE',
|
||||
{
|
||||
name: 'GEOSITE',
|
||||
example: 'youtube'
|
||||
example: 'youtube',
|
||||
validator: (value) => geositeValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
@ -149,14 +138,16 @@ const ruleDefinitionsMap = new Map<
|
||||
name: 'GEOIP',
|
||||
example: 'CN',
|
||||
noResolve: true,
|
||||
src: true
|
||||
src: true,
|
||||
validator: (value) => geoipValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
'SRC-GEOIP',
|
||||
{
|
||||
name: 'SRC-GEOIP',
|
||||
example: 'CN'
|
||||
example: 'CN',
|
||||
validator: (value) => geoipValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
@ -166,7 +157,7 @@ const ruleDefinitionsMap = new Map<
|
||||
example: '13335',
|
||||
noResolve: true,
|
||||
src: true,
|
||||
validator: (value) => (+value ? true : false)
|
||||
validator: (value) => asnValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
@ -174,7 +165,7 @@ const ruleDefinitionsMap = new Map<
|
||||
{
|
||||
name: 'SRC-IP-ASN',
|
||||
example: '9808',
|
||||
validator: (value) => (+value ? true : false)
|
||||
validator: (value) => asnValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
@ -184,7 +175,7 @@ const ruleDefinitionsMap = new Map<
|
||||
example: '127.0.0.0/8',
|
||||
noResolve: true,
|
||||
src: true,
|
||||
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value)
|
||||
validator: (value) => ipCIDRValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
@ -194,7 +185,7 @@ const ruleDefinitionsMap = new Map<
|
||||
example: '2620:0:2d0:200::7/32',
|
||||
noResolve: true,
|
||||
src: true,
|
||||
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value)
|
||||
validator: (value) => ipCIDRValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
@ -202,7 +193,7 @@ const ruleDefinitionsMap = new Map<
|
||||
{
|
||||
name: 'SRC-IP-CIDR',
|
||||
example: '192.168.1.201/32',
|
||||
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value)
|
||||
validator: (value) => ipCIDRValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
@ -212,7 +203,7 @@ const ruleDefinitionsMap = new Map<
|
||||
example: '8.8.8.8/24',
|
||||
noResolve: true,
|
||||
src: true,
|
||||
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value)
|
||||
validator: (value) => ipCIDRValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
@ -220,7 +211,7 @@ const ruleDefinitionsMap = new Map<
|
||||
{
|
||||
name: 'SRC-IP-SUFFIX',
|
||||
example: '192.168.1.201/8',
|
||||
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value)
|
||||
validator: (value) => ipCIDRValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
@ -228,7 +219,7 @@ const ruleDefinitionsMap = new Map<
|
||||
{
|
||||
name: 'SRC-PORT',
|
||||
example: '7777',
|
||||
validator: (value) => portValidator(value)
|
||||
validator: (value) => portRangeValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
@ -236,7 +227,7 @@ const ruleDefinitionsMap = new Map<
|
||||
{
|
||||
name: 'DST-PORT',
|
||||
example: '80',
|
||||
validator: (value) => portValidator(value)
|
||||
validator: (value) => portRangeValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
@ -244,21 +235,23 @@ const ruleDefinitionsMap = new Map<
|
||||
{
|
||||
name: 'IN-PORT',
|
||||
example: '7897',
|
||||
validator: (value) => portValidator(value)
|
||||
validator: (value) => portRangeValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
'DSCP',
|
||||
{
|
||||
name: 'DSCP',
|
||||
example: '4'
|
||||
example: '4',
|
||||
validator: (value) => dscpValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
'PROCESS-NAME',
|
||||
{
|
||||
name: 'PROCESS-NAME',
|
||||
example: platform === 'win32' ? 'chrome.exe' : 'curl'
|
||||
example: platform === 'win32' ? 'chrome.exe' : 'curl',
|
||||
validator: (value) => processNameValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
@ -268,21 +261,40 @@ const ruleDefinitionsMap = new Map<
|
||||
example:
|
||||
platform === 'win32'
|
||||
? 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
|
||||
: '/usr/bin/wget'
|
||||
: '/usr/bin/wget',
|
||||
validator: (value) => processPathValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
'PROCESS-NAME-WILDCARD',
|
||||
{
|
||||
name: 'PROCESS-NAME-WILDCARD',
|
||||
example: '*telegram*',
|
||||
validator: (value) => processNameWildcardValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
'PROCESS-NAME-REGEX',
|
||||
{
|
||||
name: 'PROCESS-NAME-REGEX',
|
||||
example: '.*telegram.*'
|
||||
example: '.*telegram.*',
|
||||
validator: (value) => processNameRegexValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
'PROCESS-PATH-WILDCARD',
|
||||
{
|
||||
name: 'PROCESS-PATH-WILDCARD',
|
||||
example: platform === 'win32' ? '*\\chrome.exe' : '/usr/*/wget',
|
||||
validator: (value) => processPathWildcardValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
'PROCESS-PATH-REGEX',
|
||||
{
|
||||
name: 'PROCESS-PATH-REGEX',
|
||||
example: platform === 'win32' ? '(?i).*Application\\chrome.*' : '.*bin/wget'
|
||||
example: platform === 'win32' ? '(?i).*Application\\\\chrome.*' : '.*bin/wget',
|
||||
validator: (value) => processPathRegexValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
@ -290,7 +302,7 @@ const ruleDefinitionsMap = new Map<
|
||||
{
|
||||
name: 'NETWORK',
|
||||
example: 'udp',
|
||||
validator: (value) => ['tcp', 'udp'].includes(value)
|
||||
validator: (value) => networkValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
@ -298,35 +310,39 @@ const ruleDefinitionsMap = new Map<
|
||||
{
|
||||
name: 'UID',
|
||||
example: '1001',
|
||||
validator: (value) => (+value ? true : false)
|
||||
validator: (value) => uidValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
'IN-TYPE',
|
||||
{
|
||||
name: 'IN-TYPE',
|
||||
example: 'SOCKS/HTTP'
|
||||
example: 'SOCKS/HTTP',
|
||||
validator: (value) => inTypeValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
'IN-USER',
|
||||
{
|
||||
name: 'IN-USER',
|
||||
example: 'mihomo'
|
||||
example: 'mihomo',
|
||||
validator: (value) => inUserValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
'IN-NAME',
|
||||
{
|
||||
name: 'IN-NAME',
|
||||
example: 'ss'
|
||||
example: 'ss',
|
||||
validator: (value) => inNameValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
'SUB-RULE',
|
||||
{
|
||||
name: 'SUB-RULE',
|
||||
example: '(NETWORK,tcp)'
|
||||
example: '(NETWORK,tcp)',
|
||||
validator: (value) => subRuleValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
@ -335,28 +351,32 @@ const ruleDefinitionsMap = new Map<
|
||||
name: 'RULE-SET',
|
||||
example: 'providername',
|
||||
noResolve: true,
|
||||
src: true
|
||||
src: true,
|
||||
validator: (value) => ruleSetValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
'AND',
|
||||
{
|
||||
name: 'AND',
|
||||
example: '((DOMAIN,baidu.com),(NETWORK,UDP))'
|
||||
example: '((DOMAIN,baidu.com),(NETWORK,UDP))',
|
||||
validator: (value) => logicRuleValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
'OR',
|
||||
{
|
||||
name: 'OR',
|
||||
example: '((NETWORK,UDP),(DOMAIN,baidu.com))'
|
||||
example: '((NETWORK,UDP),(DOMAIN,baidu.com))',
|
||||
validator: (value) => logicRuleValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
'NOT',
|
||||
{
|
||||
name: 'NOT',
|
||||
example: '((DOMAIN,baidu.com))'
|
||||
example: '((DOMAIN,baidu.com))',
|
||||
validator: (value) => logicRuleValidator(value)
|
||||
}
|
||||
],
|
||||
[
|
||||
@ -1214,11 +1234,7 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
||||
value={newRule.payload}
|
||||
onValueChange={(value) => setNewRule({ ...newRule, payload: value })}
|
||||
isDisabled={newRule.type === 'MATCH'}
|
||||
color={
|
||||
newRule.payload && newRule.type !== 'MATCH' && !isPayloadValid
|
||||
? 'danger'
|
||||
: 'default'
|
||||
}
|
||||
className={`${newRule.payload && newRule.type !== 'MATCH' && !isPayloadValid ? 'border-red-500 ring-1 ring-red-500 rounded-lg' : ''}`}
|
||||
/>
|
||||
|
||||
<Autocomplete
|
||||
|
||||
@ -65,6 +65,7 @@
|
||||
"common.updater.versionReady": "v{{version}} Version Ready",
|
||||
"common.updater.goToDownload": "Go to Download",
|
||||
"common.updater.update": "Update",
|
||||
"common.generateSecret": "Generate Secret",
|
||||
"common.refresh": "Refresh",
|
||||
"settings.general": "General Settings",
|
||||
"settings.mihomo": "Mihomo Settings",
|
||||
@ -201,8 +202,8 @@
|
||||
"mihomo.httpPort": "Http Port",
|
||||
"mihomo.redirPort": "Redir Port",
|
||||
"mihomo.tproxyPort": "Tproxy Port",
|
||||
"mihomo.externalController": "External Controller Address",
|
||||
"mihomo.externalControllerSecret": "External Controller Secret",
|
||||
"mihomo.externalController": "Listen Address",
|
||||
"mihomo.externalControllerSecret": "Access Token",
|
||||
"mihomo.ipv6": "IPv6",
|
||||
"mihomo.allowLanConnection": "Allow LAN Connection",
|
||||
"mihomo.allowedIpSegments": "Allowed IP Segments",
|
||||
|
||||
@ -58,6 +58,8 @@
|
||||
"common.error.dnsConfigSaveFailed": "Не удалось сохранить конфигурацию DNS",
|
||||
"common.copied": "Скопировано",
|
||||
"common.ok": "ОК",
|
||||
"common.generateSecret": "Сгенерировать ключ",
|
||||
"common.refresh": "Обновить",
|
||||
"common.error.autoUpdateNotSupported": "Автообновление не поддерживается, пожалуйста, скачайте вручную",
|
||||
"common.dialog.selectSubscriptionFile": "Выберите файл подписки",
|
||||
"core.highPrivilege.title": "High Privilege Core Detected",
|
||||
@ -177,8 +179,8 @@
|
||||
"mihomo.httpPort": "Порт Http",
|
||||
"mihomo.redirPort": "Порт Redir",
|
||||
"mihomo.tproxyPort": "Порт Tproxy",
|
||||
"mihomo.externalController": "Адрес внешнего контроллера",
|
||||
"mihomo.externalControllerSecret": "Секрет внешнего контроллера",
|
||||
"mihomo.externalController": "Адрес прослушивания",
|
||||
"mihomo.externalControllerSecret": "Ключ доступа",
|
||||
"mihomo.ipv6": "IPv6",
|
||||
"mihomo.allowLanConnection": "Разрешить LAN подключения",
|
||||
"mihomo.allowedIpSegments": "Разрешённые IP сегменты",
|
||||
|
||||
@ -65,6 +65,7 @@
|
||||
"common.updater.versionReady": "v{{version}} 版本就绪",
|
||||
"common.updater.goToDownload": "前往下载",
|
||||
"common.updater.update": "更新",
|
||||
"common.generateSecret": "生成密钥",
|
||||
"common.refresh": "刷新",
|
||||
"settings.general": "通用设置",
|
||||
"settings.mihomo": "Mihomo 设置",
|
||||
@ -201,8 +202,8 @@
|
||||
"mihomo.httpPort": "Http 端口",
|
||||
"mihomo.redirPort": "Redir 端口",
|
||||
"mihomo.tproxyPort": "Tproxy 端口",
|
||||
"mihomo.externalController": "外部控制地址",
|
||||
"mihomo.externalControllerSecret": "外部控制访问密钥",
|
||||
"mihomo.externalController": "监听地址",
|
||||
"mihomo.externalControllerSecret": "访问密钥",
|
||||
"mihomo.ipv6": "IPv6",
|
||||
"mihomo.allowLanConnection": "允许局域网连接",
|
||||
"mihomo.allowedIpSegments": "允许连接的 IP 段",
|
||||
|
||||
@ -65,6 +65,7 @@
|
||||
"common.updater.versionReady": "v{{version}} 版本就緒",
|
||||
"common.updater.goToDownload": "前往下載",
|
||||
"common.updater.update": "更新",
|
||||
"common.generateSecret": "生成密鑰",
|
||||
"common.refresh": "重新整理",
|
||||
"settings.general": "通用設置",
|
||||
"settings.mihomo": "Mihomo 設置",
|
||||
@ -201,8 +202,8 @@
|
||||
"mihomo.httpPort": "Http 埠",
|
||||
"mihomo.redirPort": "Redir 埠",
|
||||
"mihomo.tproxyPort": "Tproxy 埠",
|
||||
"mihomo.externalController": "外部控制地址",
|
||||
"mihomo.externalControllerSecret": "外部控制訪問密鑰",
|
||||
"mihomo.externalController": "监听地址",
|
||||
"mihomo.externalControllerSecret": "访问密钥",
|
||||
"mihomo.ipv6": "IPv6",
|
||||
"mihomo.allowLanConnection": "允許局域網連接",
|
||||
"mihomo.allowedIpSegments": "允许連接的 IP 段",
|
||||
|
||||
@ -20,6 +20,7 @@ import { toast } from '@renderer/components/base/toast'
|
||||
import { showError } from '@renderer/utils/error-display'
|
||||
import SettingCard from '@renderer/components/base/base-setting-card'
|
||||
import SettingItem from '@renderer/components/base/base-setting-item'
|
||||
import { isValidListenAddress, getError, isValid } from '@renderer/utils/validate'
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
|
||||
import { platform } from '@renderer/utils/init'
|
||||
@ -28,7 +29,9 @@ import {
|
||||
IoMdCloudDownload,
|
||||
IoMdInformationCircleOutline,
|
||||
IoMdRefresh,
|
||||
IoMdShuffle
|
||||
IoMdShuffle,
|
||||
IoMdEye,
|
||||
IoMdEyeOff
|
||||
} from 'react-icons/io'
|
||||
import PubSub from 'pubsub-js'
|
||||
import {
|
||||
@ -136,7 +139,12 @@ const Mihomo: React.FC = () => {
|
||||
const [redirPortInput, setRedirPortInput] = useState(showRedirPort ?? redirPort)
|
||||
const [tproxyPortInput, setTproxyPortInput] = useState(showTproxyPort ?? tproxyPort)
|
||||
const [externalControllerInput, setExternalControllerInput] = useState(externalController)
|
||||
const [externalControllerError, setExternalControllerError] = useState<string | null>(() => {
|
||||
const result = isValidListenAddress(externalController)
|
||||
return isValid(result) ? null : (getError(result) ?? '格式错误')
|
||||
})
|
||||
const [secretInput, setSecretInput] = useState(secret)
|
||||
const [isSecretVisible, setIsSecretVisible] = useState(false)
|
||||
const [lanAllowedIpsInput, setLanAllowedIpsInput] = useState(lanAllowedIps)
|
||||
const [lanDisallowedIpsInput, setLanDisallowedIpsInput] = useState(lanDisallowedIps)
|
||||
const [authenticationInput, setAuthenticationInput] = useState(authentication)
|
||||
@ -994,11 +1002,12 @@ const Mihomo: React.FC = () => {
|
||||
)}
|
||||
<SettingItem title={t('mihomo.externalController')} divider>
|
||||
<div className="flex">
|
||||
{externalControllerInput !== externalController && (
|
||||
{externalControllerInput !== externalController && !externalControllerError && (
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
className="mr-2"
|
||||
isDisabled={!!externalControllerError}
|
||||
onPress={() => {
|
||||
onChangeNeedRestart({
|
||||
'external-controller': externalControllerInput
|
||||
@ -1009,17 +1018,48 @@ const Mihomo: React.FC = () => {
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Tooltip
|
||||
content={externalControllerError}
|
||||
placement="right"
|
||||
isOpen={!!externalControllerError}
|
||||
showArrow={true}
|
||||
color="danger"
|
||||
offset={10}
|
||||
>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[200px]"
|
||||
className={`w-[200px] ${externalControllerError ? 'border-red-500 ring-1 ring-red-500 rounded-lg' : ''}`}
|
||||
value={externalControllerInput}
|
||||
onValueChange={(v) => {
|
||||
setExternalControllerInput(v)
|
||||
const result = isValidListenAddress(v)
|
||||
setExternalControllerError(isValid(result) ? null : (getError(result) ?? '格式错误'))
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title={t('mihomo.externalControllerSecret')} divider>
|
||||
<SettingItem
|
||||
title={t('mihomo.externalControllerSecret')}
|
||||
actions={
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
title={t('common.generateSecret')}
|
||||
variant="light"
|
||||
onPress={() => {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const randomSecret = Array.from({ length: 8 }, () =>
|
||||
chars[Math.floor(Math.random() * chars.length)]
|
||||
).join('');
|
||||
setSecretInput(randomSecret);
|
||||
}}
|
||||
>
|
||||
<IoMdRefresh className="text-lg" />
|
||||
</Button>
|
||||
}
|
||||
divider
|
||||
>
|
||||
<div className="flex">
|
||||
{secretInput !== secret && (
|
||||
<Button
|
||||
@ -1036,12 +1076,25 @@ const Mihomo: React.FC = () => {
|
||||
|
||||
<Input
|
||||
size="sm"
|
||||
type="password"
|
||||
type={isSecretVisible ? 'text' : 'password'}
|
||||
className="w-[200px]"
|
||||
value={secretInput}
|
||||
onValueChange={(v) => {
|
||||
setSecretInput(v)
|
||||
}}
|
||||
startContent={
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsSecretVisible(prev => !prev)}
|
||||
className="text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
{isSecretVisible ? (
|
||||
<IoMdEyeOff className="w-4 h-4" />
|
||||
) : (
|
||||
<IoMdEye className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
|
||||
375
src/renderer/src/utils/validate.ts
Normal file
375
src/renderer/src/utils/validate.ts
Normal file
@ -0,0 +1,375 @@
|
||||
import validator from 'validator'
|
||||
|
||||
const domainValidator = (value: string): boolean => {
|
||||
if (value.length > 253 || value.length < 2) return false
|
||||
|
||||
// 检查是否为合法的 FQDN (完全限定域名)
|
||||
if (validator.isFQDN(value, { require_tld: true })) return true
|
||||
|
||||
// 允许特殊的本地域名
|
||||
return ['localhost', 'local', 'localdomain'].includes(value.toLowerCase())
|
||||
}
|
||||
|
||||
const domainSuffixValidator = (value: string): boolean => {
|
||||
// 域名后缀验证 - 可以是完整域名或带通配符的域名后缀
|
||||
return validator.isFQDN(value, { require_tld: true, allow_wildcard: true })
|
||||
}
|
||||
|
||||
const domainKeywordValidator = (value: string): boolean => {
|
||||
// 域名关键字不能包含逗号和空格
|
||||
return value.length > 0 && validator.isWhitelisted(value, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._')
|
||||
}
|
||||
|
||||
const domainRegexValidator = (value: string): boolean => {
|
||||
try {
|
||||
new RegExp(value)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const portValidator = (value: string): boolean => {
|
||||
return validator.isPort(value)
|
||||
}
|
||||
|
||||
const ipv4CIDRValidator = (value: string): boolean => {
|
||||
// 验证 IPv4 CIDR 格式 (例如: 192.168.1.0/24)
|
||||
if (!value.includes('/')) return false
|
||||
|
||||
const [ip, cidr] = value.split('/')
|
||||
const cidrNum = parseInt(cidr, 10)
|
||||
|
||||
return validator.isIP(ip, 4) && !isNaN(cidrNum) && cidrNum >= 0 && cidrNum <= 32
|
||||
}
|
||||
|
||||
const ipv6CIDRValidator = (value: string): boolean => {
|
||||
// 验证 IPv6 CIDR 格式 (例如: 2001:db8::/32)
|
||||
if (!value.includes('/')) return false
|
||||
|
||||
const [ip, cidr] = value.split('/')
|
||||
const cidrNum = parseInt(cidr, 10)
|
||||
|
||||
return validator.isIP(ip, 6) && !isNaN(cidrNum) && cidrNum >= 0 && cidrNum <= 128
|
||||
}
|
||||
|
||||
// 便捷函数:将 ValidationResult 转换为布尔值
|
||||
export const isValid = (result: ValidationResult): boolean => result.ok
|
||||
|
||||
// 便捷函数:获取错误信息
|
||||
export const getError = (result: ValidationResult): string | undefined => result.error
|
||||
|
||||
// IP CIDR 验证器(同时支持 IPv4 和 IPv6)
|
||||
const ipCIDRValidator = (value: string): boolean => {
|
||||
return ipv4CIDRValidator(value) || ipv6CIDRValidator(value)
|
||||
}
|
||||
|
||||
// DOMAIN-WILDCARD 验证器 - 仅支持 * 和 ? 通配符
|
||||
const domainWildcardValidator = (value: string): boolean => {
|
||||
if (value.length === 0) return false
|
||||
// 检查是否只包含合法字符(字母、数字、点、*、?、-)
|
||||
const validPattern = /^[a-zA-Z0-9.*?-]+$/
|
||||
if (!validPattern.test(value)) return false
|
||||
// 移除通配符后验证基本格式
|
||||
const withoutWildcards = value.replace(/\*/g, 'a').replace(/\?/g, 'a')
|
||||
// 至少要有一个点(域名结构)
|
||||
return withoutWildcards.includes('.')
|
||||
}
|
||||
|
||||
// GEOSITE 验证器 - 站点名称验证
|
||||
const geositeValidator = (value: string): boolean => {
|
||||
// GEOSITE 名称只能包含字母、数字、连字符和下划线
|
||||
return validator.isAlphanumeric(value, 'en-US', { ignore: '-_' }) && value.length > 0
|
||||
}
|
||||
|
||||
// GEOIP 验证器 - 国家代码验证(ISO 3166-1 alpha-2)
|
||||
const geoipValidator = (value: string): boolean => {
|
||||
// 支持2位国家代码(大小写不敏感)
|
||||
return validator.isAlpha(value) && value.length === 2
|
||||
}
|
||||
|
||||
// ASN 验证器 - 自治系统号验证
|
||||
const asnValidator = (value: string): boolean => {
|
||||
// ASN 范围: 1 - 4294967295 (32-bit)
|
||||
return validator.isInt(value, { min: 1, max: 4294967295 })
|
||||
}
|
||||
|
||||
// UID 验证器 - Linux 用户 ID 验证
|
||||
const uidValidator = (value: string): boolean => {
|
||||
// UID 范围: 0 - 65535 (大多数系统)
|
||||
return validator.isInt(value, { min: 0, max: 65535 })
|
||||
}
|
||||
|
||||
// DSCP 验证器 - 区分服务代码点验证
|
||||
const dscpValidator = (value: string): boolean => {
|
||||
// DSCP 范围: 0 - 63 (6-bit)
|
||||
return validator.isInt(value, { min: 0, max: 63 })
|
||||
}
|
||||
|
||||
// NETWORK 验证器 - 网络类型验证
|
||||
const networkValidator = (value: string): boolean => {
|
||||
return validator.isIn(value.toLowerCase(), ['tcp', 'udp'])
|
||||
}
|
||||
|
||||
// 进程路径验证器
|
||||
const processPathValidator = (value: string): boolean => {
|
||||
if (value.length === 0) return false
|
||||
// Windows 路径或 Unix 路径
|
||||
const windowsPath = /^[a-zA-Z]:[\\/].+/
|
||||
const unixPath = /^\/.*/
|
||||
const androidPackage = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/i
|
||||
return windowsPath.test(value) || unixPath.test(value) || androidPackage.test(value)
|
||||
}
|
||||
|
||||
// 进程路径通配符验证器
|
||||
const processPathWildcardValidator = (value: string): boolean => {
|
||||
if (value.length === 0) return false
|
||||
// 包含通配符的路径,移除通配符后检查路径格式
|
||||
const withoutWildcards = value.replace(/\*/g, 'a').replace(/\?/g, 'a')
|
||||
return processPathValidator(withoutWildcards)
|
||||
}
|
||||
|
||||
// 进程路径正则验证器
|
||||
const processPathRegexValidator = (value: string): boolean => {
|
||||
try {
|
||||
new RegExp(value)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 进程名称验证器
|
||||
const processNameValidator = (value: string): boolean => {
|
||||
if (value.length === 0) return false
|
||||
// 进程名或 Android 包名
|
||||
const processName = /^[a-zA-Z0-9\-_.]+$/
|
||||
const androidPackage = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/i
|
||||
return processName.test(value) || androidPackage.test(value)
|
||||
}
|
||||
|
||||
// 进程名称通配符验证器
|
||||
const processNameWildcardValidator = (value: string): boolean => {
|
||||
if (value.length === 0) return false
|
||||
// 移除通配符后检查进程名格式
|
||||
const withoutWildcards = value.replace(/\*/g, 'a').replace(/\?/g, 'a')
|
||||
return processNameValidator(withoutWildcards)
|
||||
}
|
||||
|
||||
// 进程名称正则验证器
|
||||
const processNameRegexValidator = (value: string): boolean => {
|
||||
try {
|
||||
new RegExp(value)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IN-TYPE 验证器 - 入站类型验证
|
||||
const inTypeValidator = (value: string): boolean => {
|
||||
// 支持单个或多个类型(用 / 分隔)
|
||||
const types = value.split('/')
|
||||
const validTypes = ['http', 'https', 'socks', 'socks4', 'socks5', 'tproxy', 'redir', 'mixed']
|
||||
return types.length > 0 && types.every((type) => validator.isIn(type.toLowerCase(), validTypes))
|
||||
}
|
||||
|
||||
// IN-USER 验证器 - 入站用户名验证
|
||||
const inUserValidator = (value: string): boolean => {
|
||||
if (value.length === 0) return false
|
||||
// 支持多个用户名(用 / 分隔)
|
||||
const users = value.split('/')
|
||||
return users.every((user) => user.length > 0 && validator.isAlphanumeric(user, 'en-US', { ignore: '-_.' }))
|
||||
}
|
||||
|
||||
// IN-NAME 验证器 - 入站名称验证
|
||||
const inNameValidator = (value: string): boolean => {
|
||||
// 入站名称可以包含字母、数字、连字符和下划线
|
||||
return validator.isAlphanumeric(value, 'en-US', { ignore: '-_' }) && value.length > 0
|
||||
}
|
||||
|
||||
// RULE-SET 验证器 - 规则集名称验证
|
||||
const ruleSetValidator = (value: string): boolean => {
|
||||
// 规则集名称(对应 rule-providers 中定义的名称)
|
||||
return validator.isAlphanumeric(value, 'en-US', { ignore: '-_' }) && value.length > 0
|
||||
}
|
||||
|
||||
// 逻辑规则验证器 - AND, OR, NOT
|
||||
const logicRuleValidator = (value: string): boolean => {
|
||||
if (value.length === 0) return false
|
||||
// 检查括号是否匹配
|
||||
let depth = 0
|
||||
for (const char of value) {
|
||||
if (char === '(') depth++
|
||||
if (char === ')') depth--
|
||||
if (depth < 0) return false
|
||||
}
|
||||
return depth === 0 && value.startsWith('(') && value.endsWith(')')
|
||||
}
|
||||
|
||||
// SUB-RULE 验证器 - 子规则验证
|
||||
const subRuleValidator = (value: string): boolean => {
|
||||
if (value.length === 0) return false
|
||||
// 格式: (RULE_TYPE,payload) 或 provider_name
|
||||
if (value.startsWith('(') && value.endsWith(')')) {
|
||||
return logicRuleValidator(value)
|
||||
}
|
||||
// 如果不是括号格式,则视为 provider 名称
|
||||
return ruleSetValidator(value)
|
||||
}
|
||||
|
||||
// 端口范围验证器(支持单个端口或范围)
|
||||
const portRangeValidator = (value: string): boolean => {
|
||||
// 支持单个端口或范围格式,如: 80 或 8000-9000
|
||||
if (value.includes('-')) {
|
||||
const [start, end] = value.split('-')
|
||||
return validator.isPort(start) && validator.isPort(end) && parseInt(start) <= parseInt(end)
|
||||
}
|
||||
return validator.isPort(value)
|
||||
}
|
||||
|
||||
export {
|
||||
domainValidator,
|
||||
domainSuffixValidator,
|
||||
domainKeywordValidator,
|
||||
domainRegexValidator,
|
||||
domainWildcardValidator,
|
||||
geositeValidator,
|
||||
geoipValidator,
|
||||
asnValidator,
|
||||
uidValidator,
|
||||
dscpValidator,
|
||||
networkValidator,
|
||||
processPathValidator,
|
||||
processPathWildcardValidator,
|
||||
processPathRegexValidator,
|
||||
processNameValidator,
|
||||
processNameWildcardValidator,
|
||||
processNameRegexValidator,
|
||||
inTypeValidator,
|
||||
inUserValidator,
|
||||
inNameValidator,
|
||||
ruleSetValidator,
|
||||
logicRuleValidator,
|
||||
subRuleValidator,
|
||||
portValidator,
|
||||
portRangeValidator,
|
||||
ipv4CIDRValidator,
|
||||
ipv6CIDRValidator,
|
||||
ipCIDRValidator
|
||||
}
|
||||
|
||||
// 通用验证结果类型
|
||||
export interface ValidationResult {
|
||||
ok: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
// 验证 IPv4 地址
|
||||
export const isIPv4 = (ip: string): ValidationResult => {
|
||||
if (!validator.isIP(ip, 4)) {
|
||||
return { ok: false, error: '不是有效的 IPv4 地址' }
|
||||
}
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
// 验证 IPv6 地址
|
||||
export const isIPv6 = (ip: string): ValidationResult => {
|
||||
if (!validator.isIP(ip, 6)) {
|
||||
return { ok: false, error: '不是有效的 IPv6 地址' }
|
||||
}
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
// 验证端口
|
||||
export const isValidPort = (port: string): ValidationResult => {
|
||||
if (!validator.isPort(port)) {
|
||||
return { ok: false, error: '端口号必须在 1-65535 范围内' }
|
||||
}
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
// 验证监听地址
|
||||
export const isValidListenAddress = (s: string | undefined): ValidationResult => {
|
||||
if (!s || s.trim() === '') return { ok: true }
|
||||
|
||||
const v = s.trim()
|
||||
|
||||
// 格式: :port (仅端口)
|
||||
if (v.startsWith(':')) {
|
||||
return isValidPort(v.slice(1))
|
||||
}
|
||||
|
||||
const idx = v.lastIndexOf(':')
|
||||
if (idx === -1) return { ok: false, error: '应包含端口号' }
|
||||
|
||||
const host = v.slice(0, idx)
|
||||
const port = v.slice(idx + 1)
|
||||
|
||||
// 验证端口
|
||||
const portResult = isValidPort(port)
|
||||
if (!portResult.ok) return portResult
|
||||
|
||||
// 格式: [IPv6]:port
|
||||
if (host.startsWith('[') && host.endsWith(']')) {
|
||||
const inner = host.slice(1, -1)
|
||||
return isIPv6(inner)
|
||||
}
|
||||
|
||||
// IPv4 地址
|
||||
if (validator.isIP(host, 4)) {
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
// 域名或主机名 (使用宽松的 FQDN 验证)
|
||||
if (validator.isFQDN(host, { require_tld: false }) || validator.isAlphanumeric(host, 'en-US', { ignore: '-.' })) {
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
return { ok: false, error: '主机名包含非法字符' }
|
||||
}
|
||||
|
||||
// 验证监听地址(完整版,包含 0.0.0.0 和 ::)
|
||||
export const isValidListenAddressFull = (s: string | undefined): ValidationResult => {
|
||||
if (!s || s.trim() === '') return { ok: true }
|
||||
|
||||
const v = s.trim()
|
||||
|
||||
// 格式: :port (仅端口)
|
||||
if (v.startsWith(':')) {
|
||||
return isValidPort(v.slice(1))
|
||||
}
|
||||
|
||||
const idx = v.lastIndexOf(':')
|
||||
if (idx === -1) return { ok: false, error: '应包含端口号' }
|
||||
|
||||
const host = v.slice(0, idx)
|
||||
const port = v.slice(idx + 1)
|
||||
|
||||
// 验证端口
|
||||
const portResult = isValidPort(port)
|
||||
if (!portResult.ok) return portResult
|
||||
|
||||
// 格式: [IPv6]:port
|
||||
if (host.startsWith('[') && host.endsWith(']')) {
|
||||
const inner = host.slice(1, -1)
|
||||
return isIPv6(inner)
|
||||
}
|
||||
|
||||
// 特殊地址: 0.0.0.0 (监听所有 IPv4) 或 :: (监听所有 IPv6)
|
||||
if (host === '0.0.0.0' || host === '::') {
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
// IPv4 地址
|
||||
if (validator.isIP(host, 4)) {
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
// 域名或主机名 (使用宽松的 FQDN 验证)
|
||||
if (validator.isFQDN(host, { require_tld: false }) || validator.isAlphanumeric(host, 'en-US', { ignore: '-.' })) {
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
return { ok: false, error: '主机名包含非法字符' }
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user