mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-27 06:12:51 +08:00
411 lines
20 KiB
Lua
411 lines
20 KiB
Lua
-- p1_fixes_test.lua — P1 批次缺陷修复的回归测试
|
||
-- 用法:lua5.1 tests/p1_fixes_test.lua
|
||
--
|
||
-- 覆盖本轮修复(每项都先在修改前用探针复现过,再断言修复后的行为):
|
||
-- M1 trojan 密码未做 url_decode
|
||
-- M2 vmess classic JSON 忽略 host / path
|
||
-- M3 SSR 密码 base64 回退不可达
|
||
-- M4 vless/trojan/vmess/hysteria2/tuic/ss 缺 host/port 校验
|
||
-- M5 sing-box YAML 嵌套 tls: map 被跳过(含嵌套序列 alpn / utls.fingerprint)
|
||
-- M7 通用 JSON 节点路径忽略 type 字段、无协议白名单
|
||
-- M8 Surge 段名大小写敏感([PROXY] 不识别)
|
||
-- M10 userinfo 按首个 @ 切分(密码含 @ 时错位)
|
||
-- M12 core.load 遇到非 table 条目直接崩溃
|
||
-- M15 改名替换串里的 % 未转义(吞字符 / 注入 NUL 字节)
|
||
-- M20 output_v2ray.supported() 只排除 ssr
|
||
-- M21 clashmeta 从不写 flow
|
||
-- M22 clashmeta 丢弃 grpc / h2 传输参数
|
||
-- 另:Surge/QX 未知协议白名单(H6 的 Surge 侧)、简易 YAML 嵌套序列
|
||
|
||
package.path = "./root/usr/share/?.lua;" .. package.path
|
||
|
||
local util = require("substore.util")
|
||
local parser = require("substore.parser")
|
||
local surge = require("substore.parser_surge")
|
||
local node = require("substore.node")
|
||
local core = require("substore.core")
|
||
local output = require("substore.output")
|
||
local output_clash_meta = require("substore.output_clash_meta")
|
||
|
||
local passed, failed = 0, 0
|
||
local function check(name, cond)
|
||
if cond then passed = passed + 1; print("PASS " .. name)
|
||
else failed = failed + 1; print("FAIL " .. name) end
|
||
end
|
||
|
||
-- 解析单条 URI,返回节点(或 nil)
|
||
local function one(uri)
|
||
local r = parser.parse(uri)
|
||
return r and r.nodes and r.nodes[1] or nil
|
||
end
|
||
|
||
-- ---------- M1:trojan 密码必须 url_decode ----------
|
||
-- 未解码时密码会是字面量 "p%40ss%3Aword",认证必然失败。
|
||
local t1 = one("trojan://p%40ss%3Aword@1.2.3.4:443?sni=a.com#T")
|
||
check("M1 trojan parsed", t1 ~= nil and t1.proto == "trojan")
|
||
check("M1 trojan password url_decoded", t1 and t1.password == "p@ss:word")
|
||
check("M1 trojan server/port", t1 and t1.server == "1.2.3.4" and t1.port == 443)
|
||
|
||
-- ---------- M2:vmess classic JSON 的 host / path ----------
|
||
-- 未修复时 ws 的 Host 头与 path 全丢,服务端按默认路径匹配失败。
|
||
local vm_json = util.json_encode({
|
||
v = "2", ps = "V", add = "1.2.3.4", port = "443", id = "u", aid = "0",
|
||
net = "ws", type = "none", host = "ws.example.com", path = "/vmpath",
|
||
tls = "tls", sni = "a.com",
|
||
})
|
||
local t2 = one("vmess://" .. util.base64_encode(vm_json))
|
||
check("M2 vmess classic parsed", t2 ~= nil and t2.proto == "vmess")
|
||
check("M2 vmess keeps host", t2 and t2.host == "ws.example.com")
|
||
check("M2 vmess keeps path", t2 and t2.path == "/vmpath")
|
||
check("M2 vmess keeps net", t2 and t2.net == "ws")
|
||
|
||
-- 未提供 host/path 时不应凭空造出字段
|
||
local t2b = one("vmess://" .. util.base64_encode(util.json_encode({
|
||
v = "2", ps = "V2", add = "1.2.3.4", port = "443", id = "u", aid = "0", net = "tcp",
|
||
})))
|
||
check("M2 absent host stays nil", t2b and t2b.host == nil)
|
||
check("M2 absent path stays nil", t2b and t2b.path == nil)
|
||
|
||
-- ---------- M3:SSR 密码 base64 回退 ----------
|
||
-- 密码为纯 base64(非 base64url)时也必须解出明文,而不是乱码。
|
||
local ssr_main = "1.2.3.4:8080:origin:aes-128-cfb:plain:"
|
||
.. util.base64_url_encode("plainpass")
|
||
.. "/?remarks=" .. util.base64_url_encode("SSR")
|
||
local t3 = one("ssr://" .. util.base64_url_encode(ssr_main))
|
||
check("M3 ssr parsed", t3 ~= nil and t3.proto == "ssr")
|
||
check("M3 ssr password decoded", t3 and t3.password == "plainpass")
|
||
check("M3 ssr remarks decoded", t3 and t3.name == "SSR")
|
||
check("M3 ssr method/protocol/obfs", t3 and t3.method == "aes-128-cfb"
|
||
and t3.protocol == "origin" and t3.obfs == "plain")
|
||
|
||
-- 非 ASCII 备注必须原样还原(base64url → UTF-8),不能被解成乱码
|
||
local ssr_plain = "1.2.3.4:8080:origin:aes-128-cfb:plain:"
|
||
.. util.base64_url_encode("p") .. "/?remarks=" .. util.base64_url_encode("香港节点")
|
||
local t3b = one("ssr://" .. util.base64_url_encode(ssr_plain))
|
||
check("M3 ssr utf8 remarks preserved", t3b and t3b.name == "香港节点")
|
||
|
||
-- 外层用 base64url 字母表(含 - / _)的 ssr:// 链接同样必须正确解析。
|
||
-- 标准 base64 解码会把 -/_ 当非法字符**剔除**(util.lua 的 `[^%w%+/=]`),
|
||
-- 整串少掉若干字符、解成乱码,且不报错——用户只看到一个连不上的节点。
|
||
-- 这里的 server 取 1.2.3.100 是因为该 body 的标准 base64 恰好含 "/",
|
||
-- base64url 编码后变成 "_"(已实测确认)。
|
||
local ssr_b64u = "1.2.3.100:8080:origin:aes-128-cfb:plain:"
|
||
.. util.base64_url_encode("pw") .. "/?remarks=" .. util.base64_url_encode("U")
|
||
local outer = util.base64_url_encode(ssr_b64u)
|
||
check("M3 outer really uses base64url alphabet",
|
||
outer:find("-", 1, true) ~= nil or outer:find("_", 1, true) ~= nil)
|
||
-- 前提校验:用标准 base64 解码这份 base64url 串确实会出错(复现原缺陷)
|
||
local t3c_std = one("ssr://" .. outer)
|
||
check("M3 base64url outer decodes", t3c_std ~= nil and t3c_std.proto == "ssr")
|
||
check("M3 base64url outer server", t3c_std and t3c_std.server == "1.2.3.100")
|
||
check("M3 base64url outer password", t3c_std and t3c_std.password == "pw")
|
||
check("M3 base64url outer name", t3c_std and t3c_std.name == "U")
|
||
-- 标准 base64 外层仍然照旧
|
||
local outer_std = util.base64_encode(ssr_b64u)
|
||
local t3d = one("ssr://" .. outer_std)
|
||
check("M3 standard outer still works", t3d ~= nil and t3d.proto == "ssr"
|
||
and t3d.server == "1.2.3.100" and t3d.password == "pw")
|
||
|
||
-- ---------- M4:host / port 校验 ----------
|
||
-- 残缺节点会被写成 `server: ` / `port: 0`,mihomo 与 sing-box 会拒绝加载
|
||
-- 整份配置——一个节点废掉整个订阅。
|
||
local bad_uris = {
|
||
["vless port 0"] = "vless://uuid@1.2.3.4:0#A",
|
||
["vless port missing"] = "vless://uuid@1.2.3.4#B",
|
||
["vless port > 65535"] = "vless://uuid@1.2.3.4:99999#C",
|
||
["trojan port 0"] = "trojan://p@1.2.3.4:0#D",
|
||
["ss port 0"] = "ss://YWVzLTI1Ni1nY206cA==@1.2.3.4:0#E",
|
||
["hysteria2 port missing"] = "hysteria2://p@1.2.3.4#F",
|
||
["tuic port 0"] = "tuic://u:p@1.2.3.4:0#G",
|
||
}
|
||
for label, uri in pairs(bad_uris) do
|
||
local r = parser.parse(uri)
|
||
check("M4 dropped: " .. label, r ~= nil and #r.nodes == 0)
|
||
end
|
||
|
||
-- 合法边界值必须保留
|
||
local ok_port = one("vless://uuid@1.2.3.4:65535#OK")
|
||
check("M4 keeps port 65535", ok_port ~= nil and ok_port.port == 65535)
|
||
local ok_port1 = one("vless://uuid@1.2.3.4:1#OK1")
|
||
check("M4 keeps port 1", ok_port1 ~= nil and ok_port1.port == 1)
|
||
|
||
-- ---------- M5:sing-box YAML 嵌套 tls: map ----------
|
||
-- tls 是嵌套 map,且内部还含嵌套序列(alpn)与嵌套 map(utls)。
|
||
-- 未修复时整层 TLS 丢失:trojan/vmess/vless 静默退化成明文,
|
||
-- hysteria2/tuic 让客户端以 C.ErrTLSRequired 拒绝启动。
|
||
local sb_yaml = [[
|
||
outbounds:
|
||
- type: vless
|
||
tag: sb
|
||
server: 1.2.3.4
|
||
server_port: 443
|
||
uuid: u-1
|
||
flow: xtls-rprx-vision
|
||
tls:
|
||
enabled: true
|
||
server_name: sni.example.com
|
||
insecure: true
|
||
alpn:
|
||
- h2
|
||
- http/1.1
|
||
utls:
|
||
enabled: true
|
||
fingerprint: chrome
|
||
]]
|
||
local t5 = one(sb_yaml)
|
||
check("M5 sing-box yaml parsed", t5 ~= nil and t5.proto == "vless")
|
||
check("M5 tls enabled -> security", t5 and t5.security == "tls")
|
||
check("M5 server_name -> sni", t5 and t5.sni == "sni.example.com")
|
||
check("M5 insecure -> skip-cert-verify", t5 and t5["skip-cert-verify"] == true)
|
||
check("M5 flow preserved", t5 and t5.flow == "xtls-rprx-vision")
|
||
check("M5 alpn nested sequence", t5 and type(t5.alpn) == "table"
|
||
and t5.alpn[1] == "h2" and t5.alpn[2] == "http/1.1")
|
||
-- utls 排在 alpn 之后:若嵌套序列不被支持,read_map 会在 alpn 处截断,
|
||
-- 这个键会连带消失(这正是 read_seq 要解决的问题)
|
||
check("M5 utls fingerprint -> fp", t5 and t5.fp == "chrome")
|
||
-- 嵌套 map 不得以 table 形式漏到 tls 字段(table 恒为真值,
|
||
-- 会被下游的 `if n.tls then` 当成「已启用 TLS」)
|
||
check("M5 tls table not leaked", t5 and type(t5.tls) ~= "table")
|
||
|
||
-- tls.enabled 为假时不得开启 TLS。
|
||
-- 用 vless 而非 trojan:trojan 是 TLS-only,node.DEFAULTS 会强制补 security="tls",
|
||
-- 用它测不出「tls 块存在但未启用」这条分支(vless 的默认值是 "none")。
|
||
local sb_off = [[
|
||
outbounds:
|
||
- type: vless
|
||
tag: off
|
||
server: 1.2.3.4
|
||
server_port: 443
|
||
uuid: u-off
|
||
tls:
|
||
enabled: false
|
||
server_name: s.example.com
|
||
]]
|
||
local t5b = one(sb_off)
|
||
check("M5 tls disabled stays off", t5b and (t5b.security == nil or t5b.security == "none"))
|
||
|
||
-- ---------- M7:通用 JSON 节点用 type 映射协议 + 白名单 ----------
|
||
local t7 = one(util.json_encode({
|
||
{ type = "trojan", server = "1.2.3.4", port = 443, password = "p", name = "J" },
|
||
}))
|
||
check("M7 json type -> proto", t7 ~= nil and t7.proto == "trojan")
|
||
check("M7 json password kept", t7 and t7.password == "p")
|
||
-- type 已被消费为协议,不能原样留在节点上(vmess 的 type 是 header 类型,语义不同)
|
||
check("M7 type consumed", t7 and t7.type == nil)
|
||
|
||
-- 未知类型必须丢弃,而不是兜底成 vmess 造出字段全错的假节点
|
||
local t7b = parser.parse(util.json_encode({
|
||
{ type = "snell", server = "1.2.3.4", port = 443, password = "p" },
|
||
}))
|
||
check("M7 unknown type dropped", t7b ~= nil and #t7b.nodes == 0)
|
||
|
||
-- 无 server/port 的条目丢弃
|
||
local t7c = parser.parse(util.json_encode({
|
||
{ type = "vmess", server = "", port = 443, uuid = "u" },
|
||
}))
|
||
check("M7 empty server dropped", t7c ~= nil and #t7c.nodes == 0)
|
||
|
||
-- ---------- M8:Surge 段名大小写不敏感 ----------
|
||
check("M8 is_config [Proxy]", surge.is_config("[Proxy]\nA = vmess, 1.2.3.4, 443\n"))
|
||
check("M8 is_config [PROXY]", surge.is_config("[PROXY]\nA = vmess, 1.2.3.4, 443\n"))
|
||
check("M8 is_config [server_local]", surge.is_config("[server_local]\nvmess=1.2.3.4:443\n"))
|
||
check("M8 is_config negative", not surge.is_config("proxies:\n - name: x\n"))
|
||
-- 全大写段名必须能整份解析(未修复时被判成 JSON 数组 → 整份报错)
|
||
local t8 = parser.parse("[PROXY]\nA = vmess, 1.2.3.4, 443, username=u\n")
|
||
check("M8 uppercase section parses", t8 ~= nil and #t8.nodes == 1)
|
||
check("M8 uppercase node proto", t8 and t8.nodes[1] and t8.nodes[1].proto == "vmess")
|
||
|
||
-- Surge / QX 未知协议白名单(H6 的 Surge 侧)
|
||
check("M8/H6 surge unknown proto dropped",
|
||
#surge.parse("[Proxy]\nA = snell, 1.2.3.4, 443, psk=x\n") == 0)
|
||
check("M8/H6 qx unknown proto dropped",
|
||
#surge.parse("[server_local]\nssh=1.2.3.4:22, password=p\n") == 0)
|
||
-- 已知协议仍正常
|
||
check("M8/H6 surge known proto kept",
|
||
#surge.parse("[Proxy]\nA = trojan, 1.2.3.4, 443, password=p\n") == 1)
|
||
|
||
-- ---------- M10:userinfo 按最后一个 @ 切分 ----------
|
||
-- 密码里含未转义的 @ 时,按首个 @ 切分会把密码截断、host 变成 "ss@1.2.3.4"。
|
||
local t10 = one("hysteria2://p@ss@1.2.3.4:443?sni=a.com#H")
|
||
check("M10 hysteria2 parsed", t10 ~= nil and t10.proto == "hysteria2")
|
||
check("M10 password keeps @", t10 and t10.password == "p@ss")
|
||
check("M10 server correct", t10 and t10.server == "1.2.3.4")
|
||
check("M10 port correct", t10 and t10.port == 443)
|
||
|
||
-- 多个 @ 也按最后一个切
|
||
local t10b = one("trojan://a@b@c@1.2.3.4:443#T")
|
||
check("M10 multiple @ in password", t10b and t10b.password == "a@b@c"
|
||
and t10b.server == "1.2.3.4")
|
||
|
||
-- ---------- M12:core.load 遇坏条目不崩溃 ----------
|
||
local tmp = os.tmpname() .. "_p1fix"
|
||
os.remove(tmp)
|
||
core.DATA_DIR = tmp
|
||
core.LIST_FILE = tmp .. "/subscriptions.json"
|
||
core.NODES_DIR = tmp .. "/nodes"
|
||
core.ensure_dirs()
|
||
os.execute("mkdir -p " .. util.shq(tmp))
|
||
local fh = io.open(core.LIST_FILE, "w")
|
||
fh:write('{"seq":3,"items":{"good":{"name":"G","url":"http://e.com/s"},"bad":"not-a-table"}}')
|
||
fh:close()
|
||
|
||
-- 未修复时 M.list 里的 pairs(meta) 会抛
|
||
-- "bad argument #1 to 'pairs' (table expected, got string)",订阅列表页直接 500。
|
||
-- 注意 core.load 是模块内局部函数,对外只能通过 core.list() 观察。
|
||
local arr, lerr = core.list()
|
||
check("M12 list does not crash", type(arr) == "table")
|
||
check("M12 good entry kept", #arr == 1 and arr[1].id == "good" and arr[1].name == "G")
|
||
check("M12 corruption reported", type(lerr) == "string" and lerr:find("corrupted") ~= nil)
|
||
|
||
-- 坏条目不能因为「静默过滤」而被当成正常列表:写路径必须拒绝落盘,
|
||
-- 否则下一次 save 会把坏条目永久抹掉
|
||
local wrote = core.save_meta("good", { upload = 1 })
|
||
check("M12 write refused on corruption", wrote == nil or wrote == false)
|
||
|
||
-- 全部条目合法时不报错
|
||
local fh2 = io.open(core.LIST_FILE, "w")
|
||
fh2:write('{"_seq":4,"items":{"a":{"name":"A"}}}')
|
||
fh2:close()
|
||
local arr2, lerr2 = core.list()
|
||
check("M12 clean file no error", lerr2 == nil and #arr2 == 1 and arr2[1].id == "a")
|
||
os.execute("rm -rf " .. util.shq(tmp))
|
||
|
||
-- ---------- M15:改名替换串的 % 与 $ ----------
|
||
local function rename_one(name, rules)
|
||
local ns = node.rename_with_rules(
|
||
{ { name = name, proto = "vmess", server = "1.2.3.4", port = 443 } }, rules)
|
||
return ns[1].name
|
||
end
|
||
|
||
-- 结尾的 % 未转义会注入 NUL 字节("100%" → "100\0"),
|
||
-- 节点名会写进节点文件并下发给所有客户端
|
||
local m15a = rename_one("x", { { type = "regex", pattern = "x", replacement = "100%" } })
|
||
check("M15 trailing percent preserved", m15a == "100%")
|
||
check("M15 no NUL injected", m15a:find("%z") == nil)
|
||
check("M15 percent length exact", #m15a == 4)
|
||
|
||
-- % 后接非数字字符未转义会被静默吞掉("50%off" → "50off")
|
||
local m15b = rename_one("x", { { type = "regex", pattern = "x", replacement = "50%off" } })
|
||
check("M15 percent before letter preserved", m15b == "50%off")
|
||
check("M15 percent before letter length", #m15b == 6)
|
||
|
||
-- 捕获引用仍可用
|
||
local m15c = rename_one("ab", { { type = "regex", pattern = "(a)(b)", replacement = "$2$1" } })
|
||
check("M15 backreference still works", m15c == "ba")
|
||
|
||
-- %% 按字面 % 处理(用户写了两个就得到两个)
|
||
local m15d = rename_one("x", { { type = "regex", pattern = "x", replacement = "a%%b" } })
|
||
check("M15 double percent literal", m15d == "a%%b")
|
||
|
||
-- ---------- M20:output_v2ray 协议白名单 ----------
|
||
-- Xray 只认 vmess/vless/trojan/shadowsocks/socks/http;其余协议没有对应
|
||
-- outbound 类型,写成 `"protocol": "hysteria2"` 会让 Xray 拒绝整份配置。
|
||
local m20_nodes = {
|
||
{ proto = "vmess", name = "V", server = "1.1.1.1", port = 443, uuid = "u" },
|
||
{ proto = "vless", name = "L", server = "2.2.2.2", port = 443, uuid = "u2" },
|
||
{ proto = "trojan", name = "T", server = "3.3.3.3", port = 443, password = "p" },
|
||
{ proto = "ss", name = "S", server = "4.4.4.4", port = 8388, method = "aes-256-gcm", password = "p" },
|
||
{ proto = "socks", name = "SK", server = "5.5.5.5", port = 1080, username = "u", password = "p" },
|
||
{ proto = "hysteria2", name = "H2", server = "6.6.6.6", port = 443, password = "p" },
|
||
{ proto = "hysteria", name = "H1", server = "7.7.7.7", port = 443, password = "p" },
|
||
{ proto = "tuic", name = "TU", server = "8.8.8.8", port = 443, uuid = "u3", password = "p" },
|
||
{ proto = "wireguard", name = "WG", server = "9.9.9.9", port = 51820, ["private-key"] = "k" },
|
||
{ proto = "ssr", name = "SSR", server = "10.10.10.10", port = 8080, password = "p" },
|
||
}
|
||
local m20_out = output.generate(m20_nodes, "v2ray")
|
||
local m20_cfg = util.json_decode(m20_out)
|
||
local m20_protos = {}
|
||
for _, o in ipairs(m20_cfg and m20_cfg.outbounds or {}) do
|
||
m20_protos[o.protocol] = true
|
||
end
|
||
for _, bad in ipairs({ "hysteria2", "hysteria", "tuic", "wireguard", "ssr" }) do
|
||
check("M20 v2ray drops " .. bad, not m20_protos[bad])
|
||
end
|
||
check("M20 v2ray keeps vmess", m20_protos.vmess)
|
||
check("M20 v2ray keeps vless", m20_protos.vless)
|
||
check("M20 v2ray keeps trojan", m20_protos.trojan)
|
||
check("M20 v2ray maps ss ->shadowsocks", m20_protos.shadowsocks)
|
||
check("M20 v2ray keeps socks", m20_protos.socks)
|
||
check("M20 v2ray keeps freedom/blackhole", m20_protos.freedom and m20_protos.blackhole)
|
||
-- 被放行的 socks 凭据必须真的写进 users(否则等于放行了个空壳)
|
||
local m20_socks
|
||
for _, o in ipairs(m20_cfg.outbounds) do
|
||
if o.protocol == "socks" then m20_socks = o end
|
||
end
|
||
check("M20 socks credentials kept", m20_socks ~= nil
|
||
and m20_socks.settings.servers[1].users[1].user == "u")
|
||
local m20_ss
|
||
for _, o in ipairs(m20_cfg.outbounds) do
|
||
if o.protocol == "shadowsocks" then m20_ss = o end
|
||
end
|
||
check("M20 ss method kept", m20_ss ~= nil
|
||
and m20_ss.settings.servers[1].method == "aes-256-gcm")
|
||
-- 被丢弃的节点不能残留出站(tag 会进 balancer selector)
|
||
check("M20 dropped nodes produce no outbound",
|
||
#m20_cfg.outbounds == 7) -- vmess/vless/trojan/ss/socks + freedom/blackhole
|
||
|
||
-- ---------- M21:clashmeta 写 flow ----------
|
||
-- flow 是 XTLS Vision(xtls-rprx-vision)的必需参数;不写的话 mihomo 按普通
|
||
-- vless 处理,服务端要求 vision 时握手失败。surge / v2ray / URI 三个输出都写
|
||
-- flow,只有 clashmeta 漏了。
|
||
local m21 = output_clash_meta.generate({
|
||
{ proto = "vless", name = "L", server = "1.1.1.1", port = 443,
|
||
uuid = "u", flow = "xtls-rprx-vision", security = "tls", sni = "e.com" },
|
||
})
|
||
check("M21 vless flow written", m21:find("flow: xtls%-rprx%-vision") ~= nil)
|
||
-- vmess 没有 flow 概念,不得凭空写出该键
|
||
local m21b = output_clash_meta.generate({
|
||
{ proto = "vmess", name = "V", server = "1.1.1.1", port = 443, uuid = "u" },
|
||
})
|
||
check("M21 vmess has no flow", m21b:find("flow:", 1, true) == nil)
|
||
-- 无 flow 的 vless 也不写空键
|
||
local m21c = output_clash_meta.generate({
|
||
{ proto = "vless", name = "L2", server = "1.1.1.1", port = 443, uuid = "u" },
|
||
})
|
||
check("M21 vless without flow omits key", m21c:find("flow:", 1, true) == nil)
|
||
|
||
-- ---------- M22:clashmeta 的 grpc / h2 传输参数 ----------
|
||
-- 只写 network: grpc 时客户端用默认服务名去连,握手失败——与 ws 丢 path 同类。
|
||
-- 键名对照上游 transport 文档:grpc-service-name(带 grpc- 前缀);
|
||
-- 服务名存在 node.path(与 output_v2ray 的 serviceName、sing-box 的 service_name 同源)。
|
||
local m22g = output_clash_meta.generate({
|
||
{ proto = "vless", name = "G", server = "1.1.1.1", port = 443,
|
||
uuid = "u", net = "grpc", path = "my-grpc-svc", security = "tls" },
|
||
})
|
||
check("M22 grpc network written", m22g:find("network: grpc") ~= nil)
|
||
check("M22 grpc-opts block", m22g:find("grpc%-opts:") ~= nil)
|
||
check("M22 grpc-service-name", m22g:find("grpc%-service%-name: my%-grpc%-svc") ~= nil)
|
||
|
||
-- h2-opts.host 是**列表**,h2-opts.path 是标量(上游 transport 文档)
|
||
local m22h = output_clash_meta.generate({
|
||
{ proto = "trojan", name = "H", server = "2.2.2.2", port = 443, password = "p",
|
||
net = "h2", path = "/h2path", host = "h2.example.com" },
|
||
})
|
||
check("M22 h2 network written", m22h:find("network: h2") ~= nil)
|
||
check("M22 h2-opts block", m22h:find("h2%-opts:") ~= nil)
|
||
check("M22 h2 host is a list", m22h:find("host:\n %- h2%.example%.com") ~= nil)
|
||
check("M22 h2 path scalar", m22h:find("path: /h2path") ~= nil)
|
||
|
||
-- 无 path 的 grpc 节点不写出空 grpc-opts 块
|
||
local m22e = output_clash_meta.generate({
|
||
{ proto = "vless", name = "E", server = "4.4.4.4", port = 443, uuid = "u", net = "grpc" },
|
||
})
|
||
check("M22 grpc without path omits opts", m22e:find("grpc%-opts:", 1, true) == nil)
|
||
|
||
-- grpc 节点不得写出 h2-opts,反之亦然
|
||
check("M22 grpc has no h2-opts", m22g:find("h2%-opts:", 1, true) == nil)
|
||
check("M22 h2 has no grpc-opts", m22h:find("grpc%-opts:", 1, true) == nil)
|
||
|
||
-- ws 行为不变(回归保护)
|
||
local m22w = output_clash_meta.generate({
|
||
{ proto = "vmess", name = "W", server = "3.3.3.3", port = 443, uuid = "u",
|
||
net = "ws", path = "/ws", host = "ws.example.com" },
|
||
})
|
||
check("M22 ws-opts unchanged", m22w:find("ws%-opts:") ~= nil
|
||
and m22w:find("path: /ws") ~= nil
|
||
and m22w:find("Host: ws%.example%.com") ~= nil)
|
||
|
||
-- ---------- 结果 ----------
|
||
print(string.format("\n%d passed, %d failed", passed, failed))
|
||
os.exit(failed == 0 and 0 or 1)
|