mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-09 21:47:59 +08:00
🌴 Sync 2026-10-06 02:20:56
This commit is contained in:
1 parent
5c201dd3af
commit
487c4fad50
111 files changed
+4641
-746
No files matched your search
@@ -20,12 +20,62 @@ local function map_proto(raw)
|
||||
return TYPE_MAP[raw:lower()]
|
||||
end
|
||||
|
||||
-- 剥掉 Loon 位置参数外面的双引号:`"password"` → `password`。
|
||||
-- Loon 的节点行一律把凭据写成带引号的位置参数,Surge 家族的具名参数则不带引号;
|
||||
-- 只剥一层(`""x""` → `"x"`),没有引号时原样返回。
|
||||
local function unquote(v)
|
||||
v = util.trim(v or "")
|
||||
local inner = v:match('^"(.*)"$')
|
||||
return inner or v
|
||||
end
|
||||
|
||||
-- 按逗号切分字段,但**引号内的逗号不作为分隔符**。
|
||||
--
|
||||
-- Loon 把凭据写成双引号包裹的位置参数(nsloon.app/docs/Node/):
|
||||
-- Trojan = Trojan,trojan.example.com,443,"password",transport=tcp,...
|
||||
-- 密码里含逗号时,`rest:gmatch("[^,]+")` 会把它切成两段 ——
|
||||
-- Trojan = Trojan,h,443,"pa,ss",sni=x
|
||||
-- 的密码会静默变成 `"pa`(连引号一起、少掉后半段),用户拿到的不是他填的凭据,
|
||||
-- 而且没有任何提示。生成端早就有「值里含逗号就整条丢弃」的防护(output_formats
|
||||
-- 的 surge_line),解析端此前没有对应处理。
|
||||
--
|
||||
-- 两条边界:
|
||||
-- * 引号个数为奇数时(订阅内容不可信,可能是落单的引号)**不做引号感知**,
|
||||
-- 退回按逗号切分。否则那个引号会一直「开着」,把后面的 sni= / over-tls=
|
||||
-- 全吞进同一个字段,比按逗号切更糟。
|
||||
-- * 只跟踪引号开合,不做转义处理 —— Surge / Loon / QX 的行语法里都没有转义
|
||||
-- 机制(Loon 文档只把引号列为位置参数的写法,未提及任何转义序列)。
|
||||
--
|
||||
-- 无引号的输入与旧的 `gmatch("[^,]+")` 逐字符等价:都按逗号切、都丢弃空字段、
|
||||
-- 都对每段做 trim(旧的 `[^,]+` 会保留纯空白段,trim 后成空串,这里同样保留)。
|
||||
local function split_fields(s)
|
||||
local nquote = 0
|
||||
for _ in s:gmatch('"') do nquote = nquote + 1 end
|
||||
if nquote % 2 == 1 then
|
||||
local out = {}
|
||||
for part in s:gmatch("[^,]+") do out[#out + 1] = util.trim(part) end
|
||||
return out
|
||||
end
|
||||
local out, buf, quoted = {}, {}, false
|
||||
for i = 1, #s do
|
||||
local c = s:sub(i, i)
|
||||
if c == '"' then
|
||||
quoted = not quoted
|
||||
buf[#buf + 1] = c
|
||||
elseif c == "," and not quoted then
|
||||
if #buf > 0 then out[#out + 1] = util.trim(table.concat(buf)) end
|
||||
buf = {}
|
||||
else
|
||||
buf[#buf + 1] = c
|
||||
end
|
||||
end
|
||||
if #buf > 0 then out[#out + 1] = util.trim(table.concat(buf)) end
|
||||
return out
|
||||
end
|
||||
|
||||
-- 解析 Surge 风格行:Name = proto, server, port, k=v, ...
|
||||
local function parse_surge_line(name, rest)
|
||||
local parts = {}
|
||||
for part in rest:gmatch("[^,]+") do
|
||||
parts[#parts + 1] = util.trim(part)
|
||||
end
|
||||
local parts = split_fields(rest)
|
||||
if #parts < 3 then return nil end
|
||||
|
||||
local proto = map_proto(parts[1])
|
||||
@@ -33,10 +83,24 @@ local function parse_surge_line(name, rest)
|
||||
local server = parts[2]
|
||||
local port = tonumber(parts[3])
|
||||
|
||||
local kv = {}
|
||||
-- 具名参数(Surge 家族:`password=pwd`)与位置参数(Loon:`,443,"pwd",`)并存。
|
||||
-- Loon 的凭据一律写成端口之后的位置参数并用双引号包起来:
|
||||
-- Trojan = Trojan,trojan.example.com,443,"password",transport=tcp,...
|
||||
-- VLESS-Reality= VLESS,vless.example.com,443,"ae521383-...",transport=tcp,...
|
||||
-- AnyTLS = AnyTLS,anytls.example.com,443,"password",sni=...
|
||||
-- VMess = VMess,vmess.example.com,443,aes-128-gcm,"52396e06-...",transport=...
|
||||
-- 此前只认具名参数,Loon 配置导入后 uuid / password 全丢,节点因缺凭据被
|
||||
-- 下游静默丢弃。位置参数只收集「不含 = 的字段」,具名行完全不受影响。
|
||||
-- 切分走 split_fields:引号内的逗号不再被当作分隔符,`"pa,ss"` 这种位置
|
||||
-- 参数能原样保留(见该函数的说明)。
|
||||
local kv, pos = {}, {}
|
||||
for i = 4, #parts do
|
||||
local k, v = parts[i]:match("^([%w%-]+)%s*=%s*(.*)$")
|
||||
if k then kv[k] = util.trim(v) end
|
||||
if k then
|
||||
kv[k] = util.trim(v)
|
||||
else
|
||||
pos[#pos + 1] = unquote(parts[i])
|
||||
end
|
||||
end
|
||||
|
||||
local data = {
|
||||
@@ -46,14 +110,29 @@ local function parse_surge_line(name, rest)
|
||||
data.method = kv["encrypt-method"] or kv.cipher or kv.method
|
||||
data.password = kv.password
|
||||
elseif proto == "vmess" or proto == "vless" then
|
||||
data.uuid = kv.username or kv.uuid
|
||||
-- Loon 的 VMess 位置参数是「加密方式, UUID」两个字段;VLESS 只有一个 UUID
|
||||
if proto == "vmess" and pos[2] then
|
||||
data.cipher = pos[1]
|
||||
data.uuid = kv.username or kv.uuid or pos[2]
|
||||
else
|
||||
data.uuid = kv.username or kv.uuid or pos[1]
|
||||
end
|
||||
if kv.flow then data.flow = kv.flow end
|
||||
elseif proto == "trojan" then
|
||||
data.password = kv.password
|
||||
data.password = kv.password or pos[1]
|
||||
elseif proto == "anytls" then
|
||||
data.password = kv.password or pos[1]
|
||||
elseif proto == "hysteria2" or proto == "hysteria" then
|
||||
data.password = kv.password
|
||||
end
|
||||
|
||||
-- Reality 参数。Loon 的 VLESS / VMess / Trojan / AnyTLS Reality 行共用
|
||||
-- public-key(Base64 公钥)与 short-id(十六进制)两个参数名,公钥取值带引号:
|
||||
-- public-key="LgJ9bNTyUqBLFkDA12-QgEL7c1yQ1ztk-V1Q-3OLXSk",short-id=164168844958a16d
|
||||
-- 原样带过去即可 —— node.normalize 会据 public-key 把 security 定为 reality。
|
||||
if kv["public-key"] then data["public-key"] = unquote(kv["public-key"]) end
|
||||
if kv["short-id"] then data["short-id"] = unquote(kv["short-id"]) end
|
||||
|
||||
-- 传输/TLS 通用字段
|
||||
if kv.net or kv.network then data.net = kv.net or kv.network end
|
||||
if kv["ws"] == "true" then data.net = "ws" end
|
||||
@@ -61,8 +140,27 @@ local function parse_surge_line(name, rest)
|
||||
if kv["ws-headers"] and kv["ws-headers"]:match("^Host:") then
|
||||
data.host = kv["ws-headers"]:match("^Host:%s*(.*)$")
|
||||
end
|
||||
-- Loon 的传输写法是 transport=<tcp|ws|http> + path= + host=
|
||||
-- (nsloon.app/docs/Node/ 的示例行),与 Surge 家族的 net= / ws=true /
|
||||
-- ws-path / ws-headers=Host: 是同一组语义的两代写法 —— Loon 文档明说
|
||||
-- 「旧参数 ws=true、ws-path、ws-headers=Host:域名 分别对应 transport=ws、
|
||||
-- path、host」,即旧写法只是兼容别名。只认旧写法时,别人给的 Loon 配置
|
||||
-- 导入后 net 保持默认 tcp、path / host 全丢,导出到任何格式都按 tcp 去连一个
|
||||
-- 只开了 ws 的端口 —— 握手失败且不报错。
|
||||
-- transport=http 按同一段文档「兼容配置中的 transport=http 会按 WebSocket
|
||||
-- 处理」同样落成 ws。
|
||||
-- 放在 ws=true / ws-path / ws-headers 之后:同一行两种写法都出现时以新写法为准。
|
||||
if kv.transport then
|
||||
data.net = (kv.transport == "http") and "ws" or kv.transport
|
||||
end
|
||||
if kv.path then data.path = kv.path end
|
||||
if kv.host then data.host = kv.host end
|
||||
if kv.sni then data.sni = kv.sni end
|
||||
-- Surge 家族写 tls=true,Loon 写 over-tls=true,两种都是「启用 TLS」
|
||||
if kv.tls and kv.tls ~= "false" and kv.tls ~= "none" then data.security = "tls" end
|
||||
if kv["over-tls"] and kv["over-tls"] ~= "false" and kv["over-tls"] ~= "none" then
|
||||
data.security = "tls"
|
||||
end
|
||||
if kv["skip-cert-verify"] and kv["skip-cert-verify"] ~= "0" and kv["skip-cert-verify"] ~= "false" then
|
||||
data["skip-cert-verify"] = true
|
||||
end
|
||||
@@ -73,10 +171,7 @@ end
|
||||
local function parse_qx_line(content)
|
||||
local proto, rest = content:match("^([%w_]+)%s*=%s*(.+)$")
|
||||
if not proto then return nil end
|
||||
local parts = {}
|
||||
for part in rest:gmatch("[^,]+") do
|
||||
parts[#parts + 1] = util.trim(part)
|
||||
end
|
||||
local parts = split_fields(rest)
|
||||
if #parts < 1 then return nil end
|
||||
|
||||
local hostport = parts[1]
|
||||
@@ -99,12 +194,31 @@ local function parse_qx_line(content)
|
||||
data.uuid = kv.password or kv.uuid
|
||||
elseif proto == "trojan" then
|
||||
data.password = kv.password
|
||||
elseif proto == "anytls" then
|
||||
data.password = kv.password
|
||||
end
|
||||
-- QX 用 obfs= 表示传输 / TLS 模式(官方 sample.conf):over-tls 是「纯 TLS」、
|
||||
-- wss 是「ws + TLS」、ws 是明文 ws。obfs-host 在 ws 下是 Host 头、在 over-tls
|
||||
-- 下是 SNI —— 语义不同,必须分别落到 host / sni,否则 vless + over-tls 的节点
|
||||
-- 导入后 SNI 被写进 host,Reality 那条更是连不上。
|
||||
if kv["obfs"] == "over-tls" then
|
||||
data.security = "tls"
|
||||
if kv["obfs-host"] then data.sni = kv["obfs-host"] end
|
||||
else
|
||||
if kv["obfs"] == "ws" or kv["obfs"] == "wss" then data.net = "ws" end
|
||||
if kv["obfs"] == "wss" then data.security = "tls" end
|
||||
if kv["obfs-uri"] then data.path = kv["obfs-uri"] end
|
||||
if kv["obfs-host"] then data.host = kv["obfs-host"] end
|
||||
end
|
||||
if kv["obfs"] == "ws" then data.net = "ws" end
|
||||
if kv["obfs-uri"] then data.path = kv["obfs-uri"] end
|
||||
if kv["obfs-host"] then data.host = kv["obfs-host"] end
|
||||
if kv["tls-host"] then data.sni = kv["tls-host"] end
|
||||
if kv["tls-verification"] == "true" or kv["over-tls"] == "true" then data.security = "tls" end
|
||||
-- flow:QX 的 vless 写 vless-flow=xtls-rprx-vision(sample.conf 的 Reality 条目)
|
||||
if kv["vless-flow"] then data.flow = kv["vless-flow"] end
|
||||
-- Reality:QX 的键名与分享链接 / Clash 都不同 —— Base64 公钥是
|
||||
-- reality-base64-pubkey,十六进制 short id 是 reality-hex-shortid。
|
||||
-- node.normalize 会据 public-key 把 security 定为 reality。
|
||||
if kv["reality-base64-pubkey"] then data["public-key"] = kv["reality-base64-pubkey"] end
|
||||
if kv["reality-hex-shortid"] then data["short-id"] = kv["reality-hex-shortid"] end
|
||||
return node.normalize(data)
|
||||
end
|
||||
|
||||
|
||||
Reference in new issue
Block a user