-- ssr_test.lua — SSR(ShadowsocksR)解析与输出单元测试 -- 用法:lua5.1 tests/ssr_test.lua package.path = "./root/usr/share/?.lua;" .. package.path local util = require("substore.util") local parser = require("substore.parser") local output = require("substore.output") local output_uri = require("substore.output_uri") 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 -- 独立构造的已知 SSR 分享链接(由 Python base64 生成,非本实现生成,避免循环验证): -- server=127.0.0.1 port=8388 protocol=auth_aes128_md5 method=aes-128-cfb obfs=http_simple -- password=test-password obfsparam=download.windowsupdate.com remarks=测试节点 group=测试组 local link = "ssr://MTI3LjAuMC4xOjgzODg6YXV0aF9hZXMxMjhfbWQ1OmFlcy0xMjgtY2ZiOmh0dHBfc2ltcGxlOmRHVnpkQzF3WVhOemQyOXlaQT09Lz9vYmZzcGFyYW09Wkc5M2JteHZZV1F1ZDJsdVpHOTNjM1Z3WkdGMFpTNWpiMjAmcmVtYXJrcz01cldMNkstVjZJcUM1NEs1Jmdyb3VwPTVyV0w2Sy1WNTd1RQ==" check("detect ssr = uri", parser.detect(link) == "uri") local n = parser.parse_uri(link) check("ssr parses", n ~= nil) check("ssr proto", n and n.proto == "ssr") check("ssr server", n and n.server == "127.0.0.1") check("ssr port", n and n.port == 8388) check("ssr protocol", n and n.protocol == "auth_aes128_md5") check("ssr method", n and n.method == "aes-128-cfb") check("ssr obfs", n and n.obfs == "http_simple") check("ssr password", n and n.password == "test-password") check("ssr obfs_param", n and n.obfs_param == "download.windowsupdate.com") check("ssr name", n and n.name == "测试节点") check("ssr group", n and n.group == "测试组") -- ---------- 订阅解析(URI 列表含 ssr) ---------- local res = parser.parse(link .. "\nvless://uuid1@1.1.1.1:443#V") check("parse ssr sub count", res and res.nodes and #res.nodes == 2) check("parse ssr sub proto", res and res.nodes[1] and res.nodes[1].proto == "ssr") -- ---------- ssr:// 分享链接回环 ---------- local re = output_uri.to_ssr_uri(n) check("to_ssr_uri returns link", re and re:sub(1, 6) == "ssr://") local n2 = parser.parse_uri(re) check("ssr roundtrip proto", n2 and n2.proto == "ssr") check("ssr roundtrip server", n2 and n2.server == "127.0.0.1") check("ssr roundtrip port", n2 and n2.port == 8388) check("ssr roundtrip method", n2 and n2.method == "aes-128-cfb") check("ssr roundtrip password", n2 and n2.password == "test-password") check("ssr roundtrip protocol", n2 and n2.protocol == "auth_aes128_md5") check("ssr roundtrip obfs", n2 and n2.obfs == "http_simple") check("ssr roundtrip obfs_param", n2 and n2.obfs_param == "download.windowsupdate.com") check("ssr roundtrip name", n2 and n2.name == "测试节点") check("ssr roundtrip group", n2 and n2.group == "测试组") -- ---------- Clash.Meta / Mihomo 输出 ---------- local cm = output.generate({ n }, "clashmeta") check("clashmeta type ssr", cm and cm:find("type: ssr") ~= nil) check("clashmeta cipher", cm and cm:find("cipher: aes%-128%-cfb") ~= nil) check("clashmeta password", cm and cm:find("password: test%-password") ~= nil) check("clashmeta protocol", cm and cm:find("protocol: auth_aes128_md5") ~= nil) check("clashmeta obfs", cm and cm:find("obfs: http_simple") ~= nil) check("clashmeta obfs-param", cm and cm:find("obfs%-param: download%.windowsupdate%.com") ~= nil) -- ---------- Shadowrocket(base64 URI)输出包含 ssr:// ---------- local shr = output.generate({ n }, "shadowrocket") check("shadowrocket has ssr", shr and util.base64_decode(shr):find("ssr://") ~= nil) -- ---------- Loon 保留 SSR,Surge 丢弃 ---------- local loon = output.generate({ n }, "loon") check("loon has ssr", loon and loon:find("ssr, 127%.0%.0%.1, 8388") ~= nil) -- Loon 把加密方式与密码写成端口之后的**位置参数**(密码带双引号),不是具名参数: -- `ShadowsocksR,服务器,端口,加密方式,"密码",protocol=…,obfs=…`(nsloon.app/docs/Node/) -- 具名的 encrypt-method= / password= 在 Loon 的语法里会各自占掉一个位置参数, -- 加密方式与密码都解析不出来。 check("loon cipher positional", loon and loon:find('ssr, 127%.0%.0%.1, 8388, aes%-128%-cfb, "test%-password"') ~= nil) check("loon no named encrypt-method", loon and loon:find("encrypt%-method=") == nil) check("loon protocol", loon and loon:find("protocol=auth_aes128_md5") ~= nil) -- ---------- Loon 输出 → 导入回环(位置参数写法) ---------- -- 生成端与解析端必须成对:只改生成端的话,导出的 Loon 配置再导入回来会丢掉 -- 加密方式与密码(parser_surge 此前对 ssr 根本没有取字段的分支)。 local loon_parsed = require("substore.parser_surge").parse(loon) check("loon roundtrip count", #loon_parsed == 1) local lr = loon_parsed[1] check("loon roundtrip proto", lr and lr.proto == "ssr") check("loon roundtrip method", lr and lr.method == "aes-128-cfb") check("loon roundtrip password", lr and lr.password == "test-password") check("loon roundtrip protocol", lr and lr.protocol == "auth_aes128_md5") check("loon roundtrip obfs", lr and lr.obfs == "http_simple") check("loon roundtrip obfs_param", lr and (lr.obfs_param == "download.windowsupdate.com" or lr["obfs-param"] == "download.windowsupdate.com")) check("loon roundtrip name", lr and lr.name == "测试节点") local surge = output.generate({ n }, "surge") check("surge drops ssr", surge and surge:find("127%.0%.0%.1") == nil) -- ---------- sing-box / V2Ray 丢弃 SSR ---------- -- 完整配置里 outbounds 恒含 direct / block,因此断言「不含 ssr 出站」 -- 而非「outbounds 为空」;无可用节点时也不生成分组 local sb = output.generate({ n }, "singbox") local sbd = util.json_decode(sb) local sb_types = {} for _, o in ipairs(sbd and sbd.outbounds or {}) do sb_types[o.type] = true end check("singbox drops ssr", sbd and not sb_types.ssr and not sb_types.shadowsocksr) check("singbox no group without nodes", sbd and not sb_types.selector and not sb_types.urltest) check("singbox keeps direct/block", sb_types.direct and sb_types.block) local v2 = output.generate({ n }, "v2ray") local v2d = util.json_decode(v2) local v2_protos = {} for _, o in ipairs(v2d and v2d.outbounds or {}) do v2_protos[o.protocol] = true end check("v2ray drops ssr", v2d and not v2_protos.ssr and not v2_protos.shadowsocksr) check("v2ray keeps freedom/blackhole", v2_protos.freedom and v2_protos.blackhole) -- ---------- 最小 ssr(无参数,默认 origin/plain) ---------- local minimal = parser.parse_uri("ssr://" .. util.base64_encode("1.2.3.4:80:origin:aes-256-cfb:plain:" .. util.base64_encode("p"))) check("ssr minimal", minimal and minimal.proto == "ssr" and minimal.server == "1.2.3.4" and minimal.password == "p") -- ---------- base64url 参数解码回退 ---------- local b64u = util.base64_url_encode("香港节点") check("base64url bare", b64u == "6aaZ5riv6IqC54K5") check("base64url roundtrip", util.base64_url_decode(b64u) == "香港节点") print(string.format("\n%d passed, %d failed", passed, failed)) os.exit(failed == 0 and 0 or 1)