Files

255 lines
11 KiB
Lua
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- output_uri.lua — 分享链接(URI)输出:Shadowrocket / V2Ray URI(纯 Lua)
-- luci-app-substore
-- 生成 vmess:// / vless:// / trojan:// / ss:// / hysteria2:// / tuic:// / ssr:// 分享链接
local util = require("substore.util")
local M = {}
-- URL 编码:保留字母数字 - . _ ~,其余转 %XX
local function url_encode(s)
s = tostring(s or "")
return (s:gsub("([^%w%-%.%_%~])", function(c)
return string.format("%%%02X", c:byte())
end))
end
-- alpn 归一成逗号分隔字符串。
-- alpn 可能是数组:Clash YAML 的 alpn 列表与 sing-box JSON 的 tls.alpn 导入后
-- 就是 table。直接交给 url_encode 会 tostring 成 "table: 0x...",链接里出现
-- `alpn=table%3A%200x...`(客户端解析失败)。
local function alpn_str(v)
if type(v) == "table" then return table.concat(v, ",") end
return v
end
-- 是否跳过证书校验。模型里的权威字段是 skip-cert-verify(见 parser.lua 中
-- hysteria2/hysteria URI 解析处的说明);insecure 只是 URI 参数名,且**只有**
-- URI 解析器会写它 —— Clash YAML / sing-box JSON / 表单导入的节点只有
-- skip-cert-verify。只读 insecure 会让这些节点在导出链接时丢掉该项,
-- 客户端按严格校验握手直接失败。
local function insecure_flag(n)
local v = n["skip-cert-verify"]
if v == nil then v = n.skip_cert_verify end
if v == nil then v = n.insecure end
if v == nil then return nil end
if v == false or v == "false" or v == 0 or v == "0" then return "0" end
return "1"
end
-- 生成单节点分享链接;无法生成时返回 nil
-- 分享链接里的 server 字面量。IPv6 必须加方括号:RFC 3986 的 authority 是
-- `host:port`,而裸 IPv6 本身就含冒号,`fd00::1:443` 无法判断哪一段是端口。
-- 本项目自己的解析器(util.split_hostport)就拆不开,重新导入得到 0 个节点;
-- 规范的客户端同样会拒绝或拆错。output_wireguard_conf 的 Endpoint 一直是这么做的。
local function host_literal(server)
server = tostring(server or "")
if server:find(":", 1, true) and server:sub(1, 1) ~= "[" then
return "[" .. server .. "]"
end
return server
end
function M.to_share_uri(n)
if type(n) ~= "table" or not n.server or not n.port then return nil end
local server = host_literal(n.server)
local port = tonumber(n.port) or 0
local name = url_encode(n.name or (server .. ":" .. tostring(port)))
local proto = (n.proto or ""):lower()
if proto == "ss" then proto = "shadowsocks" end
if proto == "shadowsocks" then
local method = n.method or n.cipher or "aes-256-gcm"
local password = n.password or ""
local userinfo = util.base64_encode(method .. ":" .. password)
-- SIP002:SS-URI = "ss://" userinfo "@" host ":" port [ "/" ] [ "?" plugin ] [ "#" tag ]
-- 插件参数整体做一次百分号编码(`;` 与 `=` 在 query 里必须转义)。
-- 不回写这一项时,带插件的节点导出后再导入就永久丢失插件配置。
local query = ""
if type(n.plugin) == "string" and n.plugin ~= "" then
query = "/?plugin=" .. url_encode(n.plugin)
end
return "ss://" .. userinfo .. "@" .. server .. ":" .. tostring(port)
.. query .. "#" .. name
end
if proto == "vmess" then
local json = {
v = "2",
ps = n.name or (server .. ":" .. tostring(port)),
add = server,
port = tostring(port),
id = n.uuid or "",
aid = tostring(n.alterId or n.aid or 0),
-- scy 是 vmess 加密方式,取 cipher 而非 TLS 层
scy = n.cipher or "auto",
net = n.net or n.network or "tcp",
type = n.type or n.headerType or "none",
host = n.host or "",
path = n.path or "",
-- 经典 vmess JSON 的 tls 字段是 TLS 层("tls" 或 ""),
-- security 经 node.normalize 归一后是唯一权威来源
tls = (n.security and n.security ~= "none") and "tls" or "",
}
return "vmess://" .. util.base64_encode(util.json_encode(json))
end
if proto == "vless" then
local q = {}
q[#q + 1] = "encryption=none"
q[#q + 1] = "type=" .. url_encode(n.net or n.network or "tcp")
q[#q + 1] = "security=" .. url_encode(n.security or "none")
if n.sni then q[#q + 1] = "sni=" .. url_encode(n.sni) end
if n.fp then q[#q + 1] = "fp=" .. url_encode(n.fp) end
if n.alpn then q[#q + 1] = "alpn=" .. url_encode(alpn_str(n.alpn)) end
if n.path then q[#q + 1] = "path=" .. url_encode(n.path) end
if n.host then q[#q + 1] = "host=" .. url_encode(n.host) end
if n.flow then q[#q + 1] = "flow=" .. url_encode(n.flow) end
-- Reality 参数:键名 pbk / sid / spx 来自 Xray 分享链接规范
-- (XTLS/Xray-core discussion #716),mihomo 与 v2rayN 都按它解析。
-- 不写出 pbk 的话,链接导入到任何客户端都得不到 Reality 配置,节点不可用。
if n["public-key"] then q[#q + 1] = "pbk=" .. url_encode(n["public-key"]) end
if n["short-id"] then q[#q + 1] = "sid=" .. url_encode(n["short-id"]) end
if n["spider-x"] then q[#q + 1] = "spx=" .. url_encode(n["spider-x"]) end
return "vless://" .. (n.uuid or "") .. "@" .. server .. ":" .. tostring(port)
.. "?" .. table.concat(q, "&") .. "#" .. name
end
if proto == "trojan" then
local q = {}
q[#q + 1] = "security=" .. url_encode(n.security or "tls")
if n.sni then q[#q + 1] = "sni=" .. url_encode(n.sni) end
if n.alpn then q[#q + 1] = "alpn=" .. url_encode(alpn_str(n.alpn)) end
if n.fp then q[#q + 1] = "fp=" .. url_encode(n.fp) end
return "trojan://" .. url_encode(n.password or "") .. "@" .. server .. ":" .. tostring(port)
.. "?" .. table.concat(q, "&") .. "#" .. name
end
if proto == "anytls" then
-- anytls:// 分享链接(由 anytls-go 定义):anytls://[auth@]host[:port]/?sni=..&insecure=..
-- 密码放在 userinfo(auth)位置,端口缺省 443。URI 规范只定义了 sni 与
-- insecure 两个查询参数,alpn 之类只存在于各家客户端的配置文件里 ——
-- 写进链接是非法参数(严格解析器会拒绝,宽松的会静默忽略)。
local q = {}
if n.sni then q[#q + 1] = "sni=" .. url_encode(n.sni) end
local ins = insecure_flag(n)
if ins ~= nil then q[#q + 1] = "insecure=" .. ins end
local suffix = #q > 0 and ("?" .. table.concat(q, "&")) or ""
return "anytls://" .. url_encode(n.password or "") .. "@" .. server .. ":" .. tostring(port)
.. suffix .. "#" .. name
end
if proto == "hysteria2" or proto == "hysteria" then
local q = {}
if n.sni then q[#q + 1] = "sni=" .. url_encode(n.sni) end
local ins = insecure_flag(n)
if ins ~= nil then q[#q + 1] = "insecure=" .. ins end
-- 混淆:hysteria2 是 salamander,URI 参数为 obfs / obfs-password;
-- hysteria(v1) 的 obfs 只是普通字符串,**没有** obfs-password
-- (写到 v1 链接上是非法参数,且会误导下游客户端)
if n.obfs and n.obfs ~= "" and n.obfs ~= "plain" then
q[#q + 1] = "obfs=" .. url_encode(n.obfs)
if proto == "hysteria2" then
local opw = n["obfs-password"] or n.obfs_password
if opw and opw ~= "" then q[#q + 1] = "obfs-password=" .. url_encode(opw) end
end
end
local suffix = #q > 0 and ("?" .. table.concat(q, "&")) or ""
return (proto == "hysteria2" and "hysteria2://" or "hysteria://")
.. url_encode(n.password or "") .. "@" .. server .. ":" .. tostring(port)
.. suffix .. "#" .. name
end
if proto == "tuic" then
local userinfo = (n.uuid or "") .. ":" .. url_encode(n.password or "")
local q = {}
if n.congestion_control then q[#q + 1] = "congestion_control=" .. url_encode(n.congestion_control) end
if n.alpn then q[#q + 1] = "alpn=" .. url_encode(alpn_str(n.alpn)) end
if n.sni then q[#q + 1] = "sni=" .. url_encode(n.sni) end
local suffix = #q > 0 and ("?" .. table.concat(q, "&")) or ""
return "tuic://" .. userinfo .. "@" .. server .. ":" .. tostring(port)
.. suffix .. "#" .. name
end
if proto == "socks5" or proto == "socks" then
local userinfo = ""
if n.username then userinfo = url_encode(n.username)
if n.password then userinfo = userinfo .. ":" .. url_encode(n.password) end
userinfo = userinfo .. "@"
end
return "socks5://" .. userinfo .. server .. ":" .. tostring(port) .. "#" .. name
end
if proto == "ssr" then
return M.to_ssr_uri(n)
end
if proto == "wireguard" then
-- wireguard://base64(json)#name —— 本项目自定义 scheme(wireguard 无统一 URI 标准)
local json = {
server = server,
port = tostring(port),
["private-key"] = n["private-key"] or n.private_key,
["public-key"] = n["public-key"] or n.public_key or n["peer-public-key"] or n.peer_public_key,
["peer-public-key"] = n["peer-public-key"] or n.peer_public_key,
["pre-shared-key"] = n["pre-shared-key"] or n.pre_shared_key or n["preshared-key"] or n.preshared_key,
["preshared-key"] = n["preshared-key"] or n.preshared_key,
ip = n.ip,
ipv6 = n.ipv6,
["allowed-ips"] = n["allowed-ips"],
reserved = n.reserved,
["persistent-keepalive"] = n["persistent-keepalive"],
["listen-port"] = n["listen-port"],
dns = n.dns,
["amnezia-wg-option"] = n["amnezia-wg-option"],
}
if n.mtu then json.mtu = tostring(n.mtu) end
if n.name then json.name = n.name end
return "wireguard://" .. util.base64_encode(util.json_encode(json)) .. "#" .. name
end
return nil
end
-- 生成 ssr:// 分享链接(外层与密码为标准 base64,参数为 base64url)
function M.to_ssr_uri(n)
if type(n) ~= "table" or not n.server then return nil end
local server = n.server
local port = tostring(tonumber(n.port) or 0)
local protocol = n.protocol or "origin"
local method = n.method or n.cipher or "aes-128-cfb"
local obfs = n.obfs or "plain"
local password = n.password or ""
local main = table.concat({ server, port, protocol, method, obfs, util.base64_encode(password) }, ":")
local q = {}
local op = n.obfs_param or n["obfs-param"]
local pp = n.protocol_param or n["protocol-param"]
if op and op ~= "" then q[#q + 1] = "obfsparam=" .. util.base64_url_encode(op) end
if pp and pp ~= "" then q[#q + 1] = "protoparam=" .. util.base64_url_encode(pp) end
q[#q + 1] = "remarks=" .. util.base64_url_encode(n.name or server)
if n.group and n.group ~= "" then q[#q + 1] = "group=" .. util.base64_url_encode(n.group) end
return "ssr://" .. util.base64_encode(main .. "/?" .. table.concat(q, "&"))
end
-- 生成 URI 列表(每行一条,丢弃无法生成 URI 的节点)
function M.to_uri_list(nodes)
local out = {}
for _, n in ipairs(nodes or {}) do
local u = M.to_share_uri(n)
if u then out[#out + 1] = u end
end
return table.concat(out, "\n")
end
-- Shadowrocket 订阅:base64 编码的 URI 列表
function M.to_shadowrocket(nodes)
return util.base64_encode(M.to_uri_list(nodes))
end
-- V2Ray URI 订阅:明文 URI 列表
function M.to_v2ray_uri(nodes)
return M.to_uri_list(nodes)
end
return M