import { ReactNode, useEffect, useState } from "react"; import { useLockFn } from "ahooks"; import yaml from "js-yaml"; import { useTranslation } from "react-i18next"; import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragEndEvent, } from "@dnd-kit/core"; import { SortableContext, sortableKeyboardCoordinates, } from "@dnd-kit/sortable"; import { Autocomplete, Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, List, ListItem, ListItemText, TextField, styled, } from "@mui/material"; import { readProfileFile, saveProfileFile } from "@/services/cmds"; import { Notice, Switch } from "@/components/base"; import getSystem from "@/utils/get-system"; import { RuleItem } from "@/components/profile/rule-item"; import { BaseSearchBox } from "../base/base-search-box"; interface Props { profileUid: string; title?: string | ReactNode; property: string; open: boolean; onClose: () => void; onChange?: (prev?: string, curr?: string) => void; } 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); }; const rules: { name: string; required?: boolean; example?: string; noResolve?: boolean; validator?: (value: string) => boolean; }[] = [ { name: "DOMAIN", example: "example.com", }, { name: "DOMAIN-SUFFIX", example: "example.com", }, { name: "DOMAIN-KEYWORD", example: "example", }, { name: "DOMAIN-REGEX", example: "example.*", }, { name: "GEOSITE", example: "youtube", }, { name: "GEOIP", example: "CN", noResolve: true, }, { name: "SRC-GEOIP", example: "CN", }, { name: "IP-ASN", example: "13335", noResolve: true, validator: (value) => (+value ? true : false), }, { name: "SRC-IP-ASN", example: "9808", validator: (value) => (+value ? true : false), }, { name: "IP-CIDR", example: "127.0.0.0/8", noResolve: true, validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), }, { name: "IP-CIDR6", example: "2620:0:2d0:200::7/32", noResolve: true, validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), }, { name: "SRC-IP-CIDR", example: "192.168.1.201/32", validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), }, { name: "IP-SUFFIX", example: "8.8.8.8/24", noResolve: true, validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), }, { name: "SRC-IP-SUFFIX", example: "192.168.1.201/8", validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), }, { name: "SRC-PORT", example: "7777", validator: (value) => portValidator(value), }, { name: "DST-PORT", example: "80", validator: (value) => portValidator(value), }, { name: "IN-PORT", example: "7890", validator: (value) => portValidator(value), }, { name: "DSCP", example: "4", }, { name: "PROCESS-NAME", example: getSystem() === "windows" ? "chrome.exe" : "curl", }, { name: "PROCESS-PATH", example: getSystem() === "windows" ? "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" : "/usr/bin/wget", }, { name: "PROCESS-NAME-REGEX", example: ".*telegram.*", }, { name: "PROCESS-PATH-REGEX", example: getSystem() === "windows" ? "(?i).*Application\\chrome.*" : ".*bin/wget", }, { name: "NETWORK", example: "udp", validator: (value) => ["tcp", "udp"].includes(value), }, { name: "UID", example: "1001", validator: (value) => (+value ? true : false), }, { name: "IN-TYPE", example: "SOCKS/HTTP", }, { name: "IN-USER", example: "mihomo", }, { name: "IN-NAME", example: "ss", }, { name: "SUB-RULE", example: "(NETWORK,tcp)", }, { name: "RULE-SET", example: "providername", noResolve: true, }, { name: "AND", example: "((DOMAIN,baidu.com),(NETWORK,UDP))", }, { name: "OR", example: "((NETWORK,UDP),(DOMAIN,baidu.com))", }, { name: "NOT", example: "((DOMAIN,baidu.com))", }, { name: "MATCH", required: false, }, ]; const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"]; export const RulesEditorViewer = (props: Props) => { const { title, profileUid, property, open, onClose, onChange } = props; const { t } = useTranslation(); const [prevData, setPrevData] = useState(""); const [match, setMatch] = useState(() => (_: string) => true); const [ruleType, setRuleType] = useState<(typeof rules)[number]>(rules[0]); const [ruleContent, setRuleContent] = useState(""); const [noResolve, setNoResolve] = useState(false); const [proxyPolicy, setProxyPolicy] = useState(builtinProxyPolicies[0]); const [proxyPolicyList, setProxyPolicyList] = useState([]); const [ruleList, setRuleList] = useState([]); const [ruleSetList, setRuleSetList] = useState([]); const [subRuleList, setSubRuleList] = useState([]); const [prependSeq, setPrependSeq] = useState([]); const [appendSeq, setAppendSeq] = useState([]); const [deleteSeq, setDeleteSeq] = useState([]); const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ); const reorder = (list: string[], startIndex: number, endIndex: number) => { const result = Array.from(list); const [removed] = result.splice(startIndex, 1); result.splice(endIndex, 0, removed); return result; }; const onPrependDragEnd = async (event: DragEndEvent) => { const { active, over } = event; if (over) { if (active.id !== over.id) { let activeIndex = prependSeq.indexOf(active.id.toString()); let overIndex = prependSeq.indexOf(over.id.toString()); setPrependSeq(reorder(prependSeq, activeIndex, overIndex)); } } }; const onAppendDragEnd = async (event: DragEndEvent) => { const { active, over } = event; if (over) { if (active.id !== over.id) { let activeIndex = appendSeq.indexOf(active.id.toString()); let overIndex = appendSeq.indexOf(over.id.toString()); setAppendSeq(reorder(appendSeq, activeIndex, overIndex)); } } }; const fetchContent = async () => { let data = await readProfileFile(property); let obj = yaml.load(data) as { prepend: []; append: []; delete: [] }; setPrependSeq(obj.prepend || []); setAppendSeq(obj.append || []); setDeleteSeq(obj.delete || []); setPrevData(data); }; const fetchProfile = async () => { let data = await readProfileFile(profileUid); let groupsObj = yaml.load(data) as { "proxy-groups": [] }; let rulesObj = yaml.load(data) as { rules: [] }; let ruleSetObj = yaml.load(data) as { "rule-providers": [] }; let subRuleObj = yaml.load(data) as { "sub-rules": [] }; setProxyPolicyList( builtinProxyPolicies.concat( groupsObj["proxy-groups"] ? groupsObj["proxy-groups"].map((item: any) => item.name) : [] ) ); setRuleList(rulesObj.rules || []); setRuleSetList( ruleSetObj["rule-providers"] ? Object.keys(ruleSetObj["rule-providers"]) : [] ); setSubRuleList( subRuleObj["sub-rules"] ? Object.keys(subRuleObj["sub-rules"]) : [] ); }; useEffect(() => { fetchContent(); fetchProfile(); }, [open]); const validateRule = () => { if ((ruleType.required ?? true) && !ruleContent) { throw new Error(t("Rule Condition Required")); } if (ruleType.validator && !ruleType.validator(ruleContent)) { throw new Error(t("Invalid Rule")); } return `${ruleType.name}${ ruleContent ? "," + ruleContent : "" },${proxyPolicy}${ruleType.noResolve && noResolve ? ",no-resolve" : ""}`; }; const onSave = useLockFn(async () => { try { let currData = yaml.dump({ prepend: prependSeq, append: appendSeq, delete: deleteSeq, }); await saveProfileFile(property, currData); onChange?.(prevData, currData); onClose(); } catch (err: any) { Notice.error(err.message || err.toString()); } }); return ( {title ?? t("Edit Rules")}
} options={rules} value={ruleType} getOptionLabel={(option) => option.name} renderOption={(props, option) => (
  • {option.name}
  • )} onChange={(_, value) => value && setRuleType(value)} />
    {ruleType.name === "RULE-SET" && ( } options={ruleSetList} value={ruleContent} onChange={(_, value) => value && setRuleContent(value)} /> )} {ruleType.name === "SUB-RULE" && ( } options={subRuleList} value={ruleContent} onChange={(_, value) => value && setRuleContent(value)} /> )} {ruleType.name !== "RULE-SET" && ruleType.name !== "SUB-RULE" && ( setRuleContent(e.target.value)} /> )} } options={proxyPolicyList} value={proxyPolicy} renderOption={(props, option) => (
  • {option}
  • )} onChange={(_, value) => value && setProxyPolicy(value)} />
    {ruleType.noResolve && ( setNoResolve(!noResolve)} /> )}
    setMatch(() => match)} />
    {prependSeq.length > 0 && ( { return x; })} > {prependSeq.map((item, index) => { return ( { setPrependSeq(prependSeq.filter((v) => v !== item)); }} /> ); })} )} {ruleList .filter((item) => match(item)) .map((item, index) => { return ( { if (deleteSeq.includes(item)) { setDeleteSeq(deleteSeq.filter((v) => v !== item)); } else { setDeleteSeq([...deleteSeq, item]); } }} /> ); })} {appendSeq.length > 0 && ( { return x; })} > {appendSeq.map((item, index) => { return ( { setAppendSeq(appendSeq.filter((v) => v !== item)); }} /> ); })} )}
    ); }; const Item = styled(ListItem)(() => ({ padding: "5px 2px", }));