diff --git a/.github/update.log b/.github/update.log index 9cef3b3444..049e459977 100644 --- a/.github/update.log +++ b/.github/update.log @@ -1053,3 +1053,4 @@ Update On Sun Jul 6 20:36:31 CEST 2025 Update On Mon Jul 7 20:39:12 CEST 2025 Update On Tue Jul 8 20:39:34 CEST 2025 Update On Wed Jul 9 20:39:55 CEST 2025 +Update On Thu Jul 10 20:38:34 CEST 2025 diff --git a/clash-meta/config/config.go b/clash-meta/config/config.go index c7107d50d4..673883c44c 100644 --- a/clash-meta/config/config.go +++ b/clash-meta/config/config.go @@ -1035,46 +1035,20 @@ func parseRules(rulesConfig []string, proxies map[string]C.Proxy, ruleProviders // parse rules for idx, line := range rulesConfig { - rule := trimArr(strings.Split(line, ",")) - var ( - payload string - target string - params []string - ruleName = strings.ToUpper(rule[0]) - ) - - l := len(rule) - - if ruleName == "NOT" || ruleName == "OR" || ruleName == "AND" || ruleName == "SUB-RULE" || ruleName == "DOMAIN-REGEX" || ruleName == "PROCESS-NAME-REGEX" || ruleName == "PROCESS-PATH-REGEX" { - target = rule[l-1] - payload = strings.Join(rule[1:l-1], ",") - } else { - if l < 2 { - return nil, fmt.Errorf("%s[%d] [%s] error: format invalid", format, idx, line) - } - if l < 4 { - rule = append(rule, make([]string, 4-l)...) - } - if ruleName == "MATCH" { - l = 2 - } - if l >= 3 { - l = 3 - payload = rule[1] - } - target = rule[l-1] - params = rule[l:] + tp, payload, target, params := RC.ParseRulePayload(line, true) + if target == "" { + return nil, fmt.Errorf("%s[%d] [%s] error: format invalid", format, idx, line) } + if _, ok := proxies[target]; !ok { - if ruleName != "SUB-RULE" { + if tp != "SUB-RULE" { return nil, fmt.Errorf("%s[%d] [%s] error: proxy [%s] not found", format, idx, line, target) } else if _, ok = subRules[target]; !ok { return nil, fmt.Errorf("%s[%d] [%s] error: sub-rule [%s] not found", format, idx, line, target) } } - params = trimArr(params) - parsed, parseErr := R.ParseRule(ruleName, payload, target, params, subRules) + parsed, parseErr := R.ParseRule(tp, payload, target, params, subRules) if parseErr != nil { return nil, fmt.Errorf("%s[%d] [%s] error: %s", format, idx, line, parseErr.Error()) } diff --git a/clash-meta/config/utils.go b/clash-meta/config/utils.go index 8ce3e8b896..c72c120df7 100644 --- a/clash-meta/config/utils.go +++ b/clash-meta/config/utils.go @@ -6,19 +6,11 @@ import ( "net/netip" "os" "strconv" - "strings" "github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/common/structure" ) -func trimArr(arr []string) (r []string) { - for _, e := range arr { - r = append(r, strings.Trim(e, " ")) - } - return -} - // Check if ProxyGroups form DAG(Directed Acyclic Graph), and sort all ProxyGroups by dependency order. // Meanwhile, record the original index in the config file. // If loop is detected, return an error with location of loop. diff --git a/clash-meta/rules/common/base.go b/clash-meta/rules/common/base.go index ab53753ed2..33ab5a1f3a 100644 --- a/clash-meta/rules/common/base.go +++ b/clash-meta/rules/common/base.go @@ -34,22 +34,48 @@ func ParseParams(params []string) (isSrc bool, noResolve bool) { return } -func ParseRulePayload(ruleRaw string) (string, string, []string) { - item := strings.Split(ruleRaw, ",") - if len(item) == 1 { - return "", item[0], nil - } else if len(item) == 2 { - return item[0], item[1], nil - } else if len(item) > 2 { - // keep in sync with config/config.go [parseRules] - if item[0] == "NOT" || item[0] == "OR" || item[0] == "AND" || item[0] == "SUB-RULE" || item[0] == "DOMAIN-REGEX" || item[0] == "PROCESS-NAME-REGEX" || item[0] == "PROCESS-PATH-REGEX" { - return item[0], strings.Join(item[1:], ","), nil - } else { - return item[0], item[1], item[2:] +func trimArr(arr []string) (r []string) { + for _, e := range arr { + r = append(r, strings.Trim(e, " ")) + } + return +} + +// ParseRulePayload parse rule format like: +// `tp,payload,target(,params...)` or `tp,payload(,params...)` +// needTarget control the format contains `target` in string +func ParseRulePayload(ruleRaw string, needTarget bool) (tp, payload, target string, params []string) { + item := trimArr(strings.Split(ruleRaw, ",")) + tp = strings.ToUpper(item[0]) + if len(item) > 1 { + switch tp { + case "MATCH": + // MATCH doesn't contain payload and params + target = item[1] + case "NOT", "OR", "AND", "SUB-RULE", "DOMAIN-REGEX", "PROCESS-NAME-REGEX", "PROCESS-PATH-REGEX": + // some type of rules that has comma in payload and don't need params + if needTarget { + l := len(item) + target = item[l-1] // don't have params so target must at the end of slices + item = item[:l-1] // remove the target from slices + } + payload = strings.Join(item[1:], ",") + default: + payload = item[1] + if len(item) > 2 { + if needTarget { + target = item[2] + if len(item) > 3 { + params = item[3:] + } + } else { + params = item[2:] + } + } } } - return "", "", nil + return } type ParseRuleFunc func(tp, payload, target string, params []string, subRules map[string][]C.Rule) (C.Rule, error) diff --git a/clash-meta/rules/logic/logic.go b/clash-meta/rules/logic/logic.go index c740d8deca..e4f3817e6d 100644 --- a/clash-meta/rules/logic/logic.go +++ b/clash-meta/rules/logic/logic.go @@ -78,14 +78,14 @@ func (r Range) containRange(preStart, preEnd int) bool { } func (logic *Logic) payloadToRule(subPayload string, parseRule common.ParseRuleFunc) (C.Rule, error) { - tp, payload, param := common.ParseRulePayload(subPayload) + tp, payload, target, param := common.ParseRulePayload(subPayload, false) switch tp { case "MATCH", "SUB-RULE": return nil, fmt.Errorf("unsupported rule type [%s] on logic rule", tp) case "": return nil, fmt.Errorf("[%s] format is error", subPayload) } - return parseRule(tp, payload, "", param, nil) + return parseRule(tp, payload, target, param, nil) } func (logic *Logic) format(payload string) ([]Range, error) { diff --git a/clash-meta/rules/parser.go b/clash-meta/rules/parser.go index 675c52ec09..6d8b3b8ea9 100644 --- a/clash-meta/rules/parser.go +++ b/clash-meta/rules/parser.go @@ -10,6 +10,10 @@ import ( ) func ParseRule(tp, payload, target string, params []string, subRules map[string][]C.Rule) (parsed C.Rule, parseErr error) { + if tp != "MATCH" && payload == "" { // only MATCH allowed doesn't contain payload + return nil, fmt.Errorf("missing subsequent parameters: %s", tp) + } + switch tp { case "DOMAIN": parsed = RC.NewDomain(payload, target) @@ -83,8 +87,6 @@ func ParseRule(tp, payload, target string, params []string, subRules map[string] case "MATCH": parsed = RC.NewMatch(target) parseErr = nil - case "": - parseErr = fmt.Errorf("missing subsequent parameters: %s", payload) default: parseErr = fmt.Errorf("unsupported rule type: %s", tp) } diff --git a/clash-meta/rules/provider/classical_strategy.go b/clash-meta/rules/provider/classical_strategy.go index 2505b3018b..497f916813 100644 --- a/clash-meta/rules/provider/classical_strategy.go +++ b/clash-meta/rules/provider/classical_strategy.go @@ -12,7 +12,7 @@ import ( type classicalStrategy struct { rules []C.Rule count int - parse func(tp, payload, target string, params []string) (parsed C.Rule, parseErr error) + parse common.ParseRuleFunc } func (c *classicalStrategy) Behavior() P.RuleBehavior { @@ -39,25 +39,26 @@ func (c *classicalStrategy) Reset() { } func (c *classicalStrategy) Insert(rule string) { - ruleType, rule, params := common.ParseRulePayload(rule) - r, err := c.parse(ruleType, rule, "", params) + r, err := c.payloadToRule(rule) if err != nil { - log.Warnln("parse classical rule error: %s", err.Error()) + log.Warnln("parse classical rule [%s] error: %s", rule, err.Error()) } else { c.rules = append(c.rules, r) c.count++ } } +func (c *classicalStrategy) payloadToRule(rule string) (C.Rule, error) { + tp, payload, target, params := common.ParseRulePayload(rule, false) + switch tp { + case "MATCH", "RULE-SET", "SUB-RULE": + return nil, fmt.Errorf("unsupported rule type on classical rule-set: %s", tp) + } + return c.parse(tp, payload, target, params, nil) +} + func (c *classicalStrategy) FinishInsert() {} -func NewClassicalStrategy(parse func(tp, payload, target string, params []string, subRules map[string][]C.Rule) (parsed C.Rule, parseErr error)) *classicalStrategy { - return &classicalStrategy{rules: []C.Rule{}, parse: func(tp, payload, target string, params []string) (parsed C.Rule, parseErr error) { - switch tp { - case "MATCH", "RULE-SET", "SUB-RULE": - return nil, fmt.Errorf("unsupported rule type on classical rule-set: %s", tp) - default: - return parse(tp, payload, target, params, nil) - } - }} +func NewClassicalStrategy(parse common.ParseRuleFunc) *classicalStrategy { + return &classicalStrategy{rules: []C.Rule{}, parse: parse} } diff --git a/clash-nyanpasu/backend/Cargo.lock b/clash-nyanpasu/backend/Cargo.lock index f0cf7521af..b7b292787f 100644 --- a/clash-nyanpasu/backend/Cargo.lock +++ b/clash-nyanpasu/backend/Cargo.lock @@ -351,7 +351,7 @@ dependencies = [ "objc2-foundation 0.3.1", "parking_lot", "percent-encoding", - "windows-sys 0.59.0", + "windows-sys 0.52.0", "wl-clipboard-rs", "x11rb", ] @@ -1465,9 +1465,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.40" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" dependencies = [ "clap_builder", "clap_derive", @@ -1475,9 +1475,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.40" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" dependencies = [ "anstream", "anstyle", @@ -1487,9 +1487,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.40" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2819,7 +2819,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -6540,9 +6540,9 @@ dependencies = [ [[package]] name = "oxc_allocator" -version = "0.75.1" +version = "0.76.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5dc7d7719a4f90c691fbd6bcf8b623df2832ccc6b303c1d07d85aeb476c5b8e" +checksum = "905a588c0a12c71ddb94d4eaee6ea3770f9132017956936777763ebcde66d58c" dependencies = [ "allocator-api2", "bumpalo", @@ -6553,9 +6553,9 @@ dependencies = [ [[package]] name = "oxc_ast" -version = "0.75.1" +version = "0.76.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c1308e677a69e31e51537f5b3267aae44a67cd61a2f3a740f815839c89ea97" +checksum = "d203d52b5de3eba556625cac42290ac81f9995c82009b9f6ec041237f068ba9b" dependencies = [ "bitflags 2.9.1", "oxc_allocator", @@ -6569,9 +6569,9 @@ dependencies = [ [[package]] name = "oxc_ast_macros" -version = "0.75.1" +version = "0.76.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e85f4a6422af514f59d0d5aab918237b799ca25164bd59e11b59ea61991777ae" +checksum = "b31a7f8a301f434bd73f7735f02d7d823c2d4a83bd1ce1b04bfd3b0b50c5a054" dependencies = [ "phf 0.12.1", "proc-macro2", @@ -6581,9 +6581,9 @@ dependencies = [ [[package]] name = "oxc_ast_visit" -version = "0.75.1" +version = "0.76.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a8f7472e608b7410cc6ce5b0abb9cac2529a197374e5310f9de567d8853a5d" +checksum = "c353d88dd55b1beb8338a99d908f2d5828f336b82821298d090ff4247c7a9b15" dependencies = [ "oxc_allocator", "oxc_ast", @@ -6593,18 +6593,18 @@ dependencies = [ [[package]] name = "oxc_data_structures" -version = "0.75.1" +version = "0.76.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d713c1b57fe1f55af1d557efd461025c9c9bd28835a6b3e0e91f46cdeb995a6" +checksum = "7087f6bf6910ef41df29d07eb67b163207aafd7efa0801d3b57c2e072c8b9ed1" dependencies = [ "rustversion", ] [[package]] name = "oxc_diagnostics" -version = "0.75.1" +version = "0.76.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c6188d0a1aa83656795c29b7e7f036060f5c3e5c234ebebc2a5899fb43a579" +checksum = "4fe0cbd689125f1510f44235fee96887f1f1f42a3420afc802db60ff654030cf" dependencies = [ "cow-utils", "oxc-miette", @@ -6613,9 +6613,9 @@ dependencies = [ [[package]] name = "oxc_ecmascript" -version = "0.75.1" +version = "0.76.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "851355a2d035526d3a3525cbd66224bde1e234d7cf339835a4516bf27732dcec" +checksum = "2141d6765bc6e75ff286aeb6f580f1ceeea40c48d23d07b943e1fb92367a8968" dependencies = [ "num-bigint", "num-traits", @@ -6626,9 +6626,9 @@ dependencies = [ [[package]] name = "oxc_estree" -version = "0.75.1" +version = "0.76.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d67d74c11e1c8a741c9321c86d2f0b30de107e1cc89d5912a6c5b9771b7e56cd" +checksum = "1216c5ef637573a66b9e4006a6edb432274799bd9e55059607a52aaa6bdbad9a" [[package]] name = "oxc_index" @@ -6638,9 +6638,9 @@ checksum = "2fa07b0cfa997730afed43705766ef27792873fdf5215b1391949fec678d2392" [[package]] name = "oxc_parser" -version = "0.75.1" +version = "0.76.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bbd1f8184ae33834222f9620673c40296b4ba9c4951c54bcc5548bb7a1f2621" +checksum = "55a30d3f398929eaac185d328e43233f857626dedd8be5b8c78bfc0c63a32d7c" dependencies = [ "bitflags 2.9.1", "cow-utils", @@ -6661,9 +6661,9 @@ dependencies = [ [[package]] name = "oxc_regular_expression" -version = "0.75.1" +version = "0.76.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54d16a7346afe27716433d691343ee137d640f94f978bd3244babf7248ed6453" +checksum = "7e40e3de8fc6bb9091ec5d364a951405bca49ada74c5b0250e0cbb33583c9efc" dependencies = [ "bitflags 2.9.1", "oxc_allocator", @@ -6677,9 +6677,9 @@ dependencies = [ [[package]] name = "oxc_span" -version = "0.75.1" +version = "0.76.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93b7cf8b6447a4d0bb9aff91c6c8fcccbae3d28bad9f9ccb1128599761a982b6" +checksum = "e755e211d32f1d52255eebb39dfb8a787620e5d163e3c6bbe3ad8cb0cca75c40" dependencies = [ "compact_str", "oxc-miette", @@ -6690,9 +6690,9 @@ dependencies = [ [[package]] name = "oxc_syntax" -version = "0.75.1" +version = "0.76.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03b6ac136f155820331b326d5cbd178a9c2ee032787f04aa6e14b494c8aeb227" +checksum = "817dd63f7421c08f9cf1fe1463208590cc0cca524bfafa3f252ae519587cdf70" dependencies = [ "bitflags 2.9.1", "cow-utils", @@ -7422,7 +7422,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -8013,7 +8013,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -8026,7 +8026,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -9627,7 +9627,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix 1.0.7", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] diff --git a/clash-nyanpasu/backend/tauri/Cargo.toml b/clash-nyanpasu/backend/tauri/Cargo.toml index 8fea110397..b14d92de31 100644 --- a/clash-nyanpasu/backend/tauri/Cargo.toml +++ b/clash-nyanpasu/backend/tauri/Cargo.toml @@ -172,12 +172,12 @@ display-info = "0.5.0" # should be removed after upgrading to tauri v2 # OXC (The Oxidation Compiler) # We use it to parse and transpile the old script profile to esm based script profile -oxc_parser = "0.75" -oxc_allocator = "0.75" -oxc_span = "0.75" -oxc_ast = "0.75" -oxc_syntax = "0.75" -oxc_ast_visit = "0.75" +oxc_parser = "0.76" +oxc_allocator = "0.76" +oxc_span = "0.76" +oxc_ast = "0.76" +oxc_syntax = "0.76" +oxc_ast_visit = "0.76" # Lua Integration mlua = { version = "0.10", features = [ diff --git a/clash-nyanpasu/frontend/interface/package.json b/clash-nyanpasu/frontend/interface/package.json index ed056435ad..82b7fa1a5a 100644 --- a/clash-nyanpasu/frontend/interface/package.json +++ b/clash-nyanpasu/frontend/interface/package.json @@ -11,7 +11,7 @@ "build": "tsc" }, "dependencies": { - "@tanstack/react-query": "5.81.5", + "@tanstack/react-query": "5.82.0", "@tauri-apps/api": "2.5.0", "ahooks": "3.9.0", "dayjs": "1.11.13", diff --git a/clash-nyanpasu/frontend/nyanpasu/package.json b/clash-nyanpasu/frontend/nyanpasu/package.json index 1253ce46fe..c2b357519c 100644 --- a/clash-nyanpasu/frontend/nyanpasu/package.json +++ b/clash-nyanpasu/frontend/nyanpasu/package.json @@ -55,9 +55,9 @@ "@csstools/normalize.css": "12.1.1", "@emotion/babel-plugin": "11.13.5", "@emotion/react": "11.14.0", - "@iconify/json": "2.2.356", + "@iconify/json": "2.2.357", "@monaco-editor/react": "4.7.0", - "@tanstack/react-query": "5.81.5", + "@tanstack/react-query": "5.82.0", "@tanstack/react-router": "1.125.6", "@tanstack/react-router-devtools": "1.125.6", "@tanstack/router-plugin": "1.125.6", diff --git a/clash-nyanpasu/package.json b/clash-nyanpasu/package.json index c202bc66c0..0f4df8b384 100644 --- a/clash-nyanpasu/package.json +++ b/clash-nyanpasu/package.json @@ -93,7 +93,7 @@ "postcss-import": "16.1.1", "postcss-scss": "4.0.9", "prettier": "3.6.2", - "prettier-plugin-tailwindcss": "0.6.13", + "prettier-plugin-tailwindcss": "0.6.14", "prettier-plugin-toml": "2.0.5", "react-devtools": "6.1.5", "stylelint": "16.21.1", diff --git a/clash-nyanpasu/pnpm-lock.yaml b/clash-nyanpasu/pnpm-lock.yaml index 4a98519b88..5ef36e9ce3 100644 --- a/clash-nyanpasu/pnpm-lock.yaml +++ b/clash-nyanpasu/pnpm-lock.yaml @@ -128,8 +128,8 @@ importers: specifier: 3.6.2 version: 3.6.2 prettier-plugin-tailwindcss: - specifier: 0.6.13 - version: 0.6.13(@ianvs/prettier-plugin-sort-imports@4.4.2(prettier@3.6.2))(@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.6.2))(prettier@3.6.2) + specifier: 0.6.14 + version: 0.6.14(@ianvs/prettier-plugin-sort-imports@4.4.2(prettier@3.6.2))(@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.6.2))(prettier@3.6.2) prettier-plugin-toml: specifier: 2.0.5 version: 2.0.5(prettier@3.6.2) @@ -173,8 +173,8 @@ importers: frontend/interface: dependencies: '@tanstack/react-query': - specifier: 5.81.5 - version: 5.81.5(react@19.1.0) + specifier: 5.82.0 + version: 5.82.0(react@19.1.0) '@tauri-apps/api': specifier: 2.5.0 version: 2.5.0 @@ -337,14 +337,14 @@ importers: specifier: 11.14.0 version: 11.14.0(@types/react@19.1.8)(react@19.1.0) '@iconify/json': - specifier: 2.2.356 - version: 2.2.356 + specifier: 2.2.357 + version: 2.2.357 '@monaco-editor/react': specifier: 4.7.0 version: 4.7.0(monaco-editor@0.52.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-query': - specifier: 5.81.5 - version: 5.81.5(react@19.1.0) + specifier: 5.82.0 + version: 5.82.0(react@19.1.0) '@tanstack/react-router': specifier: 1.125.6 version: 1.125.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -1774,8 +1774,8 @@ packages: '@vue/compiler-sfc': optional: true - '@iconify/json@2.2.356': - resolution: {integrity: sha512-UVUnPLu154x8oa4GFNU3SPKkKcNbVpdNBLAGDhQFPVin2kf6dK6146WK5vDZsxs+366Mfdy8ZDkbatDqjlTcPw==} + '@iconify/json@2.2.357': + resolution: {integrity: sha512-v8fr/KwcJ0qsoEJ69k1+M928bfzNmmApyJBTIAwwIzHZrVEUneHTEOJRy7OVYKisauBMVVH067I2uFNoPA92iA==} '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} @@ -2925,11 +2925,11 @@ packages: resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} engines: {node: '>=12'} - '@tanstack/query-core@5.81.5': - resolution: {integrity: sha512-ZJOgCy/z2qpZXWaj/oxvodDx07XcQa9BF92c0oINjHkoqUPsmm3uG08HpTaviviZ/N9eP1f9CM7mKSEkIo7O1Q==} + '@tanstack/query-core@5.82.0': + resolution: {integrity: sha512-JrjoVuaajBQtnoWSg8iaPHaT4mW73lK2t+exxHNOSMqy0+13eKLqJgTKXKImLejQIfdAHQ6Un0njEhOvUtOd5w==} - '@tanstack/react-query@5.81.5': - resolution: {integrity: sha512-lOf2KqRRiYWpQT86eeeftAGnjuTR35myTP8MXyvHa81VlomoAWNEd8x5vkcAfQefu0qtYCvyqLropFZqgI2EQw==} + '@tanstack/react-query@5.82.0': + resolution: {integrity: sha512-mnk8/ofKEthFeMdhV1dV8YXRf+9HqvXAcciXkoo755d/ocfWq7N/Y9jGOzS3h7ZW9dDGwSIhs3/HANWUBsyqYg==} peerDependencies: react: ^18 || ^19 @@ -6913,11 +6913,13 @@ packages: resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} engines: {node: '>=6.0.0'} - prettier-plugin-tailwindcss@0.6.13: - resolution: {integrity: sha512-uQ0asli1+ic8xrrSmIOaElDu0FacR4x69GynTh2oZjFY10JUt6EEumTQl5tB4fMeD6I1naKd+4rXQQ7esT2i1g==} + prettier-plugin-tailwindcss@0.6.14: + resolution: {integrity: sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==} engines: {node: '>=14.21.3'} peerDependencies: '@ianvs/prettier-plugin-sort-imports': '*' + '@prettier/plugin-hermes': '*' + '@prettier/plugin-oxc': '*' '@prettier/plugin-pug': '*' '@shopify/prettier-plugin-liquid': '*' '@trivago/prettier-plugin-sort-imports': '*' @@ -6937,6 +6939,10 @@ packages: peerDependenciesMeta: '@ianvs/prettier-plugin-sort-imports': optional: true + '@prettier/plugin-hermes': + optional: true + '@prettier/plugin-oxc': + optional: true '@prettier/plugin-pug': optional: true '@shopify/prettier-plugin-liquid': @@ -10026,7 +10032,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@iconify/json@2.2.356': + '@iconify/json@2.2.357': dependencies: '@iconify/types': 2.0.0 pathe: 1.1.2 @@ -11123,11 +11129,11 @@ snapshots: dependencies: remove-accents: 0.5.0 - '@tanstack/query-core@5.81.5': {} + '@tanstack/query-core@5.82.0': {} - '@tanstack/react-query@5.81.5(react@19.1.0)': + '@tanstack/react-query@5.82.0(react@19.1.0)': dependencies: - '@tanstack/query-core': 5.81.5 + '@tanstack/query-core': 5.82.0 react: 19.1.0 '@tanstack/react-router-devtools@1.125.6(@tanstack/react-router@1.125.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@tanstack/router-core@1.125.4)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.5)(tiny-invariant@1.3.3)': @@ -15679,7 +15685,7 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier-plugin-tailwindcss@0.6.13(@ianvs/prettier-plugin-sort-imports@4.4.2(prettier@3.6.2))(@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.6.2))(prettier@3.6.2): + prettier-plugin-tailwindcss@0.6.14(@ianvs/prettier-plugin-sort-imports@4.4.2(prettier@3.6.2))(@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.6.2))(prettier@3.6.2): dependencies: prettier: 3.6.2 optionalDependencies: diff --git a/clash-verge-rev/UPDATELOG.md b/clash-verge-rev/UPDATELOG.md index a345e0726c..a70b632e9d 100644 --- a/clash-verge-rev/UPDATELOG.md +++ b/clash-verge-rev/UPDATELOG.md @@ -12,6 +12,7 @@ - 修复将快捷键名称更名为 `Clash Verge`之后无法删除图标和无法删除注册表 - 修复`DNS`覆写 `fallback` `proxy server` `nameserver` `direct Nameserver` 字段支持留空 - 修复`DNS`覆写 `nameserver-policy` 字段无法正确识别 `geo` 库 +- 修复搜索框输入特殊字符崩溃 ### ✨ 新增功能 diff --git a/clash-verge-rev/src/components/base/base-search-box.tsx b/clash-verge-rev/src/components/base/base-search-box.tsx index a01789fd5d..079c3f7399 100644 --- a/clash-verge-rev/src/components/base/base-search-box.tsx +++ b/clash-verge-rev/src/components/base/base-search-box.tsx @@ -1,11 +1,10 @@ import { Box, SvgIcon, TextField, styled } from "@mui/material"; import Tooltip from "@mui/material/Tooltip"; -import { ChangeEvent, useEffect, useRef, useState, useMemo } from "react"; - -import { useTranslation } from "react-i18next"; +import { ChangeEvent, useEffect, useMemo, useRef, useState } from "react"; import matchCaseIcon from "@/assets/image/component/match_case.svg?react"; import matchWholeWordIcon from "@/assets/image/component/match_whole_word.svg?react"; import useRegularExpressionIcon from "@/assets/image/component/use_regular_expression.svg?react"; +import { useTranslation } from "react-i18next"; export type SearchState = { text: string; @@ -56,9 +55,28 @@ export const BaseSearchBox = (props: SearchProps) => { inheritViewBox: true, }; + // 验证正则表达式的辅助函数 + const validateRegex = (pattern: string) => { + if (!pattern) return true; + try { + new RegExp(pattern); + return true; + } catch (e) { + return false; + } + }; + const createMatcher = useMemo(() => { return (searchText: string) => { try { + // 当启用正则表达式验证是否合规 + if (useRegularExpression && searchText) { + const isValid = validateRegex(searchText); + if (!isValid) { + throw new Error(t("Invalid regular expression")); + } + } + return (content: string) => { if (!searchText) return true; @@ -76,16 +94,15 @@ export const BaseSearchBox = (props: SearchProps) => { return item.includes(searchItem); }; } catch (err) { - setErrorMessage(`${err}`); - return () => true; + setErrorMessage(err instanceof Error ? err.message : `${err}`); + return () => false; // 无效正则规则 不匹配值 } }; - }, [matchCase, matchWholeWord, useRegularExpression]); + }, [matchCase, matchWholeWord, useRegularExpression, t]); useEffect(() => { if (!inputRef.current) return; const value = inputRef.current.value; - setErrorMessage(""); props.onSearch(createMatcher(value), { text: value, matchCase, @@ -97,6 +114,15 @@ export const BaseSearchBox = (props: SearchProps) => { const onChange = (e: ChangeEvent) => { const value = e.target?.value ?? ""; setErrorMessage(""); + + // 验证正则表达式 + if (useRegularExpression && value) { + const isValid = validateRegex(value); + if (!isValid) { + setErrorMessage(t("Invalid regular expression")); + } + } + props.onSearch(createMatcher(value), { text: value, matchCase, @@ -106,7 +132,7 @@ export const BaseSearchBox = (props: SearchProps) => { }; return ( - + { placeholder={props.placeholder ?? t("Filter conditions")} sx={{ input: { py: 0.65, px: 1.25 } }} onChange={onChange} + error={!!errorMessage} slotProps={{ input: { sx: { pr: 1 }, diff --git a/clash-verge-rev/src/locales/en.json b/clash-verge-rev/src/locales/en.json index 544c14fed6..4449ce15a9 100644 --- a/clash-verge-rev/src/locales/en.json +++ b/clash-verge-rev/src/locales/en.json @@ -647,5 +647,6 @@ "Allowed Origins": "Allowed Origins", "Please enter a valid url": "Please enter a valid url", "Add": "Add", - "Development mode: Automatically includes Tauri and localhost origins": "Development mode: Automatically includes Tauri and localhost origins" + "Development mode: Automatically includes Tauri and localhost origins": "Development mode: Automatically includes Tauri and localhost origins", + "Invalid regular expression": "Invalid regular expression" } diff --git a/clash-verge-rev/src/locales/zh.json b/clash-verge-rev/src/locales/zh.json index 00ce9b5bd9..cd6f290ce0 100644 --- a/clash-verge-rev/src/locales/zh.json +++ b/clash-verge-rev/src/locales/zh.json @@ -647,5 +647,6 @@ "Allowed Origins": "允许的来源", "Please enter a valid url": "请输入有效的网址", "Add": "添加", - "Development mode: Automatically includes Tauri and localhost origins": "开发模式:自动包含 Tauri 和 localhost 来源" + "Development mode: Automatically includes Tauri and localhost origins": "开发模式:自动包含 Tauri 和 localhost 来源", + "Invalid regular expression": "无效的正则表达式" } diff --git a/filebrowser/frontend/src/i18n/uk.json b/filebrowser/frontend/src/i18n/uk.json index 6ef74a4f8f..aa4080df4b 100644 --- a/filebrowser/frontend/src/i18n/uk.json +++ b/filebrowser/frontend/src/i18n/uk.json @@ -3,17 +3,17 @@ "cancel": "Відмінити", "clear": "Очистити", "close": "Закрити", - "continue": "Continue", + "continue": "Продовжити", "copy": "Копіювати", "copyFile": "Копіювати файл", "copyToClipboard": "Копіювати в буфер обміну", - "copyDownloadLinkToClipboard": "Copy download link to clipboard", + "copyDownloadLinkToClipboard": "Скопіювати завантажувальне посилання в буфер обміну", "create": "Створити", "delete": "Видалити", "download": "Завантажити", "file": "Файл", "folder": "Папка", - "fullScreen": "Toggle full screen", + "fullScreen": "Перемкнути повноекранний режим", "hideDotfiles": "Приховати точкові файли", "info": "Інфо", "more": "Більше", @@ -24,7 +24,7 @@ "ok": "ОК", "permalink": "Отримати постійне посилання", "previous": "Назад", - "preview": "Preview", + "preview": "Попередній перегляд", "publish": "Опублікувати", "rename": "Перейменувати", "replace": "Замінити", @@ -42,7 +42,7 @@ "update": "Оновити", "upload": "Вивантажити", "openFile": "Відкрити файл", - "discardChanges": "Discard" + "discardChanges": "Скасувати" }, "download": { "downloadFile": "Завантажити файл", @@ -50,7 +50,7 @@ "downloadSelected": "Завантажити вибране" }, "upload": { - "abortUpload": "Are you sure you wish to abort?" + "abortUpload": "Ви впевнені, що хочете перервати?" }, "errors": { "forbidden": "У вас немає прав доступу до цього.", @@ -66,7 +66,7 @@ "home": "Домівка", "lastModified": "Останній раз змінено", "loading": "Завантаження...", - "lonely": "Тут пусто...", + "lonely": "Тут порожньо...", "metadata": "Метадані", "multipleSelectionEnabled": "Мультивибір включений", "name": "Ім'я", @@ -81,7 +81,7 @@ "ctrl": { "click": "вибрати кілька файлів чи каталогів", "f": "відкрити пошук", - "s": "скачати файл або поточний каталог" + "s": "завантажити файл або поточний каталог" }, "del": "видалити вибрані елементи", "doubleClick": "відкрити файл чи каталог", @@ -100,7 +100,7 @@ "submit": "Увійти", "username": "Ім'я користувача", "usernameTaken": "Ім'я користувача вже використовується", - "wrongCredentials": "Невірне ім'я користувача або пароль" + "wrongCredentials": "Неправильне ім'я користувача або пароль" }, "permanent": "Постійний", "prompts": { @@ -110,7 +110,7 @@ "deleteMessageMultiple": "Видалити ці файли ({count})?", "deleteMessageSingle": "Видалити цей файл/каталог?", "deleteMessageShare": "Видалити цей спільний файл/каталог ({path})?", - "deleteUser": "Are you sure you want to delete this user?", + "deleteUser": "Видалити цього користувача?", "deleteTitle": "Видалити файли", "displayName": "Відображене ім'я:", "download": "Завантажити файли", @@ -137,11 +137,11 @@ "show": "Показати", "size": "Розмір", "upload": "Вивантажити", - "uploadFiles": "Uploading {files} files...", + "uploadFiles": "Вивантаження {files} файлів...", "uploadMessage": "Виберіть варіант для вивантаження.", "optionalPassword": "Необов'язковий пароль", - "resolution": "Resolution", - "discardEditorChanges": "Are you sure you wish to discard the changes you've made?" + "resolution": "Розширення", + "discardEditorChanges": "Чи дійсно ви хочете скасувати поточні зміни?" }, "search": { "images": "Зображення", @@ -170,14 +170,14 @@ "commandRunnerHelp": "Тут ви можете встановити команди, які будуть виконуватися у зазначених подіях. Ви повинні вказати по одній команді в кожному рядку. Змінні середовища {0} та {1} будуть доступні, будучи {0} щодо {1}. Додаткові відомості про цю функцію та доступні змінні середовища див. у {2}.", "commandsUpdated": "Команди оновлені!", "createUserDir": "Автоматичне створення домашнього каталогу користувача при додаванні нового користувача", - "minimumPasswordLength": "Minimum password length", - "tusUploads": "Chunked Uploads", - "tusUploadsHelp": "File Browser supports chunked file uploads, allowing for the creation of efficient, reliable, resumable and chunked file uploads even on unreliable networks.", - "tusUploadsChunkSize": "Indicates to maximum size of a request (direct uploads will be used for smaller uploads). You may input a plain integer denoting byte size input or a string like 10MB, 1GB etc.", - "tusUploadsRetryCount": "Number of retries to perform if a chunk fails to upload.", - "userHomeBasePath": "Base path for user home directories", - "userScopeGenerationPlaceholder": "The scope will be auto generated", - "createUserHomeDirectory": "Create user home directory", + "minimumPasswordLength": "Мінімальна довжина паролю", + "tusUploads": "Фрагментовані завантаження", + "tusUploadsHelp": "File Browser підтримує завантаження частинами, дозволяючи створення ефективних, надійних, відновлюваних та фрагментованих завантажень навіть при ненадійному з'єднанні.", + "tusUploadsChunkSize": "Вказує на максимальний розмір запиту (для менших завантажень використовуватиметься пряме завантаження). Ви можете ввести цілочисельне значення у байтах або ж рядок на кшталт 10MB, 1GB тощо.", + "tusUploadsRetryCount": "Кількість повторних спроб які потрібно виконати, якщо фрагмент не вдалося завантажити.", + "userHomeBasePath": "Основний шлях для домашніх каталогів користувачів", + "userScopeGenerationPlaceholder": "Кореневий каталог буде згенеровано автоматично", + "createUserHomeDirectory": "Створити домашній каталог користувача", "customStylesheet": "Свій стиль", "defaultUserDescription": "Це налаштування за замовчуванням для нових користувачів.", "disableExternalLinks": "Вимкнути зовнішні посилання (крім документації)", @@ -210,12 +210,12 @@ "share": "Ділітися файлами" }, "permissions": "Дозволи", - "permissionsHelp": "Можна настроїти користувача як адміністратора або вибрати індивідуальні дозволи. При виборі \"Адміністратор\" всі інші параметри будуть автоматично вибрані. Керування користувачами - привілей адміністратора.\n", + "permissionsHelp": "Можна налаштувати користувача як адміністратора чи вибрати індивідуальні дозволи. При виборі \"Адміністратор\" всі інші параметри будуть автоматично вибрані. Керування користувачами - привілей адміністратора.\n", "profileSettings": "Налаштування профілю", "ruleExample1": "запобігти доступу до будь-якого прихованого файлу (наприклад: .git, .gitignore) у кожній папці.\n", "ruleExample2": "блокує доступ до файлу з ім'ям Caddyfile у кореневій області.", "rules": "Права", - "rulesHelp": "Тут ви можете визначити набір дозволяючих та забороняючих правил для цього конкретного користувача. Блоковані файли не відображатимуться у списках, і не будуть доступні для користувача. Є підтримка регулярних виразів та відносних шляхів.\n", + "rulesHelp": "Тут ви можете визначити набір дозволів та заборон для цього конкретного користувача. Блоковані файли не відображатимуться у списках і не будуть доступними для користувача. Є підтримка регулярних виразів та відносних шляхів.\n", "scope": "Корінь", "setDateFormat": "Встановити точний формат дати", "settingsUpdated": "Налаштування застосовані!", @@ -224,7 +224,7 @@ "shareDeleted": "Спільне посилання видалено!", "singleClick": "Відкриття файлів та каталогів одним кліком", "themes": { - "default": "System default", + "default": "За замовчуванням (системна)", "dark": "Темна", "light": "Світла", "title": "Тема" @@ -232,11 +232,11 @@ "user": "Користувач", "userCommands": "Команди", "userCommandsHelp": "Список команд, доступних користувачу, розділений пробілами. Приклад:\n", - "userCreated": "Користувач створений!", + "userCreated": "Користувача створено!", "userDefaults": "Налаштування користувача за замовчуванням", - "userDeleted": "Користувач видалений!", + "userDeleted": "Користувача видалено!", "userManagement": "Керування користувачами", - "userUpdated": "Користувач змінений!", + "userUpdated": "Користувача змінено!", "username": "Ім'я користувача", "users": "Користувачі" }, diff --git a/lede/package/boot/uboot-rockchip/Makefile b/lede/package/boot/uboot-rockchip/Makefile index cc16079485..090cd00558 100644 --- a/lede/package/boot/uboot-rockchip/Makefile +++ b/lede/package/boot/uboot-rockchip/Makefile @@ -5,10 +5,10 @@ include $(TOPDIR)/rules.mk include $(INCLUDE_DIR)/kernel.mk -PKG_VERSION:=2025.07-rc1 +PKG_VERSION:=2025.07 PKG_RELEASE:=1 -PKG_HASH:=a9335ae3e71dd53e3c4698bd1afffce44938e52832f95becb2fcce9bf3383263 +PKG_HASH:=0f933f6c5a426895bf306e93e6ac53c60870e4b54cda56d95211bec99e63bec7 PKG_MAINTAINER:=Tobias Maedel diff --git a/lede/package/boot/uboot-rockchip/patches/110-force-pylibfdt-build.patch b/lede/package/boot/uboot-rockchip/patches/110-force-pylibfdt-build.patch index fcecf598d0..7c785c626a 100644 --- a/lede/package/boot/uboot-rockchip/patches/110-force-pylibfdt-build.patch +++ b/lede/package/boot/uboot-rockchip/patches/110-force-pylibfdt-build.patch @@ -1,6 +1,6 @@ --- a/Makefile +++ b/Makefile -@@ -2075,26 +2075,7 @@ endif +@@ -2076,26 +2076,7 @@ endif # Check dtc and pylibfdt, if DTC is provided, else build them PHONY += scripts_dtc scripts_dtc: scripts_basic diff --git a/lede/package/boot/uboot-rockchip/patches/111-fix-mkimage-host-build.patch b/lede/package/boot/uboot-rockchip/patches/111-fix-mkimage-host-build.patch index a06935f53e..aeff6c8cbe 100644 --- a/lede/package/boot/uboot-rockchip/patches/111-fix-mkimage-host-build.patch +++ b/lede/package/boot/uboot-rockchip/patches/111-fix-mkimage-host-build.patch @@ -1,6 +1,6 @@ --- a/tools/image-host.c +++ b/tools/image-host.c -@@ -1175,6 +1175,7 @@ static int fit_config_add_verification_d +@@ -1189,6 +1189,7 @@ static int fit_config_add_verification_d * 2) get public key (X509_get_pubkey) * 3) provide der format (d2i_RSAPublicKey) */ @@ -8,7 +8,7 @@ static int read_pub_key(const char *keydir, const void *name, unsigned char **pubkey, int *pubkey_len) { -@@ -1228,6 +1229,13 @@ err_cert: +@@ -1242,6 +1243,13 @@ err_cert: fclose(f); return ret; } diff --git a/lede/package/boot/uboot-rockchip/patches/200-radxa-e25-update-baudrate.patch b/lede/package/boot/uboot-rockchip/patches/200-radxa-e25-update-baudrate.patch index a51ddc78f1..cc49d51427 100644 --- a/lede/package/boot/uboot-rockchip/patches/200-radxa-e25-update-baudrate.patch +++ b/lede/package/boot/uboot-rockchip/patches/200-radxa-e25-update-baudrate.patch @@ -1,6 +1,6 @@ --- a/configs/radxa-e25-rk3568_defconfig +++ b/configs/radxa-e25-rk3568_defconfig -@@ -64,6 +64,7 @@ CONFIG_REGULATOR_RK8XX=y +@@ -63,6 +63,7 @@ CONFIG_REGULATOR_RK8XX=y CONFIG_PWM_ROCKCHIP=y CONFIG_SPL_RAM=y CONFIG_SCSI=y diff --git a/lede/package/boot/uboot-rockchip/patches/201-rk3568-generic-remove-spi-support.patch b/lede/package/boot/uboot-rockchip/patches/201-rk3568-generic-remove-spi-support.patch index 813f8a0df3..1579b53d41 100644 --- a/lede/package/boot/uboot-rockchip/patches/201-rk3568-generic-remove-spi-support.patch +++ b/lede/package/boot/uboot-rockchip/patches/201-rk3568-generic-remove-spi-support.patch @@ -49,16 +49,16 @@ CONFIG_DEBUG_UART=y CONFIG_FIT=y CONFIG_FIT_VERBOSE=y -@@ -25,8 +21,6 @@ CONFIG_DEFAULT_FDT_FILE="rockchip/rk3568 +@@ -24,8 +20,6 @@ CONFIG_DEFAULT_FDT_FILE="rockchip/rk3568 + # CONFIG_DISPLAY_CPUINFO is not set CONFIG_SPL_MAX_SIZE=0x40000 - CONFIG_SPL_PAD_TO=0x7f8000 # CONFIG_SPL_RAW_IMAGE_SUPPORT is not set -CONFIG_SPL_SPI_LOAD=y -CONFIG_SYS_SPI_U_BOOT_OFFS=0x60000 CONFIG_SPL_ATF=y - CONFIG_CMD_GPIO=y - CONFIG_CMD_GPT=y -@@ -57,20 +51,12 @@ CONFIG_MMC_DW_ROCKCHIP=y + CONFIG_CMD_MEMINFO=y + CONFIG_CMD_MEMINFO_MAP=y +@@ -59,20 +53,12 @@ CONFIG_MMC_DW_ROCKCHIP=y CONFIG_MMC_SDHCI=y CONFIG_MMC_SDHCI_SDMA=y CONFIG_MMC_SDHCI_ROCKCHIP=y diff --git a/mihomo/config/config.go b/mihomo/config/config.go index c7107d50d4..673883c44c 100644 --- a/mihomo/config/config.go +++ b/mihomo/config/config.go @@ -1035,46 +1035,20 @@ func parseRules(rulesConfig []string, proxies map[string]C.Proxy, ruleProviders // parse rules for idx, line := range rulesConfig { - rule := trimArr(strings.Split(line, ",")) - var ( - payload string - target string - params []string - ruleName = strings.ToUpper(rule[0]) - ) - - l := len(rule) - - if ruleName == "NOT" || ruleName == "OR" || ruleName == "AND" || ruleName == "SUB-RULE" || ruleName == "DOMAIN-REGEX" || ruleName == "PROCESS-NAME-REGEX" || ruleName == "PROCESS-PATH-REGEX" { - target = rule[l-1] - payload = strings.Join(rule[1:l-1], ",") - } else { - if l < 2 { - return nil, fmt.Errorf("%s[%d] [%s] error: format invalid", format, idx, line) - } - if l < 4 { - rule = append(rule, make([]string, 4-l)...) - } - if ruleName == "MATCH" { - l = 2 - } - if l >= 3 { - l = 3 - payload = rule[1] - } - target = rule[l-1] - params = rule[l:] + tp, payload, target, params := RC.ParseRulePayload(line, true) + if target == "" { + return nil, fmt.Errorf("%s[%d] [%s] error: format invalid", format, idx, line) } + if _, ok := proxies[target]; !ok { - if ruleName != "SUB-RULE" { + if tp != "SUB-RULE" { return nil, fmt.Errorf("%s[%d] [%s] error: proxy [%s] not found", format, idx, line, target) } else if _, ok = subRules[target]; !ok { return nil, fmt.Errorf("%s[%d] [%s] error: sub-rule [%s] not found", format, idx, line, target) } } - params = trimArr(params) - parsed, parseErr := R.ParseRule(ruleName, payload, target, params, subRules) + parsed, parseErr := R.ParseRule(tp, payload, target, params, subRules) if parseErr != nil { return nil, fmt.Errorf("%s[%d] [%s] error: %s", format, idx, line, parseErr.Error()) } diff --git a/mihomo/config/utils.go b/mihomo/config/utils.go index 8ce3e8b896..c72c120df7 100644 --- a/mihomo/config/utils.go +++ b/mihomo/config/utils.go @@ -6,19 +6,11 @@ import ( "net/netip" "os" "strconv" - "strings" "github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/common/structure" ) -func trimArr(arr []string) (r []string) { - for _, e := range arr { - r = append(r, strings.Trim(e, " ")) - } - return -} - // Check if ProxyGroups form DAG(Directed Acyclic Graph), and sort all ProxyGroups by dependency order. // Meanwhile, record the original index in the config file. // If loop is detected, return an error with location of loop. diff --git a/mihomo/rules/common/base.go b/mihomo/rules/common/base.go index ab53753ed2..33ab5a1f3a 100644 --- a/mihomo/rules/common/base.go +++ b/mihomo/rules/common/base.go @@ -34,22 +34,48 @@ func ParseParams(params []string) (isSrc bool, noResolve bool) { return } -func ParseRulePayload(ruleRaw string) (string, string, []string) { - item := strings.Split(ruleRaw, ",") - if len(item) == 1 { - return "", item[0], nil - } else if len(item) == 2 { - return item[0], item[1], nil - } else if len(item) > 2 { - // keep in sync with config/config.go [parseRules] - if item[0] == "NOT" || item[0] == "OR" || item[0] == "AND" || item[0] == "SUB-RULE" || item[0] == "DOMAIN-REGEX" || item[0] == "PROCESS-NAME-REGEX" || item[0] == "PROCESS-PATH-REGEX" { - return item[0], strings.Join(item[1:], ","), nil - } else { - return item[0], item[1], item[2:] +func trimArr(arr []string) (r []string) { + for _, e := range arr { + r = append(r, strings.Trim(e, " ")) + } + return +} + +// ParseRulePayload parse rule format like: +// `tp,payload,target(,params...)` or `tp,payload(,params...)` +// needTarget control the format contains `target` in string +func ParseRulePayload(ruleRaw string, needTarget bool) (tp, payload, target string, params []string) { + item := trimArr(strings.Split(ruleRaw, ",")) + tp = strings.ToUpper(item[0]) + if len(item) > 1 { + switch tp { + case "MATCH": + // MATCH doesn't contain payload and params + target = item[1] + case "NOT", "OR", "AND", "SUB-RULE", "DOMAIN-REGEX", "PROCESS-NAME-REGEX", "PROCESS-PATH-REGEX": + // some type of rules that has comma in payload and don't need params + if needTarget { + l := len(item) + target = item[l-1] // don't have params so target must at the end of slices + item = item[:l-1] // remove the target from slices + } + payload = strings.Join(item[1:], ",") + default: + payload = item[1] + if len(item) > 2 { + if needTarget { + target = item[2] + if len(item) > 3 { + params = item[3:] + } + } else { + params = item[2:] + } + } } } - return "", "", nil + return } type ParseRuleFunc func(tp, payload, target string, params []string, subRules map[string][]C.Rule) (C.Rule, error) diff --git a/mihomo/rules/logic/logic.go b/mihomo/rules/logic/logic.go index c740d8deca..e4f3817e6d 100644 --- a/mihomo/rules/logic/logic.go +++ b/mihomo/rules/logic/logic.go @@ -78,14 +78,14 @@ func (r Range) containRange(preStart, preEnd int) bool { } func (logic *Logic) payloadToRule(subPayload string, parseRule common.ParseRuleFunc) (C.Rule, error) { - tp, payload, param := common.ParseRulePayload(subPayload) + tp, payload, target, param := common.ParseRulePayload(subPayload, false) switch tp { case "MATCH", "SUB-RULE": return nil, fmt.Errorf("unsupported rule type [%s] on logic rule", tp) case "": return nil, fmt.Errorf("[%s] format is error", subPayload) } - return parseRule(tp, payload, "", param, nil) + return parseRule(tp, payload, target, param, nil) } func (logic *Logic) format(payload string) ([]Range, error) { diff --git a/mihomo/rules/parser.go b/mihomo/rules/parser.go index 675c52ec09..6d8b3b8ea9 100644 --- a/mihomo/rules/parser.go +++ b/mihomo/rules/parser.go @@ -10,6 +10,10 @@ import ( ) func ParseRule(tp, payload, target string, params []string, subRules map[string][]C.Rule) (parsed C.Rule, parseErr error) { + if tp != "MATCH" && payload == "" { // only MATCH allowed doesn't contain payload + return nil, fmt.Errorf("missing subsequent parameters: %s", tp) + } + switch tp { case "DOMAIN": parsed = RC.NewDomain(payload, target) @@ -83,8 +87,6 @@ func ParseRule(tp, payload, target string, params []string, subRules map[string] case "MATCH": parsed = RC.NewMatch(target) parseErr = nil - case "": - parseErr = fmt.Errorf("missing subsequent parameters: %s", payload) default: parseErr = fmt.Errorf("unsupported rule type: %s", tp) } diff --git a/mihomo/rules/provider/classical_strategy.go b/mihomo/rules/provider/classical_strategy.go index 2505b3018b..497f916813 100644 --- a/mihomo/rules/provider/classical_strategy.go +++ b/mihomo/rules/provider/classical_strategy.go @@ -12,7 +12,7 @@ import ( type classicalStrategy struct { rules []C.Rule count int - parse func(tp, payload, target string, params []string) (parsed C.Rule, parseErr error) + parse common.ParseRuleFunc } func (c *classicalStrategy) Behavior() P.RuleBehavior { @@ -39,25 +39,26 @@ func (c *classicalStrategy) Reset() { } func (c *classicalStrategy) Insert(rule string) { - ruleType, rule, params := common.ParseRulePayload(rule) - r, err := c.parse(ruleType, rule, "", params) + r, err := c.payloadToRule(rule) if err != nil { - log.Warnln("parse classical rule error: %s", err.Error()) + log.Warnln("parse classical rule [%s] error: %s", rule, err.Error()) } else { c.rules = append(c.rules, r) c.count++ } } +func (c *classicalStrategy) payloadToRule(rule string) (C.Rule, error) { + tp, payload, target, params := common.ParseRulePayload(rule, false) + switch tp { + case "MATCH", "RULE-SET", "SUB-RULE": + return nil, fmt.Errorf("unsupported rule type on classical rule-set: %s", tp) + } + return c.parse(tp, payload, target, params, nil) +} + func (c *classicalStrategy) FinishInsert() {} -func NewClassicalStrategy(parse func(tp, payload, target string, params []string, subRules map[string][]C.Rule) (parsed C.Rule, parseErr error)) *classicalStrategy { - return &classicalStrategy{rules: []C.Rule{}, parse: func(tp, payload, target string, params []string) (parsed C.Rule, parseErr error) { - switch tp { - case "MATCH", "RULE-SET", "SUB-RULE": - return nil, fmt.Errorf("unsupported rule type on classical rule-set: %s", tp) - default: - return parse(tp, payload, target, params, nil) - } - }} +func NewClassicalStrategy(parse common.ParseRuleFunc) *classicalStrategy { + return &classicalStrategy{rules: []C.Rule{}, parse: parse} } diff --git a/openwrt-passwall/luci-app-passwall/luasrc/controller/passwall.lua b/openwrt-passwall/luci-app-passwall/luasrc/controller/passwall.lua index 52fef1e66b..6a91ae4e12 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/controller/passwall.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/controller/passwall.lua @@ -113,29 +113,29 @@ end function reset_config() luci.sys.call('/etc/init.d/passwall stop') luci.sys.call('[ -f "/usr/share/passwall/0_default_config" ] && cp -f /usr/share/passwall/0_default_config /etc/config/passwall') - luci.http.redirect(api.url()) + http.redirect(api.url()) end function show_menu() api.sh_uci_del(appname, "@global[0]", "hide_from_luci", true) luci.sys.call("rm -rf /tmp/luci-*") luci.sys.call("/etc/init.d/rpcd restart >/dev/null") - luci.http.redirect(api.url()) + http.redirect(api.url()) end function hide_menu() api.sh_uci_set(appname, "@global[0]", "hide_from_luci", "1", true) luci.sys.call("rm -rf /tmp/luci-*") luci.sys.call("/etc/init.d/rpcd restart >/dev/null") - luci.http.redirect(luci.dispatcher.build_url("admin", "status", "overview")) + http.redirect(luci.dispatcher.build_url("admin", "status", "overview")) end function link_add_node() -- 分片接收以突破uhttpd的限制 local tmp_file = "/tmp/links.conf" - local chunk = luci.http.formvalue("chunk") - local chunk_index = tonumber(luci.http.formvalue("chunk_index")) - local total_chunks = tonumber(luci.http.formvalue("total_chunks")) + local chunk = http.formvalue("chunk") + local chunk_index = tonumber(http.formvalue("chunk_index")) + local total_chunks = tonumber(http.formvalue("total_chunks")) if chunk and chunk_index ~= nil and total_chunks ~= nil then -- 按顺序拼接到文件 @@ -156,8 +156,8 @@ function link_add_node() end function socks_autoswitch_add_node() - local id = luci.http.formvalue("id") - local key = luci.http.formvalue("key") + local id = http.formvalue("id") + local key = http.formvalue("key") if id and id ~= "" and key and key ~= "" then uci:set(appname, id, "enable_autoswitch", "1") local new_list = uci:get(appname, id, "autoswitch_backup_node") or {} @@ -174,12 +174,12 @@ function socks_autoswitch_add_node() uci:set_list(appname, id, "autoswitch_backup_node", new_list) api.uci_save(uci, appname) end - luci.http.redirect(api.url("socks_config", id)) + http.redirect(api.url("socks_config", id)) end function socks_autoswitch_remove_node() - local id = luci.http.formvalue("id") - local key = luci.http.formvalue("key") + local id = http.formvalue("id") + local key = http.formvalue("key") if id and id ~= "" and key and key ~= "" then uci:set(appname, id, "enable_autoswitch", "1") local new_list = uci:get(appname, id, "autoswitch_backup_node") or {} @@ -191,20 +191,20 @@ function socks_autoswitch_remove_node() uci:set_list(appname, id, "autoswitch_backup_node", new_list) api.uci_save(uci, appname) end - luci.http.redirect(api.url("socks_config", id)) + http.redirect(api.url("socks_config", id)) end function gen_client_config() - local id = luci.http.formvalue("id") + local id = http.formvalue("id") local config_file = api.TMP_PATH .. "/config_" .. id luci.sys.call(string.format("/usr/share/passwall/app.sh run_socks flag=config_%s node=%s bind=127.0.0.1 socks_port=1080 config_file=%s no_run=1", id, id, config_file)) if nixio.fs.access(config_file) then - luci.http.prepare_content("application/json") - luci.http.write(luci.sys.exec("cat " .. config_file)) + http.prepare_content("application/json") + http.write(luci.sys.exec("cat " .. config_file)) luci.sys.call("rm -f " .. config_file) else - luci.http.redirect(api.url("node_list")) + http.redirect(api.url("node_list")) end end @@ -219,13 +219,12 @@ function get_now_use_node() if udp_node then e["UDP"] = udp_node end - luci.http.prepare_content("application/json") - luci.http.write_json(e) + http_write_json(e) end function get_redir_log() - local name = luci.http.formvalue("name") - local proto = luci.http.formvalue("proto") + local name = http.formvalue("name") + local proto = http.formvalue("proto") local path = "/tmp/etc/passwall/acl/" .. name proto = proto:upper() if proto == "UDP" and (uci:get(appname, "@global[0]", "udp_node") or "nil") == "tcp" and not fs.access(path .. "/" .. proto .. ".log") then @@ -234,39 +233,39 @@ function get_redir_log() if fs.access(path .. "/" .. proto .. ".log") then local content = luci.sys.exec("tail -n 19999 ".. path .. "/" .. proto .. ".log") content = content:gsub("\n", "
") - luci.http.write(content) + http.write(content) else - luci.http.write(string.format("", i18n.translate("Not enabled log"))) + http.write(string.format("", i18n.translate("Not enabled log"))) end end function get_socks_log() - local name = luci.http.formvalue("name") + local name = http.formvalue("name") local path = "/tmp/etc/passwall/SOCKS_" .. name .. ".log" if fs.access(path) then local content = luci.sys.exec("cat ".. path) content = content:gsub("\n", "
") - luci.http.write(content) + http.write(content) else - luci.http.write(string.format("", i18n.translate("Not enabled log"))) + http.write(string.format("", i18n.translate("Not enabled log"))) end end function get_chinadns_log() - local flag = luci.http.formvalue("flag") + local flag = http.formvalue("flag") local path = "/tmp/etc/passwall/acl/" .. flag .. "/chinadns_ng.log" if fs.access(path) then local content = luci.sys.exec("tail -n 5000 ".. path) content = content:gsub("\n", "
") - luci.http.write(content) + http.write(content) else - luci.http.write(string.format("", i18n.translate("Not enabled log"))) + http.write(string.format("", i18n.translate("Not enabled log"))) end end function get_log() -- luci.sys.exec("[ -f /tmp/log/passwall.log ] && sed '1!G;h;$!d' /tmp/log/passwall.log > /tmp/log/passwall_show.log") - luci.http.write(luci.sys.exec("[ -f '/tmp/log/passwall.log' ] && cat /tmp/log/passwall.log")) + http.write(luci.sys.exec("[ -f '/tmp/log/passwall.log' ] && cat /tmp/log/passwall.log")) end function clear_log() @@ -292,20 +291,18 @@ function index_status() else e["udp_node_status"] = luci.sys.call("/bin/busybox top -bn1 | grep -v 'grep' | grep '/tmp/etc/passwall/bin/' | grep 'default' | grep 'UDP' >/dev/null") == 0 end - luci.http.prepare_content("application/json") - luci.http.write_json(e) + http_write_json(e) end function haproxy_status() local e = luci.sys.call(string.format("/bin/busybox top -bn1 | grep -v grep | grep '%s/bin/' | grep haproxy >/dev/null", appname)) == 0 - luci.http.prepare_content("application/json") - luci.http.write_json(e) + http_write_json(e) end function socks_status() local e = {} - local index = luci.http.formvalue("index") - local id = luci.http.formvalue("id") + local index = http.formvalue("index") + local id = http.formvalue("id") e.index = index e.socks_status = luci.sys.call(string.format("/bin/busybox top -bn1 | grep -v 'grep' | grep '/tmp/etc/passwall/bin/' | grep -v '_acl_' | grep '%s' | grep 'SOCKS_' > /dev/null", id)) == 0 local use_http = uci:get(appname, id, "http_port") or 0 @@ -314,14 +311,13 @@ function socks_status() e.use_http = 1 e.http_status = luci.sys.call(string.format("/bin/busybox top -bn1 | grep -v 'grep' | grep '/tmp/etc/passwall/bin/' | grep -v '_acl_' | grep '%s' | grep -E 'HTTP_|HTTP2SOCKS' > /dev/null", id)) == 0 end - luci.http.prepare_content("application/json") - luci.http.write_json(e) + http_write_json(e) end function connect_status() local e = {} e.use_time = "" - local url = luci.http.formvalue("url") + local url = http.formvalue("url") local baidu = string.find(url, "baidu") local chn_list = uci:get(appname, "@global[0]", "chn_list") or "direct" local gfw_list = uci:get(appname, "@global[0]", "use_gfw_list") or "1" @@ -357,15 +353,14 @@ function connect_status() e.ping_type = "curl" end end - luci.http.prepare_content("application/json") - http.write(jsonStringify(e)) + http_write_json(e) end function ping_node() - local index = luci.http.formvalue("index") - local address = luci.http.formvalue("address") - local port = luci.http.formvalue("port") - local type = luci.http.formvalue("type") or "icmp" + local index = http.formvalue("index") + local address = http.formvalue("address") + local port = http.formvalue("port") + local type = http.formvalue("type") or "icmp" local e = {} e.index = index if type == "tcping" and luci.sys.exec("echo -n $(command -v tcping)") ~= "" then @@ -376,13 +371,12 @@ function ping_node() else e.ping = luci.sys.exec("echo -n $(ping -c 1 -W 1 %q 2>&1 | grep -o 'time=[0-9]*' | awk -F '=' '{print $2}') 2>/dev/null" % address) end - luci.http.prepare_content("application/json") - luci.http.write_json(e) + http_write_json(e) end function urltest_node() - local index = luci.http.formvalue("index") - local id = luci.http.formvalue("id") + local index = http.formvalue("index") + local id = http.formvalue("id") local e = {} e.index = index local result = luci.sys.exec(string.format("/usr/share/passwall/test.sh url_test_node %s %s", id, "urltest_node")) @@ -398,20 +392,19 @@ function urltest_node() end end end - luci.http.prepare_content("application/json") - luci.http.write_json(e) + http_write_json(e) end function set_node() - local protocol = luci.http.formvalue("protocol") - local section = luci.http.formvalue("section") + local protocol = http.formvalue("protocol") + local section = http.formvalue("section") uci:set(appname, "@global[0]", protocol .. "_node", section) api.uci_save(uci, appname, true, true) - luci.http.redirect(api.url("log")) + http.redirect(api.url("log")) end function copy_node() - local section = luci.http.formvalue("section") + local section = http.formvalue("section") local uuid = api.gen_short_uuid() uci:section(appname, "nodes", uuid) for k, v in pairs(uci:get_all(appname, section)) do @@ -428,7 +421,7 @@ function copy_node() uci:delete(appname, uuid, "add_from") uci:set(appname, uuid, "add_mode", 1) api.uci_save(uci, appname) - luci.http.redirect(api.url("node_config", uuid)) + http.redirect(api.url("node_config", uuid)) end function clear_all_nodes() @@ -459,7 +452,7 @@ function clear_all_nodes() end function delete_select_nodes() - local ids = luci.http.formvalue("ids") + local ids = http.formvalue("ids") string.gsub(ids, '[^' .. "," .. ']+', function(w) if (uci:get(appname, "@global[0]", "tcp_node") or "") == w then uci:delete(appname, '@global[0]', "tcp_node") @@ -518,31 +511,31 @@ function delete_select_nodes() end function update_rules() - local update = luci.http.formvalue("update") + local update = http.formvalue("update") luci.sys.call("lua /usr/share/passwall/rule_update.lua log '" .. update .. "' > /dev/null 2>&1 &") http_write_json() end function server_user_status() local e = {} - e.index = luci.http.formvalue("index") - e.status = luci.sys.call(string.format("/bin/busybox top -bn1 | grep -v 'grep' | grep '%s/bin/' | grep -i '%s' >/dev/null", appname .. "_server", luci.http.formvalue("id"))) == 0 + e.index = http.formvalue("index") + e.status = luci.sys.call(string.format("/bin/busybox top -bn1 | grep -v 'grep' | grep '%s/bin/' | grep -i '%s' >/dev/null", appname .. "_server", http.formvalue("id"))) == 0 http_write_json(e) end function server_user_log() - local id = luci.http.formvalue("id") + local id = http.formvalue("id") if fs.access("/tmp/etc/passwall_server/" .. id .. ".log") then local content = luci.sys.exec("cat /tmp/etc/passwall_server/" .. id .. ".log") content = content:gsub("\n", "
") - luci.http.write(content) + http.write(content) else - luci.http.write(string.format("", i18n.translate("Not enabled log"))) + http.write(string.format("", i18n.translate("Not enabled log"))) end end function server_get_log() - luci.http.write(luci.sys.exec("[ -f '/tmp/log/passwall_server.log' ] && cat /tmp/log/passwall_server.log")) + http.write(luci.sys.exec("[ -f '/tmp/log/passwall_server.log' ] && cat /tmp/log/passwall_server.log")) end function server_clear_log() @@ -608,7 +601,7 @@ function create_backup() local tar_file = "/tmp/passwall-" .. date .. "-backup.tar.gz" fs.remove(tar_file) local cmd = "tar -czf " .. tar_file .. " " .. table.concat(backup_files, " ") - api.sys.call(cmd) + luci.sys.call(cmd) http.header("Content-Disposition", "attachment; filename=passwall-" .. date .. "-backup.tar.gz") http.header("X-Backup-Filename", "passwall-" .. date .. "-backup.tar.gz") http.prepare_content("application/octet-stream") @@ -636,25 +629,25 @@ function restore_backup() fp:write(decoded) fp:close() if chunk_index + 1 == total_chunks then - api.sys.call("echo '' > /tmp/log/passwall.log") + luci.sys.call("echo '' > /tmp/log/passwall.log") api.log(" * PassWall 配置文件上传成功…") local temp_dir = '/tmp/passwall_bak' - api.sys.call("mkdir -p " .. temp_dir) - if api.sys.call("tar -xzf " .. file_path .. " -C " .. temp_dir) == 0 then + luci.sys.call("mkdir -p " .. temp_dir) + if luci.sys.call("tar -xzf " .. file_path .. " -C " .. temp_dir) == 0 then for _, backup_file in ipairs(backup_files) do local temp_file = temp_dir .. backup_file if fs.access(temp_file) then - api.sys.call("cp -f " .. temp_file .. " " .. backup_file) + luci.sys.call("cp -f " .. temp_file .. " " .. backup_file) end end api.log(" * PassWall 配置还原成功…") api.log(" * 重启 PassWall 服务中…\n") - api.sys.call('/etc/init.d/passwall restart > /dev/null 2>&1 &') - api.sys.call('/etc/init.d/passwall_server restart > /dev/null 2>&1 &') + luci.sys.call('/etc/init.d/passwall restart > /dev/null 2>&1 &') + luci.sys.call('/etc/init.d/passwall_server restart > /dev/null 2>&1 &') else api.log(" * PassWall 配置文件解压失败,请重试!") end - api.sys.call("rm -rf " .. temp_dir) + luci.sys.call("rm -rf " .. temp_dir) fs.remove(file_path) http_write_json({ status = "success", message = "Upload completed", path = file_path }) else @@ -667,8 +660,8 @@ function restore_backup() end function geo_view() - local action = luci.http.formvalue("action") - local value = luci.http.formvalue("value") + local action = http.formvalue("action") + local value = http.formvalue("value") if not value or value == "" then http.prepare_content("text/plain") http.write(i18n.translate("Please enter query content!")) @@ -717,14 +710,14 @@ function geo_view() end function subscribe_del_node() - local remark = luci.http.formvalue("remark") + local remark = http.formvalue("remark") if remark and remark ~= "" then luci.sys.call("lua /usr/share/" .. appname .. "/subscribe.lua truncate " .. luci.util.shellquote(remark) .. " > /dev/null 2>&1") end - luci.http.status(200, "OK") + http.status(200, "OK") end function subscribe_del_all() luci.sys.call("lua /usr/share/" .. appname .. "/subscribe.lua truncate > /dev/null 2>&1") - luci.http.status(200, "OK") + http.status(200, "OK") end diff --git a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua index c741fea975..5d7a62a8b5 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua @@ -152,7 +152,10 @@ local pi = s:option(Value, _n("probeInterval"), translate("Probe Interval")) pi:depends({ [_n("balancingStrategy")] = "leastPing" }) pi:depends({ [_n("balancingStrategy")] = "leastLoad" }) pi.default = "1m" -pi.description = translate("The interval between initiating probes. The time format is numbers + units, such as '10s', '2h45m', and the supported time units are ns, us, ms, s, m, h, which correspond to nanoseconds, microseconds, milliseconds, seconds, minutes, and hours, respectively.") +pi.placeholder = "1m" +pi.description = translate("The interval between initiating probes.") .. "
" .. + translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are s, m, h, which correspond to seconds, minutes, and hours, respectively.") .. "
" .. + translate("When the unit is not filled in, it defaults to seconds.") if api.compare_versions(xray_version, ">=", "1.8.12") then ucpu:depends({ [_n("protocol")] = "_balancing" }) diff --git a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua index d6b0037e01..3eb6f8bd49 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua @@ -123,20 +123,26 @@ o.description = translate("The URL used to detect the connection status.") o = s:option(Value, _n("urltest_interval"), translate("Test interval")) o:depends({ [_n("protocol")] = "_urltest" }) -o.datatype = "uinteger" -o.default = "180" -o.description = translate("The test interval in seconds.") .. "
" .. +o.default = "3m" +o.placeholder = "3m" +o.description = translate("The interval between initiating probes.") .. "
" .. + translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are s, m, h, which correspond to seconds, minutes, and hours, respectively.") .. "
" .. + translate("When the unit is not filled in, it defaults to seconds.") .. "
" .. translate("Test interval must be less or equal than idle timeout.") o = s:option(Value, _n("urltest_tolerance"), translate("Test tolerance"), translate("The test tolerance in milliseconds.")) o:depends({ [_n("protocol")] = "_urltest" }) o.datatype = "uinteger" +o.placeholder = "50" o.default = "50" -o = s:option(Value, _n("urltest_idle_timeout"), translate("Idle timeout"), translate("The idle timeout in seconds.")) +o = s:option(Value, _n("urltest_idle_timeout"), translate("Idle timeout")) o:depends({ [_n("protocol")] = "_urltest" }) -o.datatype = "uinteger" -o.default = "1800" +o.placeholder = "30m" +o.default = "30m" +o.description = translate("The idle timeout.") .. "
" .. + translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are s, m, h, which correspond to seconds, minutes, and hours, respectively.") .. "
" .. + translate("When the unit is not filled in, it defaults to seconds.") o = s:option(Flag, _n("urltest_interrupt_exist_connections"), translate("Interrupt existing connections")) o:depends({ [_n("protocol")] = "_urltest" }) diff --git a/openwrt-passwall/luci-app-passwall/luasrc/passwall/api.lua b/openwrt-passwall/luci-app-passwall/luasrc/passwall/api.lua index 8ace0fbb95..3e5ef044c0 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/passwall/api.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/passwall/api.lua @@ -1316,3 +1316,33 @@ function get_std_domain(domain) end return domain end + +function format_go_time(input) + input = input and trim(input) + local N = 0 + if input and input:match("^%d+$") then + N = tonumber(input) + elseif input and input ~= "" then + for value, unit in input:gmatch("(%d+)%s*([hms])") do + value = tonumber(value) + if unit == "h" then + N = N + value * 3600 + elseif unit == "m" then + N = N + value * 60 + elseif unit == "s" then + N = N + value + end + end + end + if N <= 0 then + return "0s" + end + local result = "" + local h = math.floor(N / 3600) + local m = math.floor(N % 3600 / 60) + local s = N % 60 + if h > 0 then result = result .. h .. "h" end + if m > 0 then result = result .. m .. "m" end + if s > 0 or result == "" then result = result .. s .. "s" end + return result +end diff --git a/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_sing-box.lua b/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_sing-box.lua index 714cdfa89e..f1f1e7df58 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_sing-box.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_sing-box.lua @@ -1051,9 +1051,9 @@ function gen_config(var) tag = urltest_tag, outbounds = valid_nodes, url = _node.urltest_url or "https://www.gstatic.com/generate_204", - interval = _node.urltest_interval and tonumber(_node.urltest_interval) and string.format("%dm", tonumber(_node.urltest_interval) / 60) or "3m", - tolerance = _node.urltest_tolerance and tonumber(_node.urltest_tolerance) and tonumber(_node.urltest_tolerance) or 50, - idle_timeout = _node.urltest_idle_timeout and tonumber(_node.urltest_idle_timeout) and string.format("%dm", tonumber(_node.urltest_idle_timeout) / 60) or "30m", + interval = (api.format_go_time(_node.urltest_interval) ~= "0s") and api.format_go_time(_node.urltest_interval) or "3m", + tolerance = (_node.urltest_tolerance and tonumber(_node.urltest_tolerance) > 0) and tonumber(_node.urltest_tolerance) or 50, + idle_timeout = (api.format_go_time(_node.urltest_idle_timeout) ~= "0s") and api.format_go_time(_node.urltest_idle_timeout) or "30m", interrupt_exist_connections = (_node.urltest_interrupt_exist_connections == "true" or _node.urltest_interrupt_exist_connections == "1") and true or false } table.insert(outbounds, outbound) diff --git a/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_xray.lua b/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_xray.lua index bf79aa3cb4..b29263531a 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_xray.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_xray.lua @@ -794,7 +794,7 @@ function gen_config(var) subjectSelector = { "blc-" }, pingConfig = { destination = _node.useCustomProbeUrl and _node.probeUrl or nil, - interval = _node.probeInterval or "1m", + interval = (api.format_go_time(_node.probeInterval) ~= "0s") and api.format_go_time(_node.probeInterval) or "1m", sampling = 3, timeout = "5s" } diff --git a/openwrt-passwall/luci-app-passwall/po/zh-cn/passwall.po b/openwrt-passwall/luci-app-passwall/po/zh-cn/passwall.po index 5ba83b06cf..0ffa338b81 100644 --- a/openwrt-passwall/luci-app-passwall/po/zh-cn/passwall.po +++ b/openwrt-passwall/luci-app-passwall/po/zh-cn/passwall.po @@ -460,8 +460,14 @@ msgstr "用于检测连接状态的网址。" msgid "Probe Interval" msgstr "探测间隔" -msgid "The interval between initiating probes. The time format is numbers + units, such as '10s', '2h45m', and the supported time units are ns, us, ms, s, m, h, which correspond to nanoseconds, microseconds, milliseconds, seconds, minutes, and hours, respectively." -msgstr "发起探测的间隔。时间格式为数字+单位,比如"10s", "2h45m",支持的时间单位有 nsusmssmh,分别对应纳秒、微秒、毫秒、秒、分、时。" +msgid "The interval between initiating probes." +msgstr "发起探测的间隔。" + +msgid "The time format is numbers + units, such as '10s', '2h45m', and the supported time units are s, m, h, which correspond to seconds, minutes, and hours, respectively." +msgstr "时间格式为数字+单位,比如"10s", "2h45m",支持的时间单位有 smh,分别对应秒、分、时。" + +msgid "When the unit is not filled in, it defaults to seconds." +msgstr "未填写单位时,默认为秒。" msgid "Preferred Node Count" msgstr "优选节点数量" @@ -1840,9 +1846,6 @@ msgstr "要测试的节点列表,/dev/null") - luci.http.redirect(api.url()) + http.redirect(api.url()) end function hide_menu() api.sh_uci_set(appname, "@global[0]", "hide_from_luci", "1", true) luci.sys.call("rm -rf /tmp/luci-*") luci.sys.call("/etc/init.d/rpcd restart >/dev/null") - luci.http.redirect(luci.dispatcher.build_url("admin", "status", "overview")) + http.redirect(luci.dispatcher.build_url("admin", "status", "overview")) end function link_add_node() -- 分片接收以突破uhttpd的限制 local tmp_file = "/tmp/links.conf" - local chunk = luci.http.formvalue("chunk") - local chunk_index = tonumber(luci.http.formvalue("chunk_index")) - local total_chunks = tonumber(luci.http.formvalue("total_chunks")) + local chunk = http.formvalue("chunk") + local chunk_index = tonumber(http.formvalue("chunk_index")) + local total_chunks = tonumber(http.formvalue("total_chunks")) if chunk and chunk_index ~= nil and total_chunks ~= nil then -- 按顺序拼接到文件 @@ -156,8 +156,8 @@ function link_add_node() end function socks_autoswitch_add_node() - local id = luci.http.formvalue("id") - local key = luci.http.formvalue("key") + local id = http.formvalue("id") + local key = http.formvalue("key") if id and id ~= "" and key and key ~= "" then uci:set(appname, id, "enable_autoswitch", "1") local new_list = uci:get(appname, id, "autoswitch_backup_node") or {} @@ -174,12 +174,12 @@ function socks_autoswitch_add_node() uci:set_list(appname, id, "autoswitch_backup_node", new_list) api.uci_save(uci, appname) end - luci.http.redirect(api.url("socks_config", id)) + http.redirect(api.url("socks_config", id)) end function socks_autoswitch_remove_node() - local id = luci.http.formvalue("id") - local key = luci.http.formvalue("key") + local id = http.formvalue("id") + local key = http.formvalue("key") if id and id ~= "" and key and key ~= "" then uci:set(appname, id, "enable_autoswitch", "1") local new_list = uci:get(appname, id, "autoswitch_backup_node") or {} @@ -191,20 +191,20 @@ function socks_autoswitch_remove_node() uci:set_list(appname, id, "autoswitch_backup_node", new_list) api.uci_save(uci, appname) end - luci.http.redirect(api.url("socks_config", id)) + http.redirect(api.url("socks_config", id)) end function gen_client_config() - local id = luci.http.formvalue("id") + local id = http.formvalue("id") local config_file = api.TMP_PATH .. "/config_" .. id luci.sys.call(string.format("/usr/share/passwall/app.sh run_socks flag=config_%s node=%s bind=127.0.0.1 socks_port=1080 config_file=%s no_run=1", id, id, config_file)) if nixio.fs.access(config_file) then - luci.http.prepare_content("application/json") - luci.http.write(luci.sys.exec("cat " .. config_file)) + http.prepare_content("application/json") + http.write(luci.sys.exec("cat " .. config_file)) luci.sys.call("rm -f " .. config_file) else - luci.http.redirect(api.url("node_list")) + http.redirect(api.url("node_list")) end end @@ -219,13 +219,12 @@ function get_now_use_node() if udp_node then e["UDP"] = udp_node end - luci.http.prepare_content("application/json") - luci.http.write_json(e) + http_write_json(e) end function get_redir_log() - local name = luci.http.formvalue("name") - local proto = luci.http.formvalue("proto") + local name = http.formvalue("name") + local proto = http.formvalue("proto") local path = "/tmp/etc/passwall/acl/" .. name proto = proto:upper() if proto == "UDP" and (uci:get(appname, "@global[0]", "udp_node") or "nil") == "tcp" and not fs.access(path .. "/" .. proto .. ".log") then @@ -234,39 +233,39 @@ function get_redir_log() if fs.access(path .. "/" .. proto .. ".log") then local content = luci.sys.exec("tail -n 19999 ".. path .. "/" .. proto .. ".log") content = content:gsub("\n", "
") - luci.http.write(content) + http.write(content) else - luci.http.write(string.format("", i18n.translate("Not enabled log"))) + http.write(string.format("", i18n.translate("Not enabled log"))) end end function get_socks_log() - local name = luci.http.formvalue("name") + local name = http.formvalue("name") local path = "/tmp/etc/passwall/SOCKS_" .. name .. ".log" if fs.access(path) then local content = luci.sys.exec("cat ".. path) content = content:gsub("\n", "
") - luci.http.write(content) + http.write(content) else - luci.http.write(string.format("", i18n.translate("Not enabled log"))) + http.write(string.format("", i18n.translate("Not enabled log"))) end end function get_chinadns_log() - local flag = luci.http.formvalue("flag") + local flag = http.formvalue("flag") local path = "/tmp/etc/passwall/acl/" .. flag .. "/chinadns_ng.log" if fs.access(path) then local content = luci.sys.exec("tail -n 5000 ".. path) content = content:gsub("\n", "
") - luci.http.write(content) + http.write(content) else - luci.http.write(string.format("", i18n.translate("Not enabled log"))) + http.write(string.format("", i18n.translate("Not enabled log"))) end end function get_log() -- luci.sys.exec("[ -f /tmp/log/passwall.log ] && sed '1!G;h;$!d' /tmp/log/passwall.log > /tmp/log/passwall_show.log") - luci.http.write(luci.sys.exec("[ -f '/tmp/log/passwall.log' ] && cat /tmp/log/passwall.log")) + http.write(luci.sys.exec("[ -f '/tmp/log/passwall.log' ] && cat /tmp/log/passwall.log")) end function clear_log() @@ -292,20 +291,18 @@ function index_status() else e["udp_node_status"] = luci.sys.call("/bin/busybox top -bn1 | grep -v 'grep' | grep '/tmp/etc/passwall/bin/' | grep 'default' | grep 'UDP' >/dev/null") == 0 end - luci.http.prepare_content("application/json") - luci.http.write_json(e) + http_write_json(e) end function haproxy_status() local e = luci.sys.call(string.format("/bin/busybox top -bn1 | grep -v grep | grep '%s/bin/' | grep haproxy >/dev/null", appname)) == 0 - luci.http.prepare_content("application/json") - luci.http.write_json(e) + http_write_json(e) end function socks_status() local e = {} - local index = luci.http.formvalue("index") - local id = luci.http.formvalue("id") + local index = http.formvalue("index") + local id = http.formvalue("id") e.index = index e.socks_status = luci.sys.call(string.format("/bin/busybox top -bn1 | grep -v 'grep' | grep '/tmp/etc/passwall/bin/' | grep -v '_acl_' | grep '%s' | grep 'SOCKS_' > /dev/null", id)) == 0 local use_http = uci:get(appname, id, "http_port") or 0 @@ -314,14 +311,13 @@ function socks_status() e.use_http = 1 e.http_status = luci.sys.call(string.format("/bin/busybox top -bn1 | grep -v 'grep' | grep '/tmp/etc/passwall/bin/' | grep -v '_acl_' | grep '%s' | grep -E 'HTTP_|HTTP2SOCKS' > /dev/null", id)) == 0 end - luci.http.prepare_content("application/json") - luci.http.write_json(e) + http_write_json(e) end function connect_status() local e = {} e.use_time = "" - local url = luci.http.formvalue("url") + local url = http.formvalue("url") local baidu = string.find(url, "baidu") local chn_list = uci:get(appname, "@global[0]", "chn_list") or "direct" local gfw_list = uci:get(appname, "@global[0]", "use_gfw_list") or "1" @@ -357,15 +353,14 @@ function connect_status() e.ping_type = "curl" end end - luci.http.prepare_content("application/json") - http.write(jsonStringify(e)) + http_write_json(e) end function ping_node() - local index = luci.http.formvalue("index") - local address = luci.http.formvalue("address") - local port = luci.http.formvalue("port") - local type = luci.http.formvalue("type") or "icmp" + local index = http.formvalue("index") + local address = http.formvalue("address") + local port = http.formvalue("port") + local type = http.formvalue("type") or "icmp" local e = {} e.index = index if type == "tcping" and luci.sys.exec("echo -n $(command -v tcping)") ~= "" then @@ -376,13 +371,12 @@ function ping_node() else e.ping = luci.sys.exec("echo -n $(ping -c 1 -W 1 %q 2>&1 | grep -o 'time=[0-9]*' | awk -F '=' '{print $2}') 2>/dev/null" % address) end - luci.http.prepare_content("application/json") - luci.http.write_json(e) + http_write_json(e) end function urltest_node() - local index = luci.http.formvalue("index") - local id = luci.http.formvalue("id") + local index = http.formvalue("index") + local id = http.formvalue("id") local e = {} e.index = index local result = luci.sys.exec(string.format("/usr/share/passwall/test.sh url_test_node %s %s", id, "urltest_node")) @@ -398,20 +392,19 @@ function urltest_node() end end end - luci.http.prepare_content("application/json") - luci.http.write_json(e) + http_write_json(e) end function set_node() - local protocol = luci.http.formvalue("protocol") - local section = luci.http.formvalue("section") + local protocol = http.formvalue("protocol") + local section = http.formvalue("section") uci:set(appname, "@global[0]", protocol .. "_node", section) api.uci_save(uci, appname, true, true) - luci.http.redirect(api.url("log")) + http.redirect(api.url("log")) end function copy_node() - local section = luci.http.formvalue("section") + local section = http.formvalue("section") local uuid = api.gen_short_uuid() uci:section(appname, "nodes", uuid) for k, v in pairs(uci:get_all(appname, section)) do @@ -428,7 +421,7 @@ function copy_node() uci:delete(appname, uuid, "add_from") uci:set(appname, uuid, "add_mode", 1) api.uci_save(uci, appname) - luci.http.redirect(api.url("node_config", uuid)) + http.redirect(api.url("node_config", uuid)) end function clear_all_nodes() @@ -459,7 +452,7 @@ function clear_all_nodes() end function delete_select_nodes() - local ids = luci.http.formvalue("ids") + local ids = http.formvalue("ids") string.gsub(ids, '[^' .. "," .. ']+', function(w) if (uci:get(appname, "@global[0]", "tcp_node") or "") == w then uci:delete(appname, '@global[0]', "tcp_node") @@ -518,31 +511,31 @@ function delete_select_nodes() end function update_rules() - local update = luci.http.formvalue("update") + local update = http.formvalue("update") luci.sys.call("lua /usr/share/passwall/rule_update.lua log '" .. update .. "' > /dev/null 2>&1 &") http_write_json() end function server_user_status() local e = {} - e.index = luci.http.formvalue("index") - e.status = luci.sys.call(string.format("/bin/busybox top -bn1 | grep -v 'grep' | grep '%s/bin/' | grep -i '%s' >/dev/null", appname .. "_server", luci.http.formvalue("id"))) == 0 + e.index = http.formvalue("index") + e.status = luci.sys.call(string.format("/bin/busybox top -bn1 | grep -v 'grep' | grep '%s/bin/' | grep -i '%s' >/dev/null", appname .. "_server", http.formvalue("id"))) == 0 http_write_json(e) end function server_user_log() - local id = luci.http.formvalue("id") + local id = http.formvalue("id") if fs.access("/tmp/etc/passwall_server/" .. id .. ".log") then local content = luci.sys.exec("cat /tmp/etc/passwall_server/" .. id .. ".log") content = content:gsub("\n", "
") - luci.http.write(content) + http.write(content) else - luci.http.write(string.format("", i18n.translate("Not enabled log"))) + http.write(string.format("", i18n.translate("Not enabled log"))) end end function server_get_log() - luci.http.write(luci.sys.exec("[ -f '/tmp/log/passwall_server.log' ] && cat /tmp/log/passwall_server.log")) + http.write(luci.sys.exec("[ -f '/tmp/log/passwall_server.log' ] && cat /tmp/log/passwall_server.log")) end function server_clear_log() @@ -608,7 +601,7 @@ function create_backup() local tar_file = "/tmp/passwall-" .. date .. "-backup.tar.gz" fs.remove(tar_file) local cmd = "tar -czf " .. tar_file .. " " .. table.concat(backup_files, " ") - api.sys.call(cmd) + luci.sys.call(cmd) http.header("Content-Disposition", "attachment; filename=passwall-" .. date .. "-backup.tar.gz") http.header("X-Backup-Filename", "passwall-" .. date .. "-backup.tar.gz") http.prepare_content("application/octet-stream") @@ -636,25 +629,25 @@ function restore_backup() fp:write(decoded) fp:close() if chunk_index + 1 == total_chunks then - api.sys.call("echo '' > /tmp/log/passwall.log") + luci.sys.call("echo '' > /tmp/log/passwall.log") api.log(" * PassWall 配置文件上传成功…") local temp_dir = '/tmp/passwall_bak' - api.sys.call("mkdir -p " .. temp_dir) - if api.sys.call("tar -xzf " .. file_path .. " -C " .. temp_dir) == 0 then + luci.sys.call("mkdir -p " .. temp_dir) + if luci.sys.call("tar -xzf " .. file_path .. " -C " .. temp_dir) == 0 then for _, backup_file in ipairs(backup_files) do local temp_file = temp_dir .. backup_file if fs.access(temp_file) then - api.sys.call("cp -f " .. temp_file .. " " .. backup_file) + luci.sys.call("cp -f " .. temp_file .. " " .. backup_file) end end api.log(" * PassWall 配置还原成功…") api.log(" * 重启 PassWall 服务中…\n") - api.sys.call('/etc/init.d/passwall restart > /dev/null 2>&1 &') - api.sys.call('/etc/init.d/passwall_server restart > /dev/null 2>&1 &') + luci.sys.call('/etc/init.d/passwall restart > /dev/null 2>&1 &') + luci.sys.call('/etc/init.d/passwall_server restart > /dev/null 2>&1 &') else api.log(" * PassWall 配置文件解压失败,请重试!") end - api.sys.call("rm -rf " .. temp_dir) + luci.sys.call("rm -rf " .. temp_dir) fs.remove(file_path) http_write_json({ status = "success", message = "Upload completed", path = file_path }) else @@ -667,8 +660,8 @@ function restore_backup() end function geo_view() - local action = luci.http.formvalue("action") - local value = luci.http.formvalue("value") + local action = http.formvalue("action") + local value = http.formvalue("value") if not value or value == "" then http.prepare_content("text/plain") http.write(i18n.translate("Please enter query content!")) @@ -717,14 +710,14 @@ function geo_view() end function subscribe_del_node() - local remark = luci.http.formvalue("remark") + local remark = http.formvalue("remark") if remark and remark ~= "" then luci.sys.call("lua /usr/share/" .. appname .. "/subscribe.lua truncate " .. luci.util.shellquote(remark) .. " > /dev/null 2>&1") end - luci.http.status(200, "OK") + http.status(200, "OK") end function subscribe_del_all() luci.sys.call("lua /usr/share/" .. appname .. "/subscribe.lua truncate > /dev/null 2>&1") - luci.http.status(200, "OK") + http.status(200, "OK") end diff --git a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua index c741fea975..5d7a62a8b5 100644 --- a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua +++ b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua @@ -152,7 +152,10 @@ local pi = s:option(Value, _n("probeInterval"), translate("Probe Interval")) pi:depends({ [_n("balancingStrategy")] = "leastPing" }) pi:depends({ [_n("balancingStrategy")] = "leastLoad" }) pi.default = "1m" -pi.description = translate("The interval between initiating probes. The time format is numbers + units, such as '10s', '2h45m', and the supported time units are ns, us, ms, s, m, h, which correspond to nanoseconds, microseconds, milliseconds, seconds, minutes, and hours, respectively.") +pi.placeholder = "1m" +pi.description = translate("The interval between initiating probes.") .. "
" .. + translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are s, m, h, which correspond to seconds, minutes, and hours, respectively.") .. "
" .. + translate("When the unit is not filled in, it defaults to seconds.") if api.compare_versions(xray_version, ">=", "1.8.12") then ucpu:depends({ [_n("protocol")] = "_balancing" }) diff --git a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua index d6b0037e01..3eb6f8bd49 100644 --- a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua +++ b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua @@ -123,20 +123,26 @@ o.description = translate("The URL used to detect the connection status.") o = s:option(Value, _n("urltest_interval"), translate("Test interval")) o:depends({ [_n("protocol")] = "_urltest" }) -o.datatype = "uinteger" -o.default = "180" -o.description = translate("The test interval in seconds.") .. "
" .. +o.default = "3m" +o.placeholder = "3m" +o.description = translate("The interval between initiating probes.") .. "
" .. + translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are s, m, h, which correspond to seconds, minutes, and hours, respectively.") .. "
" .. + translate("When the unit is not filled in, it defaults to seconds.") .. "
" .. translate("Test interval must be less or equal than idle timeout.") o = s:option(Value, _n("urltest_tolerance"), translate("Test tolerance"), translate("The test tolerance in milliseconds.")) o:depends({ [_n("protocol")] = "_urltest" }) o.datatype = "uinteger" +o.placeholder = "50" o.default = "50" -o = s:option(Value, _n("urltest_idle_timeout"), translate("Idle timeout"), translate("The idle timeout in seconds.")) +o = s:option(Value, _n("urltest_idle_timeout"), translate("Idle timeout")) o:depends({ [_n("protocol")] = "_urltest" }) -o.datatype = "uinteger" -o.default = "1800" +o.placeholder = "30m" +o.default = "30m" +o.description = translate("The idle timeout.") .. "
" .. + translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are s, m, h, which correspond to seconds, minutes, and hours, respectively.") .. "
" .. + translate("When the unit is not filled in, it defaults to seconds.") o = s:option(Flag, _n("urltest_interrupt_exist_connections"), translate("Interrupt existing connections")) o:depends({ [_n("protocol")] = "_urltest" }) diff --git a/small/luci-app-passwall/luasrc/passwall/api.lua b/small/luci-app-passwall/luasrc/passwall/api.lua index 8ace0fbb95..3e5ef044c0 100644 --- a/small/luci-app-passwall/luasrc/passwall/api.lua +++ b/small/luci-app-passwall/luasrc/passwall/api.lua @@ -1316,3 +1316,33 @@ function get_std_domain(domain) end return domain end + +function format_go_time(input) + input = input and trim(input) + local N = 0 + if input and input:match("^%d+$") then + N = tonumber(input) + elseif input and input ~= "" then + for value, unit in input:gmatch("(%d+)%s*([hms])") do + value = tonumber(value) + if unit == "h" then + N = N + value * 3600 + elseif unit == "m" then + N = N + value * 60 + elseif unit == "s" then + N = N + value + end + end + end + if N <= 0 then + return "0s" + end + local result = "" + local h = math.floor(N / 3600) + local m = math.floor(N % 3600 / 60) + local s = N % 60 + if h > 0 then result = result .. h .. "h" end + if m > 0 then result = result .. m .. "m" end + if s > 0 or result == "" then result = result .. s .. "s" end + return result +end diff --git a/small/luci-app-passwall/luasrc/passwall/util_sing-box.lua b/small/luci-app-passwall/luasrc/passwall/util_sing-box.lua index 714cdfa89e..f1f1e7df58 100644 --- a/small/luci-app-passwall/luasrc/passwall/util_sing-box.lua +++ b/small/luci-app-passwall/luasrc/passwall/util_sing-box.lua @@ -1051,9 +1051,9 @@ function gen_config(var) tag = urltest_tag, outbounds = valid_nodes, url = _node.urltest_url or "https://www.gstatic.com/generate_204", - interval = _node.urltest_interval and tonumber(_node.urltest_interval) and string.format("%dm", tonumber(_node.urltest_interval) / 60) or "3m", - tolerance = _node.urltest_tolerance and tonumber(_node.urltest_tolerance) and tonumber(_node.urltest_tolerance) or 50, - idle_timeout = _node.urltest_idle_timeout and tonumber(_node.urltest_idle_timeout) and string.format("%dm", tonumber(_node.urltest_idle_timeout) / 60) or "30m", + interval = (api.format_go_time(_node.urltest_interval) ~= "0s") and api.format_go_time(_node.urltest_interval) or "3m", + tolerance = (_node.urltest_tolerance and tonumber(_node.urltest_tolerance) > 0) and tonumber(_node.urltest_tolerance) or 50, + idle_timeout = (api.format_go_time(_node.urltest_idle_timeout) ~= "0s") and api.format_go_time(_node.urltest_idle_timeout) or "30m", interrupt_exist_connections = (_node.urltest_interrupt_exist_connections == "true" or _node.urltest_interrupt_exist_connections == "1") and true or false } table.insert(outbounds, outbound) diff --git a/small/luci-app-passwall/luasrc/passwall/util_xray.lua b/small/luci-app-passwall/luasrc/passwall/util_xray.lua index bf79aa3cb4..b29263531a 100644 --- a/small/luci-app-passwall/luasrc/passwall/util_xray.lua +++ b/small/luci-app-passwall/luasrc/passwall/util_xray.lua @@ -794,7 +794,7 @@ function gen_config(var) subjectSelector = { "blc-" }, pingConfig = { destination = _node.useCustomProbeUrl and _node.probeUrl or nil, - interval = _node.probeInterval or "1m", + interval = (api.format_go_time(_node.probeInterval) ~= "0s") and api.format_go_time(_node.probeInterval) or "1m", sampling = 3, timeout = "5s" } diff --git a/small/luci-app-passwall/po/zh-cn/passwall.po b/small/luci-app-passwall/po/zh-cn/passwall.po index 5ba83b06cf..0ffa338b81 100644 --- a/small/luci-app-passwall/po/zh-cn/passwall.po +++ b/small/luci-app-passwall/po/zh-cn/passwall.po @@ -460,8 +460,14 @@ msgstr "用于检测连接状态的网址。" msgid "Probe Interval" msgstr "探测间隔" -msgid "The interval between initiating probes. The time format is numbers + units, such as '10s', '2h45m', and the supported time units are ns, us, ms, s, m, h, which correspond to nanoseconds, microseconds, milliseconds, seconds, minutes, and hours, respectively." -msgstr "发起探测的间隔。时间格式为数字+单位,比如"10s", "2h45m",支持的时间单位有 nsusmssmh,分别对应纳秒、微秒、毫秒、秒、分、时。" +msgid "The interval between initiating probes." +msgstr "发起探测的间隔。" + +msgid "The time format is numbers + units, such as '10s', '2h45m', and the supported time units are s, m, h, which correspond to seconds, minutes, and hours, respectively." +msgstr "时间格式为数字+单位,比如"10s", "2h45m",支持的时间单位有 smh,分别对应秒、分、时。" + +msgid "When the unit is not filled in, it defaults to seconds." +msgstr "未填写单位时,默认为秒。" msgid "Preferred Node Count" msgstr "优选节点数量" @@ -1840,9 +1846,6 @@ msgstr "要测试的节点列表,
+ - \ No newline at end of file diff --git a/yt-dlp/test/test_jsinterp.py b/yt-dlp/test/test_jsinterp.py index a1088cea49..43b1d0fdee 100644 --- a/yt-dlp/test/test_jsinterp.py +++ b/yt-dlp/test/test_jsinterp.py @@ -536,6 +536,11 @@ class TestJSInterpreter(unittest.TestCase): } ''', 31) + def test_undefined_varnames(self): + jsi = JSInterpreter('function f(){ var a; return [a, b]; }') + self._test(jsi, [JS_Undefined, JS_Undefined]) + self.assertEqual(jsi._undefined_varnames, {'b'}) + if __name__ == '__main__': unittest.main() diff --git a/yt-dlp/test/test_youtube_signature.py b/yt-dlp/test/test_youtube_signature.py index 98607df55e..4562467534 100644 --- a/yt-dlp/test/test_youtube_signature.py +++ b/yt-dlp/test/test_youtube_signature.py @@ -373,6 +373,10 @@ _NSIG_TESTS = [ 'https://www.youtube.com/s/player/e12fbea4/player_ias_tce.vflset/en_US/base.js', 'kM5r52fugSZRAKHfo3', 'XkeRfXIPOkSwfg', ), + ( + 'https://www.youtube.com/s/player/ef259203/player_ias_tce.vflset/en_US/base.js', + 'rPqBC01nJpqhhi2iA2U', 'hY7dbiKFT51UIA', + ), ] diff --git a/yt-dlp/yt_dlp/extractor/_extractors.py b/yt-dlp/yt_dlp/extractor/_extractors.py index ada12b3a8a..84da570b0a 100644 --- a/yt-dlp/yt_dlp/extractor/_extractors.py +++ b/yt-dlp/yt_dlp/extractor/_extractors.py @@ -1147,6 +1147,7 @@ from .minds import ( MindsIE, ) from .minoto import MinotoIE +from .mir24tv import Mir24TvIE from .mirrativ import ( MirrativIE, MirrativUserIE, diff --git a/yt-dlp/yt_dlp/extractor/mir24tv.py b/yt-dlp/yt_dlp/extractor/mir24tv.py new file mode 100644 index 0000000000..5832901bf1 --- /dev/null +++ b/yt-dlp/yt_dlp/extractor/mir24tv.py @@ -0,0 +1,37 @@ +from .common import InfoExtractor +from ..utils import parse_qs, url_or_none +from ..utils.traversal import require, traverse_obj + + +class Mir24TvIE(InfoExtractor): + IE_NAME = 'mir24.tv' + _VALID_URL = r'https?://(?:www\.)?mir24\.tv/news/(?P[0-9]+)/[^/?#]+' + _TESTS = [{ + 'url': 'https://mir24.tv/news/16635210/dni-kultury-rossii-otkrylis-v-uzbekistane.-na-prazdnichnom-koncerte-vystupili-zvezdy-rossijskoj-estrada', + 'info_dict': { + 'id': '16635210', + 'title': 'Дни культуры России открылись в Узбекистане. На праздничном концерте выступили звезды российской эстрады', + 'ext': 'mp4', + 'thumbnail': r're:https://images\.mir24\.tv/.+\.jpg', + }, + }] + + def _real_extract(self, url): + video_id = self._match_id(url) + webpage = self._download_webpage(url, video_id, impersonate=True) + + iframe_url = self._search_regex( + r']+\bsrc=["\'](https?://mir24\.tv/players/[^"\']+)', + webpage, 'iframe URL') + + m3u8_url = traverse_obj(iframe_url, ( + {parse_qs}, 'source', -1, {self._proto_relative_url}, {url_or_none}, {require('m3u8 URL')})) + formats, subtitles = self._extract_m3u8_formats_and_subtitles(m3u8_url, video_id, 'mp4', m3u8_id='hls') + + return { + 'id': video_id, + 'title': self._og_search_title(webpage, default=None) or self._html_extract_title(webpage), + 'thumbnail': self._og_search_thumbnail(webpage, default=None), + 'formats': formats, + 'subtitles': subtitles, + } diff --git a/yt-dlp/yt_dlp/extractor/newspicks.py b/yt-dlp/yt_dlp/extractor/newspicks.py index 4a1cb0a735..5f19eed984 100644 --- a/yt-dlp/yt_dlp/extractor/newspicks.py +++ b/yt-dlp/yt_dlp/extractor/newspicks.py @@ -1,53 +1,72 @@ -import re - from .common import InfoExtractor -from ..utils import ExtractorError +from ..utils import ( + clean_html, + parse_iso8601, + parse_qs, + url_or_none, +) +from ..utils.traversal import require, traverse_obj class NewsPicksIE(InfoExtractor): - _VALID_URL = r'https?://newspicks\.com/movie-series/(?P\d+)\?movieId=(?P\d+)' - + _VALID_URL = r'https?://newspicks\.com/movie-series/(?P[^?/#]+)' _TESTS = [{ - 'url': 'https://newspicks.com/movie-series/11?movieId=1813', + 'url': 'https://newspicks.com/movie-series/11/?movieId=1813', 'info_dict': { 'id': '1813', - 'title': '日本の課題を破壊せよ【ゲスト:成田悠輔】', - 'description': 'md5:09397aad46d6ded6487ff13f138acadf', - 'channel': 'HORIE ONE', - 'channel_id': '11', - 'release_date': '20220117', - 'thumbnail': r're:https://.+jpg', 'ext': 'mp4', + 'title': '日本の課題を破壊せよ【ゲスト:成田悠輔】', + 'cast': 'count:4', + 'description': 'md5:09397aad46d6ded6487ff13f138acadf', + 'duration': 2940, + 'release_date': '20220117', + 'release_timestamp': 1642424400, + 'series': 'HORIE ONE', + 'series_id': '11', + 'thumbnail': r're:https?://resources\.newspicks\.com/.+\.(?:jpe?g|png)', + 'timestamp': 1642424420, + 'upload_date': '20220117', + }, + }, { + 'url': 'https://newspicks.com/movie-series/158/?movieId=3932', + 'info_dict': { + 'id': '3932', + 'ext': 'mp4', + 'title': '【検証】専門家は、KADOKAWAをどう見るか', + 'cast': 'count:3', + 'description': 'md5:2c2d4bf77484a4333ec995d676f9a91d', + 'duration': 1320, + 'release_date': '20240622', + 'release_timestamp': 1719088080, + 'series': 'NPレポート', + 'series_id': '158', + 'thumbnail': r're:https?://resources\.newspicks\.com/.+\.(?:jpe?g|png)', + 'timestamp': 1719086400, + 'upload_date': '20240622', }, }] def _real_extract(self, url): - video_id, channel_id = self._match_valid_url(url).group('id', 'channel_id') + series_id = self._match_id(url) + video_id = traverse_obj(parse_qs(url), ('movieId', -1, {str}, {require('movie ID')})) webpage = self._download_webpage(url, video_id) - entries = self._parse_html5_media_entries( - url, webpage.replace('movie-for-pc', 'movie'), video_id, 'hls') - if not entries: - raise ExtractorError('No HTML5 media elements found') - info = entries[0] - title = self._html_search_meta('og:title', webpage, fatal=False) - description = self._html_search_meta( - ('og:description', 'twitter:title'), webpage, fatal=False) - channel = self._html_search_regex( - r'value="11".+?(.+?)\s*(\d+)年(\d+)月(\d+)日\s*', - webpage, 'release date', fatal=False, group=(1, 2, 3)) - - info.update({ + return { 'id': video_id, - 'title': title, - 'description': description, - 'channel': channel, - 'channel_id': channel_id, - 'release_date': ('%04d%02d%02d' % tuple(map(int, release_date))) if release_date else None, - }) - return info + 'formats': formats, + 'series': traverse_obj(fragment, ('series', 'title', {str})), + 'series_id': series_id, + 'subtitles': subtitles, + **traverse_obj(fragment, ('movie', { + 'title': ('title', {str}), + 'cast': ('relatedUsers', ..., 'displayName', {str}, filter, all, filter), + 'description': ('explanation', {clean_html}), + 'release_timestamp': ('onAirStartDate', {parse_iso8601}), + 'thumbnail': (('image', 'coverImageUrl'), {url_or_none}, any), + 'timestamp': ('published', {parse_iso8601}), + })), + } diff --git a/yt-dlp/yt_dlp/extractor/ninegag.py b/yt-dlp/yt_dlp/extractor/ninegag.py index 2979f3a50e..1b88e9c544 100644 --- a/yt-dlp/yt_dlp/extractor/ninegag.py +++ b/yt-dlp/yt_dlp/extractor/ninegag.py @@ -1,6 +1,5 @@ from .common import InfoExtractor from ..utils import ( - ExtractorError, determine_ext, int_or_none, traverse_obj, @@ -61,10 +60,10 @@ class NineGagIE(InfoExtractor): post = self._download_json( 'https://9gag.com/v1/post', post_id, query={ 'id': post_id, - })['data']['post'] + }, impersonate=True)['data']['post'] if post.get('type') != 'Animated': - raise ExtractorError( + self.raise_no_formats( 'The given url does not contain a video', expected=True) diff --git a/yt-dlp/yt_dlp/jsinterp.py b/yt-dlp/yt_dlp/jsinterp.py index f06d96832f..460bc2c03e 100644 --- a/yt-dlp/yt_dlp/jsinterp.py +++ b/yt-dlp/yt_dlp/jsinterp.py @@ -677,8 +677,9 @@ class JSInterpreter: # Set value as JS_Undefined or its pre-existing value local_vars.set_local(var, ret) else: - ret = local_vars.get(var, JS_Undefined) - if ret is JS_Undefined: + ret = local_vars.get(var, NO_DEFAULT) + if ret is NO_DEFAULT: + ret = JS_Undefined self._undefined_varnames.add(var) return ret, should_return