mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-27 05:00:30 +08:00
feat: rule example & rule validation
This commit is contained in:
parent
f61072c309
commit
877a84dc32
@ -20,6 +20,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import yaml from 'js-yaml'
|
import yaml from 'js-yaml'
|
||||||
import { IoMdTrash, IoMdArrowUp, IoMdArrowDown, IoMdUndo } from 'react-icons/io'
|
import { IoMdTrash, IoMdArrowUp, IoMdArrowDown, IoMdUndo } from 'react-icons/io'
|
||||||
import { MdVerticalAlignTop, MdVerticalAlignBottom } from 'react-icons/md'
|
import { MdVerticalAlignTop, MdVerticalAlignBottom } from 'react-icons/md'
|
||||||
|
import { platform } from '@renderer/utils/init'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string
|
id: string
|
||||||
@ -33,61 +34,216 @@ interface RuleItem {
|
|||||||
additionalParams?: string[]
|
additionalParams?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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/
|
// 内置路由规则 https://wiki.metacubex.one/config/rules/
|
||||||
const ruleTypes = [
|
const ruleDefinitionsMap = new Map<string, {
|
||||||
'DOMAIN',
|
name: string;
|
||||||
'DOMAIN-SUFFIX',
|
required?: boolean;
|
||||||
'DOMAIN-KEYWORD',
|
example?: string;
|
||||||
'DOMAIN-WILDCARD',
|
noResolve?: boolean;
|
||||||
'DOMAIN-REGEX',
|
src?: boolean;
|
||||||
'GEOSITE',
|
validator?: (value: string) => boolean;
|
||||||
'IP-CIDR',
|
}>([
|
||||||
'IP-CIDR6',
|
["DOMAIN", {
|
||||||
'IP-SUFFIX',
|
name: "DOMAIN",
|
||||||
'IP-ASN',
|
example: "example.com",
|
||||||
'GEOIP',
|
}],
|
||||||
'SRC-GEOIP',
|
["DOMAIN-SUFFIX", {
|
||||||
'SRC-IP-ASN',
|
name: "DOMAIN-SUFFIX",
|
||||||
'SRC-IP-CIDR',
|
example: "example.com",
|
||||||
'SRC-IP-SUFFIX',
|
}],
|
||||||
'DST-PORT',
|
["DOMAIN-KEYWORD", {
|
||||||
'SRC-PORT',
|
name: "DOMAIN-KEYWORD",
|
||||||
'IN-PORT',
|
example: "example",
|
||||||
'IN-TYPE',
|
}],
|
||||||
'IN-USER',
|
["DOMAIN-REGEX", {
|
||||||
'IN-NAME',
|
name: "DOMAIN-REGEX",
|
||||||
'PROCESS-PATH',
|
example: "example.*",
|
||||||
'PROCESS-PATH-REGEX',
|
}],
|
||||||
'PROCESS-NAME',
|
["GEOSITE", {
|
||||||
'PROCESS-NAME-REGEX',
|
name: "GEOSITE",
|
||||||
'UID',
|
example: "youtube",
|
||||||
'NETWORK',
|
}],
|
||||||
'DSCP',
|
["GEOIP", {
|
||||||
'RULE-SET',
|
name: "GEOIP",
|
||||||
'AND',
|
example: "CN",
|
||||||
'OR',
|
noResolve: true,
|
||||||
'NOT',
|
src: true,
|
||||||
'SUB-RULE',
|
}],
|
||||||
'MATCH'
|
["SRC-GEOIP", {
|
||||||
]
|
name: "SRC-GEOIP",
|
||||||
|
example: "CN",
|
||||||
|
}],
|
||||||
|
["IP-ASN", {
|
||||||
|
name: "IP-ASN",
|
||||||
|
example: "13335",
|
||||||
|
noResolve: true,
|
||||||
|
src: true,
|
||||||
|
validator: (value) => (+value ? true : false),
|
||||||
|
}],
|
||||||
|
["SRC-IP-ASN", {
|
||||||
|
name: "SRC-IP-ASN",
|
||||||
|
example: "9808",
|
||||||
|
validator: (value) => (+value ? true : false),
|
||||||
|
}],
|
||||||
|
["IP-CIDR", {
|
||||||
|
name: "IP-CIDR",
|
||||||
|
example: "127.0.0.0/8",
|
||||||
|
noResolve: true,
|
||||||
|
src: true,
|
||||||
|
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
||||||
|
}],
|
||||||
|
["IP-CIDR6", {
|
||||||
|
name: "IP-CIDR6",
|
||||||
|
example: "2620:0:2d0:200::7/32",
|
||||||
|
noResolve: true,
|
||||||
|
src: true,
|
||||||
|
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
||||||
|
}],
|
||||||
|
["SRC-IP-CIDR", {
|
||||||
|
name: "SRC-IP-CIDR",
|
||||||
|
example: "192.168.1.201/32",
|
||||||
|
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
||||||
|
}],
|
||||||
|
["IP-SUFFIX", {
|
||||||
|
name: "IP-SUFFIX",
|
||||||
|
example: "8.8.8.8/24",
|
||||||
|
noResolve: true,
|
||||||
|
src: true,
|
||||||
|
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
||||||
|
}],
|
||||||
|
["SRC-IP-SUFFIX", {
|
||||||
|
name: "SRC-IP-SUFFIX",
|
||||||
|
example: "192.168.1.201/8",
|
||||||
|
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
||||||
|
}],
|
||||||
|
["SRC-PORT", {
|
||||||
|
name: "SRC-PORT",
|
||||||
|
example: "7777",
|
||||||
|
validator: (value) => portValidator(value),
|
||||||
|
}],
|
||||||
|
["DST-PORT", {
|
||||||
|
name: "DST-PORT",
|
||||||
|
example: "80",
|
||||||
|
validator: (value) => portValidator(value),
|
||||||
|
}],
|
||||||
|
["IN-PORT", {
|
||||||
|
name: "IN-PORT",
|
||||||
|
example: "7897",
|
||||||
|
validator: (value) => portValidator(value),
|
||||||
|
}],
|
||||||
|
["DSCP", {
|
||||||
|
name: "DSCP",
|
||||||
|
example: "4",
|
||||||
|
}],
|
||||||
|
["PROCESS-NAME", {
|
||||||
|
name: "PROCESS-NAME",
|
||||||
|
example: platform === "win32" ? "chrome.exe" : "curl",
|
||||||
|
}],
|
||||||
|
["PROCESS-PATH", {
|
||||||
|
name: "PROCESS-PATH",
|
||||||
|
example:
|
||||||
|
platform === "win32"
|
||||||
|
? "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
|
||||||
|
: "/usr/bin/wget",
|
||||||
|
}],
|
||||||
|
["PROCESS-NAME-REGEX", {
|
||||||
|
name: "PROCESS-NAME-REGEX",
|
||||||
|
example: ".*telegram.*",
|
||||||
|
}],
|
||||||
|
["PROCESS-PATH-REGEX", {
|
||||||
|
name: "PROCESS-PATH-REGEX",
|
||||||
|
example:
|
||||||
|
platform === "win32" ? "(?i).*Application\\chrome.*" : ".*bin/wget",
|
||||||
|
}],
|
||||||
|
["NETWORK", {
|
||||||
|
name: "NETWORK",
|
||||||
|
example: "udp",
|
||||||
|
validator: (value) => ["tcp", "udp"].includes(value),
|
||||||
|
}],
|
||||||
|
["UID", {
|
||||||
|
name: "UID",
|
||||||
|
example: "1001",
|
||||||
|
validator: (value) => (+value ? true : false),
|
||||||
|
}],
|
||||||
|
["IN-TYPE", {
|
||||||
|
name: "IN-TYPE",
|
||||||
|
example: "SOCKS/HTTP",
|
||||||
|
}],
|
||||||
|
["IN-USER", {
|
||||||
|
name: "IN-USER",
|
||||||
|
example: "mihomo",
|
||||||
|
}],
|
||||||
|
["IN-NAME", {
|
||||||
|
name: "IN-NAME",
|
||||||
|
example: "ss",
|
||||||
|
}],
|
||||||
|
["SUB-RULE", {
|
||||||
|
name: "SUB-RULE",
|
||||||
|
example: "(NETWORK,tcp)",
|
||||||
|
}],
|
||||||
|
["RULE-SET", {
|
||||||
|
name: "RULE-SET",
|
||||||
|
example: "providername",
|
||||||
|
noResolve: true,
|
||||||
|
src: true,
|
||||||
|
}],
|
||||||
|
["AND", {
|
||||||
|
name: "AND",
|
||||||
|
example: "((DOMAIN,baidu.com),(NETWORK,UDP))",
|
||||||
|
}],
|
||||||
|
["OR", {
|
||||||
|
name: "OR",
|
||||||
|
example: "((NETWORK,UDP),(DOMAIN,baidu.com))",
|
||||||
|
}],
|
||||||
|
["NOT", {
|
||||||
|
name: "NOT",
|
||||||
|
example: "((DOMAIN,baidu.com))",
|
||||||
|
}],
|
||||||
|
["MATCH", {
|
||||||
|
name: "MATCH",
|
||||||
|
required: false,
|
||||||
|
}],
|
||||||
|
]);
|
||||||
|
|
||||||
// 支持 no-resolve 参数的规则类型
|
const ruleTypes = Array.from(ruleDefinitionsMap.keys());
|
||||||
const noResolveSupportTypes = [
|
|
||||||
'IP-CIDR',
|
|
||||||
'IP-CIDR6',
|
|
||||||
'IP-SUFFIX',
|
|
||||||
'IP-ASN',
|
|
||||||
'GEOIP'
|
|
||||||
]
|
|
||||||
|
|
||||||
// 支持 src 参数的规则类型
|
const isRuleSupportsNoResolve = (ruleType: string): boolean => {
|
||||||
const srcSupportTypes = [
|
const rule = ruleDefinitionsMap.get(ruleType);
|
||||||
'IP-CIDR',
|
return rule?.noResolve === true;
|
||||||
'IP-CIDR6',
|
};
|
||||||
'IP-SUFFIX',
|
|
||||||
'IP-ASN',
|
const isRuleSupportsSrc = (ruleType: string): boolean => {
|
||||||
'GEOIP'
|
const rule = ruleDefinitionsMap.get(ruleType);
|
||||||
]
|
return rule?.src === true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRuleExample = (ruleType: string): string => {
|
||||||
|
const rule = ruleDefinitionsMap.get(ruleType);
|
||||||
|
return rule?.example || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const isAddRuleDisabled = (newRule: RuleItem, validateRulePayload: (ruleType: string, payload: string) => boolean): boolean => {
|
||||||
|
return (!(newRule.payload.trim() || newRule.type === 'MATCH')) || !newRule.type ||
|
||||||
|
(newRule.type !== 'MATCH' && newRule.payload.trim() !== '' && !validateRulePayload(newRule.type, newRule.payload));
|
||||||
|
};
|
||||||
|
|
||||||
const EditRulesModal: React.FC<Props> = (props) => {
|
const EditRulesModal: React.FC<Props> = (props) => {
|
||||||
const { id, onClose } = props
|
const { id, onClose } = props
|
||||||
@ -262,8 +418,8 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleRuleTypeChange = (selected: string): void => {
|
const handleRuleTypeChange = (selected: string): void => {
|
||||||
const noResolveSupported = noResolveSupportTypes.includes(selected);
|
const noResolveSupported = isRuleSupportsNoResolve(selected);
|
||||||
const srcSupported = srcSupportTypes.includes(selected);
|
const srcSupported = isRuleSupportsSrc(selected);
|
||||||
|
|
||||||
let additionalParams = [...(newRule.additionalParams || [])];
|
let additionalParams = [...(newRule.additionalParams || [])];
|
||||||
if (!noResolveSupported) {
|
if (!noResolveSupported) {
|
||||||
@ -298,7 +454,12 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAddRule = (position: 'prepend' | 'append' = 'append'): void => {
|
const handleAddRule = (position: 'prepend' | 'append' = 'append'): void => {
|
||||||
if (newRule.payload.trim() !== '' || newRule.type === 'MATCH') {
|
if (newRule.type === 'MATCH' || newRule.payload.trim() !== '') {
|
||||||
|
if (newRule.type !== 'MATCH' && newRule.payload.trim() !== '' && !validateRulePayload(newRule.type, newRule.payload)) {
|
||||||
|
alert(t('profiles.editRules.invalidPayload') + ': ' + getRuleExample(newRule.type));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newRuleItem = { ...newRule };
|
const newRuleItem = { ...newRule };
|
||||||
let updatedRules: RuleItem[];
|
let updatedRules: RuleItem[];
|
||||||
|
|
||||||
@ -372,6 +533,24 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const validateRulePayload = (ruleType: string, payload: string): boolean => {
|
||||||
|
if (ruleType === 'MATCH') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validator = getRuleValidator(ruleType);
|
||||||
|
if (!validator) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validator(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRuleValidator = (ruleType: string): ((value: string) => boolean) | undefined => {
|
||||||
|
const rule = ruleDefinitionsMap.get(ruleType);
|
||||||
|
return rule?.validator;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
backdrop="blur"
|
backdrop="blur"
|
||||||
@ -410,10 +589,11 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
<Input
|
<Input
|
||||||
label={t('profiles.editRules.payload')}
|
label={t('profiles.editRules.payload')}
|
||||||
placeholder={t('profiles.editRules.payloadPlaceholder')}
|
placeholder={getRuleExample(newRule.type) || t('profiles.editRules.payloadPlaceholder')}
|
||||||
value={newRule.payload}
|
value={newRule.payload}
|
||||||
onValueChange={(value) => setNewRule({ ...newRule, payload: value })}
|
onValueChange={(value) => setNewRule({ ...newRule, payload: value })}
|
||||||
isDisabled={newRule.type === 'MATCH'}
|
isDisabled={newRule.type === 'MATCH'}
|
||||||
|
color={newRule.payload && newRule.type !== 'MATCH' && !validateRulePayload(newRule.type, newRule.payload) ? "danger" : "default"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
@ -432,11 +612,11 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
</Autocomplete>
|
</Autocomplete>
|
||||||
|
|
||||||
{/* 附加参数 */}
|
{/* 附加参数 */}
|
||||||
{(noResolveSupportTypes.includes(newRule.type) || srcSupportTypes.includes(newRule.type)) && (
|
{(isRuleSupportsNoResolve(newRule.type) || isRuleSupportsSrc(newRule.type)) && (
|
||||||
<>
|
<>
|
||||||
<Divider className="my-2" />
|
<Divider className="my-2" />
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{noResolveSupportTypes.includes(newRule.type) && (
|
{isRuleSupportsNoResolve(newRule.type) && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
isSelected={newRule.additionalParams?.includes('no-resolve') || false}
|
isSelected={newRule.additionalParams?.includes('no-resolve') || false}
|
||||||
onValueChange={(checked) => handleAdditionalParamChange('no-resolve', checked)}
|
onValueChange={(checked) => handleAdditionalParamChange('no-resolve', checked)}
|
||||||
@ -444,7 +624,7 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
{t('profiles.editRules.noResolve')}
|
{t('profiles.editRules.noResolve')}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
)}
|
)}
|
||||||
{srcSupportTypes.includes(newRule.type) && (
|
{isRuleSupportsSrc(newRule.type) && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
isSelected={newRule.additionalParams?.includes('src') || false}
|
isSelected={newRule.additionalParams?.includes('src') || false}
|
||||||
onValueChange={(checked) => handleAdditionalParamChange('src', checked)}
|
onValueChange={(checked) => handleAdditionalParamChange('src', checked)}
|
||||||
@ -460,7 +640,7 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
onPress={() => handleAddRule('prepend')}
|
onPress={() => handleAddRule('prepend')}
|
||||||
isDisabled={!(newRule.payload.trim() || newRule.type === 'MATCH') || !newRule.type}
|
isDisabled={isAddRuleDisabled(newRule, validateRulePayload)}
|
||||||
startContent={<MdVerticalAlignTop className="text-lg" />}
|
startContent={<MdVerticalAlignTop className="text-lg" />}
|
||||||
>
|
>
|
||||||
{t('profiles.editRules.addRulePrepend')}
|
{t('profiles.editRules.addRulePrepend')}
|
||||||
@ -469,7 +649,7 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
color="primary"
|
color="primary"
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
onPress={() => handleAddRule('append')}
|
onPress={() => handleAddRule('append')}
|
||||||
isDisabled={!(newRule.payload.trim() || newRule.type === 'MATCH') || !newRule.type}
|
isDisabled={isAddRuleDisabled(newRule, validateRulePayload)}
|
||||||
startContent={<MdVerticalAlignBottom className="text-lg" />}
|
startContent={<MdVerticalAlignBottom className="text-lg" />}
|
||||||
>
|
>
|
||||||
{t('profiles.editRules.addRuleAppend')}
|
{t('profiles.editRules.addRuleAppend')}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user