mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-07 20:54:14 +08:00
199 lines
6.2 KiB
Lua
199 lines
6.2 KiB
Lua
#!/usr/bin/lua
|
|
-- Prints one server address per node of a subscription file, the fallback of the watchdog
|
|
-- for provider files that are not YAML.
|
|
--
|
|
-- The share links are the ones mihomo accepts (common/convert/converter.go), they follow
|
|
-- the schemes of the clients themselves:
|
|
-- ss:// SIP002, with the plugin query, or the legacy fully encoded form
|
|
-- ssr:// base64 of host:port:protocol:method:obfs:password/?params
|
|
-- vmess:// V2RayN base64 json, or the Xray VMessAEAD uri (XTLS/Xray-core#716)
|
|
-- vless:// Xray uri, the host of the uri may be base64 encoded
|
|
-- trojan:// password as userinfo
|
|
-- hysteria:// github.com/apernet/hysteria (docs/developers/URI-Scheme)
|
|
-- hysteria2:// and hy2://, also with the +realm suffix and with port hopping
|
|
-- tuic:// temporary unofficial standard, uuid:password or the v5 token as userinfo
|
|
-- anytls:// github.com/anytls/anytls-go (docs/uri_scheme.md)
|
|
-- mierus:// one uri per profile, the port and protocol query keys are repeated
|
|
-- socks5://, socks5h://, http://, https://
|
|
--
|
|
-- Only the server address is needed, every other part of the uri is ignored.
|
|
|
|
local util = require "luci.util"
|
|
local jsonc = require "luci.jsonc"
|
|
|
|
local BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
local BASE64_PAD = 61
|
|
|
|
-- Decodes base64 in every form a subscription may use: standard or url safe, padded or not.
|
|
-- A string that is not base64 at all is not a link payload, the caller gets nil.
|
|
local function decode64(text)
|
|
if not text then
|
|
return nil
|
|
end
|
|
text = text:gsub("[%s]", ""):gsub("%-", "+"):gsub("_", "/"):gsub("=+$", "")
|
|
if text == "" or #text % 4 == 1 or not text:match("^[A-Za-z0-9+/]+$") then
|
|
return nil
|
|
end
|
|
text = text .. ("===="):sub(1, (4 - #text % 4) % 4)
|
|
local result = {}
|
|
for group in text:gmatch("....") do
|
|
local c3, c4 = group:byte(3, 4)
|
|
local i1 = BASE64:find(group:sub(1, 1), 1, true)
|
|
local i2 = BASE64:find(group:sub(2, 2), 1, true)
|
|
local i3 = c3 == BASE64_PAD and 0 or BASE64:find(group:sub(3, 3), 1, true)
|
|
local i4 = c4 == BASE64_PAD and 0 or BASE64:find(group:sub(4, 4), 1, true)
|
|
if not (i1 and i2 and i3 and i4) then
|
|
return nil
|
|
end
|
|
i1, i2, i3, i4 = i1 - 1, i2 - 1, i3 - 1, i4 - 1
|
|
result[#result + 1] = string.char(i1 * 4 + math.floor(i2 / 16))
|
|
if c3 ~= BASE64_PAD then
|
|
result[#result + 1] = string.char((i2 % 16) * 16 + math.floor(i3 / 4))
|
|
end
|
|
if c4 ~= BASE64_PAD then
|
|
result[#result + 1] = string.char((i3 % 4) * 64 + i4)
|
|
end
|
|
end
|
|
return table.concat(result)
|
|
end
|
|
|
|
-- scheme, userinfo, host, port, query and fragment of a uri. A host without a port and an
|
|
-- IPv6 host in brackets are accepted, the port of a hop link is a list of ports.
|
|
local function parse_uri(uri)
|
|
local scheme, body = uri:match("^([%w%+%-%.]+)://(.*)$")
|
|
if not scheme then
|
|
return nil
|
|
end
|
|
local authority = body:match("^([^/%?#]*)") or ""
|
|
local remainder = body:sub(#authority + 1)
|
|
local userinfo, hostport = authority:match("^(.*)@([^@]+)$")
|
|
if not hostport then
|
|
userinfo, hostport = nil, authority
|
|
end
|
|
local host, port
|
|
if hostport:match("^%[") then
|
|
host, port = hostport:match("^%[([^%]]+)%]:?([%d,%-]*)$")
|
|
else
|
|
host, port = hostport:match("^([^:]*):([%d,%-]*)$")
|
|
end
|
|
return {
|
|
scheme = scheme:lower(),
|
|
userinfo = userinfo,
|
|
host = host or hostport,
|
|
port = port,
|
|
path = remainder:match("^[^%?#]*"),
|
|
query = remainder:match("%?([^#]*)"),
|
|
fragment = remainder:match("#(.*)$")
|
|
}
|
|
end
|
|
|
|
-- mihomo decodes the host of a vless uri when it is base64 encoded, the watchdog can only
|
|
-- use it when the result looks like an address
|
|
local function decoded_host(host)
|
|
local decoded = decode64(host)
|
|
if decoded and decoded:match("^[%w%.%-%_]+$") and decoded:find("%.") then
|
|
return decoded
|
|
end
|
|
return host
|
|
end
|
|
|
|
local function without_brackets(host)
|
|
if not host then
|
|
return nil
|
|
end
|
|
return host:match("^%[(.-)%]$") or host
|
|
end
|
|
|
|
-- The server address of one share link, nil when the line is not a link the plugin knows
|
|
local function server_of(uri)
|
|
local parsed = parse_uri(uri)
|
|
if not parsed then
|
|
return nil
|
|
end
|
|
local scheme = parsed.scheme
|
|
local host = parsed.host
|
|
|
|
if scheme == "vmess" then
|
|
-- V2RayN: the whole link body is the base64 of a json document
|
|
local decoded = decode64((uri:match("^%w+://([^#]+)") or ""))
|
|
if decoded and decoded:sub(1, 1) == "{" then
|
|
local ok, node = pcall(jsonc.parse, decoded)
|
|
if ok and type(node) == "table" and type(node.add) == "string" and node.add ~= "" then
|
|
return without_brackets(node.add)
|
|
end
|
|
return nil
|
|
end
|
|
-- Xray VMessAEAD: uuid@host:port
|
|
return host
|
|
end
|
|
|
|
if scheme == "ss" then
|
|
if parsed.port and parsed.port ~= "" then
|
|
return host
|
|
end
|
|
-- the legacy link is the base64 of method:password@host:port
|
|
local decoded = decode64(host)
|
|
if decoded then
|
|
return decoded:match("@%[([^%]]+)%]") or decoded:match("@([^:/]+)")
|
|
end
|
|
return nil
|
|
end
|
|
|
|
if scheme == "ssr" then
|
|
local decoded = decode64((uri:match("^%w+://([^#]+)") or ""))
|
|
if not decoded then
|
|
return nil
|
|
end
|
|
return decoded:match("^([^:]+):")
|
|
end
|
|
|
|
if scheme == "hysteria2" or scheme == "hy2" or scheme == "hysteria2+realm" or scheme == "hy2+realm" then
|
|
return without_brackets(host)
|
|
end
|
|
|
|
if scheme == "vless" then
|
|
return decoded_host(host)
|
|
end
|
|
|
|
if scheme == "hysteria" or scheme == "tuic" or scheme == "trojan" or scheme == "mierus"
|
|
or scheme == "anytls" or scheme == "socks" or scheme == "socks5" or scheme == "socks5h"
|
|
or scheme == "http" or scheme == "https" then
|
|
return without_brackets(host)
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
-- A provider file that is a broken yaml still names the address of its nodes
|
|
local function server_of_yaml(line)
|
|
local value = line:match("^%s*server%s*:%s*([^%s#%]]+)")
|
|
if not value then
|
|
return nil
|
|
end
|
|
return value:gsub("^[\"']", ""):gsub("[\"']$", "")
|
|
end
|
|
|
|
local path = arg[1]
|
|
local file = path and io.open(path, "r")
|
|
if not file then
|
|
os.exit(1)
|
|
end
|
|
local content = file:read("*a")
|
|
file:close()
|
|
|
|
local decoded = decode64(content:gsub("%s+", ""))
|
|
if decoded and decoded:find("://", 1, true) then
|
|
content = decoded
|
|
end
|
|
|
|
for line in content:gmatch("[^\r\n]+") do
|
|
local text = util.trim(line)
|
|
local ok, server = pcall(server_of, text)
|
|
if not ok or not server or server == "" then
|
|
server = server_of_yaml(text)
|
|
end
|
|
if server and server ~= "" then
|
|
print(server)
|
|
end
|
|
end
|