op-packages/luci-app-openclash/root/usr/share/openclash/openclash_sub_parser.lua
github-actions[bot] 32600acc60 🌴 Sync 2026-03-05 23:51:42
2026-03-05 23:51:42 +08:00

241 lines
6.6 KiB
Lua

#!/usr/bin/lua
-- Parse subscription files (plain or base64) and extract server addresses.
local nixio = require "nixio"
local jsonc = require "luci.jsonc"
local util = require "luci.util"
if not nixio.fs.access("/usr/bin/base64") and not nixio.fs.access("/bin/base64") then
os.exit(1)
end
local file_path = arg[1]
if not file_path or not nixio.fs.access(file_path) then
os.exit(1)
end
local function raw_base64_decode(str)
if not str or str == "" then return nil end
str = str:gsub("-", "+"):gsub("_", "/")
local padding = #str % 4
if padding > 0 then
str = str .. string.rep("=", 4 - padding)
end
local cmd = "printf '%s' '" .. str .. "' | base64 -d 2>/dev/null"
local f = io.popen(cmd, "r")
if not f then return nil end
local result = f:read("*a")
f:close()
if result and result ~= "" then
return result
end
return nil
end
local function base64_decode_subscription(str)
local result = raw_base64_decode(str)
if result and result:find("://") then
return result
end
return nil
end
local function url_decode(str)
return str:gsub("%%(%x%x)", function(h) return string.char(tonumber(h, 16)) end)
end
local function parse_query_params(query_string)
local params = {}
if not query_string then return params end
for param in query_string:gmatch("([^&]+)") do
local key, value = param:match("([^=]*)=?(.*)")
if key then
params[key] = url_decode(value or "")
end
end
return params
end
local function parse_url(url_str)
local scheme, rest = url_str:match("^([%w%-]+)://(.+)")
if not scheme then return nil end
local userinfo, host_part = rest:match("^([^@]+)@(.+)")
if not userinfo then
userinfo = ""
host_part = rest
end
local path_query_fragment = host_part:match("^[^/]*(.*)") or ""
local host_port = host_part:match("^([^/]*)")
local host, port = host_port:match("^%[([^%]]+)%]:(%d+)") -- IPv6
if not host then
host, port = host_port:match("^([^:]+):(%d+)")
end
if not host then
host = host_port
port = nil
end
local path, query_fragment = path_query_fragment:match("^([^?]*)(.*)")
path = path or ""
local query, fragment
if query_fragment then
if query_fragment:sub(1,1) == "?" then
query, fragment = query_fragment:sub(2):match("^([^#]*)(.*)")
if fragment and fragment:sub(1,1) == "#" then
fragment = fragment:sub(2)
end
elseif query_fragment:sub(1,1) == "#" then
fragment = query_fragment:sub(2)
end
end
local username, password = userinfo:match("^([^:]*):?(.*)")
return {
scheme = scheme:lower(),
username = username or "",
password = password or "",
host = host,
port = port,
path = path,
query = query,
fragment = fragment
}
end
local function get_server_from_url(line)
line = util.trim(line)
local scheme, original_body = line:match("^([%w%-]+)://(.+)")
if not scheme or not original_body then return nil end
local server = nil
scheme = scheme:lower()
if scheme == "vmess" then
-- fragment
original_body = original_body:match("([^#]+)") or original_body
-- base64
local decoded = raw_base64_decode(original_body)
if decoded then
-- V2RayN JSON
local ok, data = pcall(jsonc.parse, decoded)
if ok and type(data) == "table" and data.add and data.add ~= "" then
server = data.add
end
else
-- Xray VMessAEAD
local url_parts = parse_url(line)
if url_parts and url_parts.host then
server = url_parts.host
end
end
elseif scheme == "vless" then
local url_parts = parse_url(line)
if url_parts and url_parts.host then
server = url_parts.host
end
elseif scheme == "trojan" then
local url_parts = parse_url(line)
if url_parts and url_parts.host then
server = url_parts.host
end
elseif scheme == "ss" then
local url_parts = parse_url(line)
if url_parts and url_parts.host then
server = url_parts.host
else
-- ss://base64
original_body = original_body:match("([^#]+)") or original_body
local decoded = raw_base64_decode(original_body)
if decoded then
server = decoded:match("@([^:/]+)")
else
server = original_body:match("[^@]+@([^:/]+)")
end
end
elseif scheme == "ssr" then
original_body = original_body:match("([^#]+)") or original_body
local decoded = raw_base64_decode(original_body)
if decoded then
-- ssr://host:port:protocol:method:obfs:urlsafebase64pass/?params
local before_query = decoded:match("^([^/?]+)")
if before_query then
local parts = {}
for part in before_query:gmatch("([^:]+)") do
table.insert(parts, part)
end
-- host:port:protocol:method:obfs:password
if #parts == 6 then
server = parts[1]
end
end
end
elseif scheme == "hysteria" or scheme == "hysteria2" or scheme == "hy2" then
local url_parts = parse_url(line)
if url_parts and url_parts.host then
server = url_parts.host
end
elseif scheme == "tuic" then
local url_parts = parse_url(line)
if url_parts and url_parts.host then
server = url_parts.host
end
elseif scheme == "socks" or scheme == "socks5" or scheme == "socks5h" or
scheme == "http" or scheme == "https" then
local url_parts = parse_url(line)
if url_parts and url_parts.host then
server = url_parts.host
end
elseif scheme == "anytls" then
local url_parts = parse_url(line)
if url_parts and url_parts.host then
server = url_parts.host
end
end
-- fallback
if not server then
local body_to_parse = raw_base64_decode(original_body) or original_body
server = body_to_parse:match("@([^:/]+)") or -- user@host
body_to_parse:match("([^:/]+)") or -- host
body_to_parse:match("//([^:/]+)") -- //host
end
return server
end
local f = io.open(file_path, "r")
if not f then os.exit(1) end
local content = f:read("*a")
f:close()
local decoded_content = base64_decode_subscription(content:gsub("%s+", ""))
if decoded_content then
content = decoded_content
end
for line in content:gmatch("([^\r\n]+)") do
local server = get_server_from_url(line)
if server and util.trim(server) ~= "" then
print(util.trim(server))
end
end