#!/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