op-packages/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua
2026-07-12 20:20:38 +08:00

2586 lines
87 KiB
Lua
Executable File

#!/usr/bin/lua
------------------------------------------------
-- @author William Chan <root@williamchan.me>
------------------------------------------------
require 'luci.util'
require 'luci.jsonc'
require 'luci.sys'
local appname = 'passwall2'
local api = require ("luci.passwall2.api")
local datatypes = require "luci.cbi.datatypes"
-- these global functions are accessed all the time by the event handler
-- so caching them is worth the effort
local tinsert = table.insert
local ssub, slen, schar, sbyte, sformat, sgsub = string.sub, string.len, string.char, string.byte, string.format, string.gsub
local split = api.split
local jsonParse, jsonStringify = luci.jsonc.parse, luci.jsonc.stringify
local base64Decode = api.base64Decode
local UrlEncode = api.UrlEncode
local UrlDecode = api.UrlDecode
local uci = api.uci
local fs = api.fs
local log = api.log
local i18n = api.i18n
local lyaml = require "lyaml"
local has_ss_rust = api.is_finded("sslocal")
local has_ssr = api.is_finded("ssr-local") and api.is_finded("ssr-redir")
local has_singbox = api.finded_com("sing-box")
local has_xray = api.finded_com("xray")
local has_hysteria2 = api.finded_com("hysteria")
local DEFAULT_ALLOWINSECURE = true
local DEFAULT_FILTER_KEYWORD_MODE = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0"
local DEFAULT_FILTER_KEYWORD_DISCARD_LIST = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {}
local DEFAULT_FILTER_KEYWORD_KEEP_LIST = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {}
-- Nodes should be retrieved using the core type (if not set on the node subscription page, the default type will be used automatically).
local DEFAULT_SS_TYPE = api.get_core("ss_type", {{has_ss_rust,"shadowsocks-rust"},{has_singbox,"sing-box"},{has_xray,"xray"}})
local DEFAULT_TROJAN_TYPE = api.get_core("trojan_type", {{has_singbox,"sing-box"},{has_xray,"xray"}})
local DEFAULT_VMESS_TYPE = api.get_core("vmess_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
local DEFAULT_VLESS_TYPE = api.get_core("vless_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
local DEFAULT_HYSTERIA2_TYPE = api.get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{has_singbox,"sing-box"},{has_xray,"xray"}})
local core_has = {
["xray"] = has_xray,
["sing-box"] = has_singbox,
["shadowsocks-rust"] = has_ss_rust,
["hysteria2"] = has_hysteria2
}
-- Determine whether to filter node keywords
local function is_filter_keyword(sub_cfg, value)
local mode = DEFAULT_FILTER_KEYWORD_MODE
local discard_list = DEFAULT_FILTER_KEYWORD_DISCARD_LIST
local keep_list = DEFAULT_FILTER_KEYWORD_KEEP_LIST
if sub_cfg then
local filter_keyword_mode = sub_cfg.filter_keyword_mode or "5" -- 5 is global
if filter_keyword_mode == "0" then
mode = "0"
elseif filter_keyword_mode == "1" then
mode = "1"
discard_list = sub_cfg.filter_discard_list or {}
elseif filter_keyword_mode == "2" then
mode = "2"
keep_list = sub_cfg.filter_keep_list or {}
elseif filter_keyword_mode == "3" then
mode = "3"
keep_list = sub_cfg.filter_keep_list or {}
discard_list = sub_cfg.filter_discard_list or {}
elseif filter_keyword_mode == "4" then
mode = "4"
keep_list = sub_cfg.filter_keep_list or {}
discard_list = sub_cfg.filter_discard_list or {}
end
end
if mode == "1" then
for k,v in ipairs(discard_list) do
if value:find(v, 1, true) then
return true
end
end
elseif mode == "2" then
local result = true
for k,v in ipairs(keep_list) do
if value:find(v, 1, true) then
result = false
end
end
return result
elseif mode == "3" then
local result = false
for k,v in ipairs(discard_list) do
if value:find(v, 1, true) then
result = true
end
end
for k,v in ipairs(keep_list) do
if value:find(v, 1, true) then
result = false
end
end
return result
elseif mode == "4" then
local result = true
for k,v in ipairs(keep_list) do
if value:find(v, 1, true) then
result = false
end
end
for k,v in ipairs(discard_list) do
if value:find(v, 1, true) then
result = true
end
end
return result
end
return false
end
local nodeResult = {} -- update result
local nodes_table = {}
for k, e in ipairs(api.get_valid_nodes()) do
if e.node_type == "normal" then
nodes_table[#nodes_table + 1] = e
end
end
-- To retrieve the current server's dynamic configurations, you can use `get` and `set`. `get` requires access to the node table.
local CONFIG = {}
do
if true then
local szType = "@global[0]"
local option = "node"
local node_id = uci:get(appname, szType, option)
CONFIG[#CONFIG + 1] = {
log = true,
remarks = i18n.translatef("Node"),
currentNode = node_id and uci:get_all(appname, node_id) or nil,
set = function(o, server)
uci:set(appname, szType, option, server)
o.newNodeId = server
end
}
end
if true then
local i = 0
local option = "node"
uci:foreach(appname, "socks", function(t)
i = i + 1
local id = t[".name"]
local node_id = t[option]
CONFIG[#CONFIG + 1] = {
log = true,
id = id,
remarks = i18n.translatef("Socks node list [%s]", i),
currentNode = node_id and uci:get_all(appname, node_id) or nil,
set = function(o, server)
if not server or server == "" then
if #nodes_table > 0 then
server = nodes_table[1][".name"]
end
end
uci:set(appname, t[".name"], option, server)
o.newNodeId = server
end
}
if t.autoswitch_backup_node and #t.autoswitch_backup_node > 0 then
local flag = i18n.translatef("Socks node list [%s]", i) .. " " .. i18n.translatef("Backup node list")
local currentNodes = {}
local newNodes = {}
for k, node_id in ipairs(t.autoswitch_backup_node) do
if node_id then
local currentNode = uci:get_all(appname, node_id) or nil
if currentNode then
currentNodes[#currentNodes + 1] = {
log = true,
remarks = flag .. "[" .. k .. "]",
currentNode = currentNode,
set = function(o, server)
if server and server ~= "nil" then
table.insert(o.newNodes, server)
end
end
}
end
end
end
CONFIG[#CONFIG + 1] = {
remarks = flag,
currentNodes = currentNodes,
newNodes = newNodes,
set = function(o, newNodes)
if o then
if not newNodes then newNodes = o.newNodes end
uci:set_list(appname, id, "autoswitch_backup_node", newNodes or {})
end
end
}
end
end)
end
if true then
local i = 0
local option = "lbss"
local function is_ip_port(str)
if type(str) ~= "string" then return false end
local ip, port = str:match("^([%d%.]+):(%d+)$")
return ip and datatypes.ipaddr(ip) and tonumber(port) and tonumber(port) <= 65535
end
uci:foreach(appname, "haproxy_config", function(t)
i = i + 1
local node_id = t[option]
CONFIG[#CONFIG + 1] = {
log = true,
id = t[".name"],
remarks = i18n.translatef("HAProxy node list [%s]", i),
currentNode = node_id and uci:get_all(appname, node_id) or nil,
set = function(o, server)
-- Modify the LBS value only if it is not in IP:Port format.
if not is_ip_port(t[option]) then
uci:set(appname, t[".name"], option, server)
o.newNodeId = server
end
end,
delete = function(o)
-- Deletion is only performed if the current LBS value is not in IP:port format.
if not is_ip_port(t[option]) then
uci:delete(appname, t[".name"])
end
end
}
end)
end
if true then
local i = 0
uci:foreach(appname, "acl_rule", function(t)
i = i + 1
local option = "node"
local node_id = t[option]
CONFIG[#CONFIG + 1] = {
log = true,
id = t[".name"],
remarks = i18n.translatef("ACL list [%s]", i),
currentNode = node_id and uci:get_all(appname, node_id) or nil,
set = function(o, server)
uci:set(appname, t[".name"], option, server)
o.newNodeId = server
end
}
end)
end
uci:foreach(appname, "nodes", function(node)
local node_id = node[".name"]
if node.protocol and node.protocol == '_shunt' then
local rules = {}
uci:foreach(appname, "shunt_rules", function(e)
if e[".name"] and e.remarks then
table.insert(rules, e)
table.insert(rules, {
[".name"] = e[".name"] .. "_proxy_tag",
remarks = e.remarks .. " " .. i18n.translate("Preproxy")
})
end
end)
table.insert(rules, {
[".name"] = "default_node",
remarks = i18n.translatef("Default")
})
table.insert(rules, {
[".name"] = "default_proxy_tag",
remarks = i18n.translatef("Default") .. " " .. i18n.translate("Preproxy")
})
for k, e in pairs(rules) do
local _node_id = node[e[".name"]] or nil
if _node_id and not _node_id:find("Socks_") then
CONFIG[#CONFIG + 1] = {
log = false,
currentNode = _node_id and uci:get_all(appname, _node_id) or nil,
remarks = i18n.translatef("Shunt [%s] node", e.remarks),
set = function(o, server)
if not server then server = "" end
uci:set(appname, node_id, e[".name"], server)
o.newNodeId = server
end
}
end
end
elseif node.protocol and node.protocol == '_balancing' then
local flag = i18n.translatef("Xray Load Balancing node [%s] list", node_id)
local currentNodes = {}
local newNodes = {}
if node.balancing_node then
for k, node in pairs(node.balancing_node) do
currentNodes[#currentNodes + 1] = {
log = true,
node = node,
currentNode = (function()
if node and node:find("Socks_") then
return { Socks = node }
end
return node and uci:get_all(appname, node) or nil
end)(),
remarks = node,
set = function(o, server)
if o and server and server ~= "nil" then
table.insert(o.newNodes, server)
end
end
}
end
end
CONFIG[#CONFIG + 1] = {
remarks = flag,
currentNodes = currentNodes,
newNodes = newNodes,
set = function(o, newNodes)
if o then
if not newNodes then newNodes = o.newNodes end
uci:set_list(appname, node_id, "balancing_node", newNodes or {})
end
end
}
-- Backup Node
local currentNode = uci:get_all(appname, node_id) or nil
if currentNode and currentNode.fallback_node and not currentNode.fallback_node:find("Socks_") then
CONFIG[#CONFIG + 1] = {
log = true,
id = node_id,
remarks = i18n.translatef("Xray Load Balancing node [%s] backup node", node_id),
currentNode = uci:get_all(appname, currentNode.fallback_node) or nil,
set = function(o, server)
uci:set(appname, node_id, "fallback_node", server)
o.newNodeId = server
end,
delete = function(o)
uci:delete(appname, node_id, "fallback_node")
end
}
end
elseif node.protocol and node.protocol == '_urltest' then
local flag = i18n.translatef("Sing-Box URLTest node [%s] list", node_id)
local currentNodes = {}
local newNodes = {}
if node.urltest_node then
for k, node in pairs(node.urltest_node) do
currentNodes[#currentNodes + 1] = {
log = true,
node = node,
currentNode = (function()
if node and node:find("Socks_") then
return { Socks = node }
end
return node and uci:get_all(appname, node) or nil
end)(),
remarks = node,
set = function(o, server)
if o and server and server ~= "nil" then
table.insert(o.newNodes, server)
end
end
}
end
end
CONFIG[#CONFIG + 1] = {
remarks = flag,
currentNodes = currentNodes,
newNodes = newNodes,
set = function(o, newNodes)
if o then
if not newNodes then newNodes = o.newNodes end
uci:set_list(appname, node_id, "urltest_node", newNodes or {})
end
end
}
else
-- Preproxy Node
local currentNode = uci:get_all(appname, node_id) or nil
if currentNode and currentNode.preproxy_node and not currentNode.preproxy_node:find("Socks_") then
CONFIG[#CONFIG + 1] = {
log = true,
id = node_id,
remarks = i18n.translatef("Node [%s] preproxy node", node_id),
currentNode = uci:get_all(appname, currentNode.preproxy_node) or nil,
set = function(o, server)
uci:set(appname, node_id, "preproxy_node", server)
o.newNodeId = server
end,
delete = function(o)
uci:delete(appname, node_id, "preproxy_node")
end
}
end
-- Landing node
local currentNode = uci:get_all(appname, node_id) or nil
if currentNode and currentNode.to_node and not currentNode.to_node:find("Socks_") then
CONFIG[#CONFIG + 1] = {
log = true,
id = node_id,
remarks = i18n.translatef("Node [%s] landing node", node_id),
currentNode = uci:get_all(appname, currentNode.to_node) or nil,
set = function(o, server)
uci:set(appname, node_id, "to_node", server)
o.newNodeId = server
end,
delete = function(o)
uci:delete(appname, node_id, "to_node")
end
}
end
end
end)
for k, v in pairs(CONFIG) do
if v.currentNodes and type(v.currentNodes) == "table" then
for kk, vv in pairs(v.currentNodes) do
if vv.currentNode == nil then
CONFIG[k].currentNodes[kk] = nil
end
end
else
if v.currentNode == nil then
if v.delete then
v.delete()
end
CONFIG[k] = nil
end
end
end
end
-- Retrieve subscribe information (remaining data allowance, expiration time).
local subscribe_info = {}
local function get_subscribe_info(cfgid, value)
if type(cfgid) ~= "string" or cfgid == "" or type(value) ~= "string" then
return
end
value = value:gsub("%s+", "")
local date_patterns = {"套餐到期:(.+)", "过期时间:(.+)", "有效期至:(.+)", "到期时间:(.+)", "截止日期:(.+)"}
local expired_date
for _, p in ipairs(date_patterns) do expired_date = value:match(p) or expired_date end
local rem_patterns = {"剩余流量:(.+)", "流量剩余:(.+)", "可用流量:(.+)", "套餐剩余:(.+)"}
local rem_traffic
for _, p in ipairs(rem_patterns) do rem_traffic = value:match(p) or rem_traffic end
subscribe_info[cfgid] = subscribe_info[cfgid] or {expired_date = "", rem_traffic = ""}
if expired_date then
subscribe_info[cfgid]["expired_date"] = expired_date
end
if rem_traffic then
subscribe_info[cfgid]["rem_traffic"] = rem_traffic
end
end
-- Configure the SS protocol implementation type
local function set_ss_implementation(ss_type, result)
if ss_type == "shadowsocks-rust" and has_ss_rust then
result.type = 'SS-Rust'
elseif ss_type == "xray" and has_xray then
result.type = 'Xray'
result.protocol = 'shadowsocks'
result.transport = 'raw'
elseif ss_type == "sing-box" and has_singbox then
result.type = 'sing-box'
result.protocol = 'shadowsocks'
else
log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", "SS", "SS"))
return nil
end
return result
end
local function parseClashNode(node, add_mode, group, sub_cfg)
local sub_allowinsecure = DEFAULT_ALLOWINSECURE
local sub_ss_type = DEFAULT_SS_TYPE
local sub_trojan_type = DEFAULT_TROJAN_TYPE
local sub_vmess_type = DEFAULT_VMESS_TYPE
local sub_vless_type = DEFAULT_VLESS_TYPE
local sub_hysteria2_type = DEFAULT_HYSTERIA2_TYPE
local sub_hy_up_mbps, sub_hy_down_mbps
if sub_cfg then
if sub_cfg.allowInsecure and sub_cfg.allowInsecure ~= "1" then
sub_allowinsecure = nil
end
local ss_type = sub_cfg.ss_type or "global"
if ss_type ~= "global" and core_has[ss_type] then
sub_ss_type = ss_type
end
local trojan_type = sub_cfg.trojan_type or "global"
if trojan_type ~= "global" and core_has[trojan_type] then
sub_trojan_type = trojan_type
end
local vmess_type = sub_cfg.vmess_type or "global"
if vmess_type ~= "global" and core_has[vmess_type] then
sub_vmess_type = vmess_type
end
local vless_type = sub_cfg.vless_type or "global"
if vless_type ~= "global" and core_has[vless_type] then
sub_vless_type = vless_type
end
local hysteria2_type = sub_cfg.hysteria2_type or "global"
if hysteria2_type ~= "global" and core_has[hysteria2_type] then
sub_hysteria2_type = hysteria2_type
end
sub_hy_up_mbps = sub_cfg.hysteria_up_mbps
sub_hy_down_mbps = sub_cfg.hysteria_down_mbps
end
local result = {
timeout = 60,
add_mode = add_mode, -- `0` for manual configuration, `1` for import, `2` for subscription
group = group
}
result.remarks = node.name
result.address = node.server
result.port = node.port
if node.type == 'ss' then
result = set_ss_implementation(sub_ss_type, result)
if not result then return nil end
result.method = node.cipher
result.password = node.password
if node.plugin == "obfs" then
result.plugin = "obfs-local"
elseif node.plugin == "v2ray-plugin" then
result.plugin = "v2ray-plugin"
end
if result.plugin then
result.plugin_enabled = "1"
if result.type == 'Xray' then
if result.plugin ~= "obfs-local" then
result.plugin_enabled = nil
result.error_msg = i18n.translatef("%s unsupport SS %s plugin.", "Xray", result.plugin)
end
elseif result.type == 'sing-box' then
if result.plugin ~= "obfs-local" and result.plugin ~= "v2ray-plugin" then
result.plugin_enabled = nil
result.error_msg = i18n.translatef("%s unsupport SS %s plugin.", "Sing-Box", result.plugin)
end
end
if node["plugin-opts"] and result.plugin_enabled == "1" then
if node.plugin == "obfs" then
local plugin_opts = ""
local opts_mode = node["plugin-opts"].mode
if opts_mode then
plugin_opts = plugin_opts .. "obfs=" .. opts_mode .. ";"
end
local opts_host = node["plugin-opts"].host
if opts_host then
plugin_opts = plugin_opts .. "obfs-host=" .. opts_host
end
result.plugin_opts = plugin_opts
elseif node.plugin == "v2ray-plugin" then
local plugin_opts = ""
local opts_mode = node["plugin-opts"].mode
local opts_tls = node["plugin-opts"].tls
if opts_tls then
plugin_opts = plugin_opts .. "tls;"
end
local opts_skip_cert_verify = node["plugin-opts"]["skip-cert-verify"]
local opts_host = node["plugin-opts"].host
if opts_host then
plugin_opts = plugin_opts .. "host=" .. opts_host .. ";"
end
local opts_path = node["plugin-opts"].path
local opts_mux = node["plugin-opts"].mux
if node["plugin-opts"].headers then
--todo
end
result.plugin_opts = plugin_opts
end
end
end
elseif node.type == 'ssr' then
if not has_ssr then
log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", "SSR", "shadowsocksr-libev"))
return nil
end
result.type = "SSR"
result.method = node.cipher
result.password = node.password
result.obfs = node.obfs
result.protocol = node.protocol
result.obfs_param = node["obfs-param"]
result.protocol_param = node["protocol-param"]
elseif node.type == 'vmess' then
if sub_vmess_type == "sing-box" and has_singbox then
result.type = 'sing-box'
elseif sub_vmess_type == "xray" and has_xray then
result.type = "Xray"
else
log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", "VMess", "VMess"))
return nil
end
result.protocol = 'vmess'
result.uuid = node.uuid
result.alter_id = node.alterId
result.security = node.cipher or "auto"
result.tcp_fast_open = node.tfo
result.tls = "0"
if node.tls then
result.tls = "1"
result.tls_serverName = node.servername or ""
local insecure = node["skip-cert-verify"]
result.tls_allowInsecure = insecure and "1" or "0"
if sub_allowinsecure then
result.tls_allowInsecure = "1"
end
end
result.transport = node.network and string.lower(node.network) or "tcp"
if result.type == "sing-box" and result.transport == "raw" then
result.transport = "tcp"
elseif result.type == "Xray" and result.transport == "tcp" then
result.transport = "raw"
end
if result.transport == 'ws' then
local ws_opts = node["ws-opts"]
if ws_opts then
if ws_opts.headers then
result.ws_host = ws_opts.headers.Host or ws_opts.headers.host
end
if ws_opts.path then
result.ws_path = ws_opts.path
if ws_opts["max-early-data"] then
if result.type == "sing-box" then
result.ws_enableEarlyData = "1"
result.ws_maxEarlyData = tonumber(ws_opts["max-early-data"])
result.ws_earlyDataHeaderName = "Sec-WebSocket-Protocol"
elseif result.type == "Xray" then
result.ws_path = result.ws_path .. "?ed=" .. ws_opts["max-early-data"]
end
end
end
end
elseif result.transport == 'h2' then
local h2_opts = node["h2-opts"]
if h2_opts then
if result.type == "sing-box" then
result.http_path = h2_opts.path
result.http_host = h2_opts.host
elseif result.type == "Xray" then
result.xhttp_mode = "stream-one"
result.xhttp_path = h2_opts.path
result.xhttp_host = h2_opts.host
end
end
elseif result.transport == 'grpc' then
local grpc_opts = node["grpc-opts"]
if grpc_opts then
result.grpc_serviceName = grpc_opts["grpc-service-name"]
end
end
elseif node.type == 'vless' then
if sub_vless_type == "sing-box" and has_singbox then
result.type = 'sing-box'
elseif sub_vless_type == "xray" and has_xray then
result.type = "Xray"
else
log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", "VLESS", "VLESS"))
return nil
end
result.protocol = "vless"
result.uuid = node.uuid
result.tcp_fast_open = node.tfo
result.encryption = node.cipher or "none"
result.flow = node.flow
result.tls = "0"
if node.tls then
result.tls = "1"
result.tls_serverName = node.servername or ""
local insecure = node["skip-cert-verify"]
result.tls_allowInsecure = insecure and "1" or "0"
if sub_allowinsecure then
result.tls_allowInsecure = "1"
end
end
if node.tls and node["reality-opts"] and node["reality-opts"]["public-key"] then
result.reality = "1"
result.reality_publicKey = (node["reality-opts"] and node["reality-opts"]["public-key"]) or nil
result.reality_shortId = (node["reality-opts"] and node["reality-opts"]["short-id"]) or nil
end
result.transport = node.network and string.lower(node.network) or "tcp"
if result.type == "sing-box" and result.transport == "raw" then
result.transport = "tcp"
elseif result.type == "Xray" and result.transport == "tcp" then
result.transport = "raw"
end
if result.transport == 'ws' then
local ws_opts = node["ws-opts"]
if ws_opts then
if ws_opts.headers then
result.ws_host = ws_opts.headers.Host or ws_opts.headers.host
end
if ws_opts.path then
result.ws_path = ws_opts.path
if ws_opts["max-early-data"] then
if result.type == "sing-box" then
result.ws_enableEarlyData = "1"
result.ws_maxEarlyData = tonumber(ws_opts["max-early-data"])
result.ws_earlyDataHeaderName = "Sec-WebSocket-Protocol"
elseif result.type == "Xray" then
result.ws_path = result.ws_path .. "?ed=" .. ws_opts["max-early-data"]
end
end
end
end
end
elseif node.type == 'trojan' then
if sub_trojan_type == "sing-box" and has_singbox then
result.type = 'sing-box'
result.protocol = 'trojan'
elseif sub_trojan_type == "xray" and has_xray then
result.type = 'Xray'
result.protocol = 'trojan'
else
log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", "Trojan", "Trojan"))
return nil
end
result.password = node.password
result.tls = '1'
result.tls_serverName = node.sni or ""
local insecure = node["skip-cert-verify"]
result.tls_allowInsecure = insecure and "1" or "0"
if sub_allowinsecure then
result.tls_allowInsecure = "1"
end
if node.alpn then
--todo
end
if node.network == "grpc" then
if node["grpc-opts"] then
result.grpc_serviceName = node["grpc-opts"]["grpc-service-name"]
end
elseif node.network == "ws" then
local ws_opts = node["ws-opts"]
if ws_opts then
result.ws_path = ws_opts.path
if ws_opts.headers then
result.ws_host = ws_opts.headers.Host or ws_opts.headers.host
end
end
end
elseif node.type == 'anytls' then
if has_singbox then
result.type = 'sing-box'
result.protocol = "anytls"
else
log(2, i18n.translatef("Skip the %s node because the %s core program is not installed.", "AnyTLS", "AnyTLS", "Sing-Box 1.12"))
return nil
end
result.password = node.password
result.tls = '1'
result.tls_serverName = node.sni or ""
local insecure = node["skip-cert-verify"]
result.tls_allowInsecure = insecure and "1" or "0"
if sub_allowinsecure then
result.tls_allowInsecure = "1"
end
if node["client-fingerprint"] then
result.utls = "1"
result.fingerprint = node["client-fingerprint"]
end
if node.tls and node["reality-opts"] and node["reality-opts"]["public-key"] then
result.reality = "1"
result.reality_publicKey = node["reality-opts"]["public-key"]
result.reality_shortId = node["reality-opts"]["short-id"]
end
end
if not result.remarks or result.remarks == "" then
if result.address and result.port then
result.remarks = result.address .. ':' .. result.port
else
result.remarks = "NULL"
end
end
return result
end
-- Processing Clash data
local function processClashData(content, add_mode, group, sub_cfg)
local results = {}
for i, node in ipairs(content.proxies or {}) do
local result = parseClashNode(node, add_mode, group, sub_cfg)
if result then
table.insert(results, result)
end
end
return results
end
-- Processing data
local function processData(szType, content, add_mode, group, sub_cfg)
--log(2, content, add_mode, group)
local sub_allowinsecure = DEFAULT_ALLOWINSECURE
local sub_ss_type = DEFAULT_SS_TYPE
local sub_trojan_type = DEFAULT_TROJAN_TYPE
local sub_vmess_type = DEFAULT_VMESS_TYPE
local sub_vless_type = DEFAULT_VLESS_TYPE
local sub_hysteria2_type = DEFAULT_HYSTERIA2_TYPE
local sub_hy_up_mbps, sub_hy_down_mbps
if sub_cfg then
if sub_cfg.allowInsecure and sub_cfg.allowInsecure ~= "1" then
sub_allowinsecure = nil
end
local ss_type = sub_cfg.ss_type or "global"
if ss_type ~= "global" and core_has[ss_type] then
sub_ss_type = ss_type
end
local trojan_type = sub_cfg.trojan_type or "global"
if trojan_type ~= "global" and core_has[trojan_type] then
sub_trojan_type = trojan_type
end
local vmess_type = sub_cfg.vmess_type or "global"
if vmess_type ~= "global" and core_has[vmess_type] then
sub_vmess_type = vmess_type
end
local vless_type = sub_cfg.vless_type or "global"
if vless_type ~= "global" and core_has[vless_type] then
sub_vless_type = vless_type
end
local hysteria2_type = sub_cfg.hysteria2_type or "global"
if hysteria2_type ~= "global" and core_has[hysteria2_type] then
sub_hysteria2_type = hysteria2_type
end
sub_hy_up_mbps = sub_cfg.hysteria_up_mbps
sub_hy_down_mbps = sub_cfg.hysteria_down_mbps
end
local result = {
timeout = 60,
add_mode = add_mode, -- `0` for manual configuration, `1` for import, `2` for subscription
group = group
}
--ssr://base64(host:port:protocol:method:obfs:base64pass/?obfsparam=base64param&protoparam=base64param&remarks=base64remarks&group=base64group&udpport=0&uot=0)
if szType == 'ssr' then
if not has_ssr then
log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", "SSR", "shadowsocksr-libev"))
return nil
end
result.type = "SSR"
local dat = split(content:gsub("/%?", "?"), "%?")
local hostInfo = split(dat[1], ':')
if dat[1]:match('%[(.*)%]') then
result.address = dat[1]:match('%[(.*)%]')
else
result.address = hostInfo[#hostInfo-5]
end
result.port = hostInfo[#hostInfo-4]
result.protocol = hostInfo[#hostInfo-3]
result.method = hostInfo[#hostInfo-2]
result.obfs = hostInfo[#hostInfo-1]
result.password = base64Decode(hostInfo[#hostInfo])
local params = {}
for _, v in pairs(split(dat[2], '&')) do
local s = v:find("=", 1, true)
if s and s > 1 then
params[v:sub(1, s - 1)] = v:sub(s + 1)
end
end
result.obfs_param = base64Decode(params.obfsparam)
result.protocol_param = base64Decode(params.protoparam)
-- local ssr_group = base64Decode(params.group)
-- if ssr_group then result.ssr_group = ssr_group end
result.remarks = base64Decode(params.remarks)
elseif szType == 'vmess' then
local info = jsonParse(content)
if sub_vmess_type == "sing-box" and has_singbox then
result.type = 'sing-box'
elseif sub_vmess_type == "xray" and has_xray then
result.type = "Xray"
else
log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", "VMess", "VMess"))
return nil
end
result.address = info.add
result.port = info.port
result.protocol = 'vmess'
result.alter_id = info.aid
result.uuid = info.id
result.remarks = info.ps
-- result.mux = 1
-- result.mux_concurrency = 8
info.path = (info.path and info.path ~= "") and UrlDecode(info.path) or nil
if not info.net then info.net = "tcp" end
info.net = string.lower(info.net)
if result.type == "sing-box" and info.net == "raw" then
info.net = "tcp"
elseif result.type == "Xray" and info.net == "tcp" then
info.net = "raw"
end
if info.net == 'h2' or info.net == 'http' then
info.net = "http"
result.transport = (result.type == "Xray") and "xhttp" or "http"
else
result.transport = info.net
end
if info.net == 'ws' then
result.ws_host = info.host
result.ws_path = info.path
if result.type == "sing-box" and info.path then
local ws_path_dat = split(info.path, "?")
local ws_path = ws_path_dat[1]
local ws_path_params = {}
for _, v in pairs(split(ws_path_dat[2], '&')) do
local t = split(v, '=')
ws_path_params[t[1]] = t[2]
end
if ws_path_params.ed and tonumber(ws_path_params.ed) then
result.ws_path = ws_path
result.ws_enableEarlyData = "1"
result.ws_maxEarlyData = tonumber(ws_path_params.ed)
result.ws_earlyDataHeaderName = "Sec-WebSocket-Protocol"
end
end
end
if info.net == "http" then
if result.type == "Xray" then
result.xhttp_mode = "stream-one"
result.xhttp_host = info.host
result.xhttp_path = info.path
else
result.http_host = (info.host and info.host ~= "") and { info.host } or nil
result.http_path = info.path
end
end
if info.net == 'raw' or info.net == 'tcp' then
if info.type and info.type ~= "http" then
info.type = "none"
end
result.tcp_guise = info.type
result.tcp_guise_http_host = (info.host and info.host ~= "") and { info.host } or nil
result.tcp_guise_http_path = (info.path and info.path ~= "") and { info.path } or nil
end
if info.net == 'kcp' or info.net == 'mkcp' then
info.net = "mkcp"
result.mkcp_guise = info.type
result.mkcp_seed = info.seed
end
if info.net == 'quic' then
result.quic_guise = info.type
result.quic_key = info.key
result.quic_security = info.securty
end
if info.net == 'grpc' then
result.grpc_serviceName = info.path
end
if info.net == 'xhttp' or info.net == 'splithttp' then
result.xhttp_host = info.host
result.xhttp_path = info.path
result.xhttp_mode = params.mode or "auto"
result.xhttp_extra = params.extra
local success, Data = pcall(jsonParse, params.extra)
if success and Data then
local address = (Data.extra and Data.extra.downloadSettings and Data.extra.downloadSettings.address)
or (Data.downloadSettings and Data.downloadSettings.address)
result.download_address = address and address ~= "" and address or nil
else
result.download_address = nil
end
end
if info.net == 'httpupgrade' then
result.httpupgrade_host = info.host
result.httpupgrade_path = info.path
end
result.security = info.security or info.scy or "auto"
if info.tls == "tls" or info.tls == "1" then
result.tls = "1"
result.alpn = info.alpn
if info.fp and info.fp ~= "" then
result.utls = "1"
result.fingerprint = info.fp
end
result.tls_serverName = (info.sni and info.sni ~= "") and info.sni or info.host
result.tls_pinSHA256 = info.pcs
result.tls_CertByName = info.vcn
local insecure = info.allowinsecure or info.allowInsecure or info.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (sub_allowinsecure and "1" or "0")
else
result.tls = "0"
end
if info.ech and info.ech ~= "" then
result.ech = "1"
result.ech_config = info.ech
end
result.tcp_fast_open = info.tfo
info.fm = (info.fm and info.fm ~= "") and UrlDecode(info.fm) or nil
result.use_finalmask = (info.fm and info.fm ~= "") and "1" or nil
result.finalmask = (info.fm and info.fm ~= "") and api.base64Encode(info.fm) or nil
if result.type == "sing-box" and (result.transport == "mkcp" or result.transport == "xhttp") then
log(2, i18n.translatef("Skip node: %s. Because Sing-Box does not support the %s protocol's %s transmission method, Xray needs to be used instead.", result.remarks, szType, result.transport))
return nil
end
elseif szType == "ss" then
result = set_ss_implementation(sub_ss_type, result)
if not result then return nil end
--SS-URI = "ss://" userinfo "@" hostname ":" port [ "/" ] [ "?" plugin ] [ "#" tag ]
--userinfo = websafe-base64-encode-utf8(method ":" password)
--ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1
--ss://cmM0LW1kNTpwYXNzd2Q@192.168.100.1:8888/?plugin=obfs-local%3Bobfs%3Dhttp#Example2
--ss://2022-blake3-aes-256-gcm:YctPZ6U7xPPcU%2Bgp3u%2B0tx%2FtRizJN9K8y%2BuKlW2qjlI%3D@192.168.100.1:8888#Example3
--ss://2022-blake3-aes-256-gcm:YctPZ6U7xPPcU%2Bgp3u%2B0tx%2FtRizJN9K8y%2BuKlW2qjlI%3D@192.168.100.1:8888/?plugin=v2ray-plugin%3Bserver#Example3
--ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTp0ZXN0@xxxxxx.com:443?type=ws&path=%2Ftestpath&host=xxxxxx.com&security=tls&fp=&alpn=h3%2Ch2%2Chttp%2F1.1&sni=xxxxxx.com#test-1%40ss
local idx_sp = content:find("#") or 0
local alias = ""
if idx_sp > 0 then
alias = content:sub(idx_sp + 1, -1)
end
result.remarks = UrlDecode(alias)
local info = content:sub(1, idx_sp - 1):gsub("/%?", "?")
local params = {}
if info:find("%?") then
local find_index = info:find("%?")
local query = split(info, "%?")
for _, v in pairs(split(query[2], '&')) do
local s = v:find("=", 1, true)
if s and s > 1 then
params[v:sub(1, s - 1)] = UrlDecode(v:sub(s + 1))
end
end
if params.plugin then
local plugin_info = params.plugin
local idx_pn = plugin_info:find(";")
if idx_pn then
result.plugin = plugin_info:sub(1, idx_pn - 1)
result.plugin_opts = plugin_info:sub(idx_pn + 1, #plugin_info)
else
result.plugin = plugin_info
end
end
if result.plugin and result.plugin == "simple-obfs" then
result.plugin = "obfs-local"
end
info = info:sub(1, find_index - 1)
end
local hostInfo = split(base64Decode(UrlDecode(info)), "@")
if hostInfo and #hostInfo > 0 then
local host_port = hostInfo[#hostInfo]
-- [2001:4860:4860::8888]:443
-- 8.8.8.8:443
if host_port:find(":") then
local sp = split(host_port, ":")
result.port = sp[#sp]
if api.is_ipv6addrport(host_port) then
result.address = api.get_ipv6_only(host_port)
else
result.address = sp[1]
end
else
result.address = host_port
end
local userinfo = nil
if #hostInfo > 2 then
userinfo = {}
for i = 1, #hostInfo - 1 do
tinsert(userinfo, hostInfo[i])
end
userinfo = table.concat(userinfo, '@')
else
userinfo = base64Decode(hostInfo[1])
end
local method, password
if userinfo:find(":") then
method = userinfo:sub(1, userinfo:find(":") - 1)
password = userinfo:sub(userinfo:find(":") + 1, #userinfo)
else
password = hostInfo[1] -- Some links use plaintext UUIDs as passwords.
end
-- Determine if the password is URL encoded
local function isURLEncodedPassword(pwd)
if not pwd:find("%%[0-9A-Fa-f][0-9A-Fa-f]") then
return false
end
local ok, decoded = pcall(UrlDecode, pwd)
return ok and UrlEncode(decoded) == pwd
end
local decoded = UrlDecode(password)
if isURLEncodedPassword(password) and decoded then
password = decoded
end
local _method = (method or "none"):lower()
method = (_method == "chacha20-poly1305" and "chacha20-ietf-poly1305") or
(_method == "xchacha20-poly1305" and "xchacha20-ietf-poly1305") or _method
result.method = method
result.password = password
result.tcp_fast_open = params.tfo
result.use_finalmask = (params.fm and params.fm ~= "") and "1" or nil
result.finalmask = (params.fm and params.fm ~= "") and api.base64Encode(params.fm) or nil
local need_upgrade = (result.type ~= "Xray" and result.type ~= "sing-box")
and (params.type and params.type ~= "tcp")
and (params.headerType and params.headerType ~= "none")
if has_xray and (need_upgrade or params.type == "xhttp") then
result.type = "Xray"
result.protocol = "shadowsocks"
elseif has_singbox and need_upgrade then
result.type = "sing-box"
result.protocol = "shadowsocks"
end
if result.plugin then
if result.type == 'Xray' then
-- The obfs-local plugin converts data to a format supported by xray.
if result.plugin ~= "obfs-local" then
result.error_msg = i18n.translatef("%s unsupport SS %s plugin.", "Xray", result.plugin)
else
local obfs = result.plugin_opts:match("obfs=([^;]+)") or ""
local obfs_host = result.plugin_opts:match("obfs%-host=([^;]+)") or ""
if obfs == "" or obfs_host == "" then
result.error_msg = "SS " .. result.plugin .. " " .. i18n.translatef("Plugin options Incomplete.")
end
if obfs == "http" then
result.transport = "raw"
result.tcp_guise = "http"
result.tcp_guise_http_host = (obfs_host and obfs_host ~= "") and { obfs_host } or nil
result.tcp_guise_http_path = { "/" }
elseif obfs == "tls" then
result.tls = "1"
result.tls_serverName = obfs_host
result.tls_allowInsecure = "1"
end
result.plugin = nil
result.plugin_opts = nil
end
elseif result.type == 'sing-box' then
if result.plugin ~= "obfs-local" and result.plugin ~= "v2ray-plugin" then
result.error_msg = i18n.translatef("%s unsupport SS %s plugin.", "Sing-Box", result.plugin)
else
result.plugin_enabled = "1"
end
else
result.plugin_enabled = "1"
end
end
if params.type then
params.type = string.lower(params.type)
if result.type == "sing-box" and params.type == "raw" then
params.type = "tcp"
elseif result.type == "Xray" and params.type == "tcp" then
params.type = "raw"
end
if params.type == "h2" or params.type == "http" then
params.type = "http"
result.transport = (result.type == "Xray") and "xhttp" or "http"
else
result.transport = params.type
end
if result.type ~= "SS-Rust" then
if params.type == 'ws' then
result.ws_host = params.host
result.ws_path = params.path
if result.type == "sing-box" and params.path then
local ws_path_dat = split(params.path, "%?")
local ws_path = ws_path_dat[1]
local ws_path_params = {}
for _, v in pairs(split(ws_path_dat[2], '&')) do
local t = split(v, '=')
ws_path_params[t[1]] = t[2]
end
if ws_path_params.ed and tonumber(ws_path_params.ed) then
result.ws_path = ws_path
result.ws_enableEarlyData = "1"
result.ws_maxEarlyData = tonumber(ws_path_params.ed)
result.ws_earlyDataHeaderName = "Sec-WebSocket-Protocol"
end
end
end
if params.type == "http" then
if result.type == "sing-box" then
result.transport = "http"
result.http_host = (params.host and params.host ~= "") and { params.host } or nil
result.http_path = params.path
elseif result.type == "Xray" then
result.transport = "xhttp"
result.xhttp_mode = "stream-one"
result.xhttp_host = params.host
result.xhttp_path = params.path
end
end
if params.type == 'raw' or params.type == 'tcp' then
result.tcp_guise = params.headerType or "none"
result.tcp_guise_http_host = (params.host and params.host ~= "") and { params.host } or nil
result.tcp_guise_http_path = (params.path and params.path ~= "") and { params.path } or nil
end
if params.type == 'kcp' or params.type == 'mkcp' then
result.transport = "mkcp"
result.mkcp_guise = params.headerType or "none"
result.mkcp_seed = params.seed
end
if params.type == 'quic' then
result.quic_guise = params.headerType or "none"
result.quic_key = params.key
result.quic_security = params.quicSecurity or "none"
end
if params.type == 'grpc' then
if params.path then result.grpc_serviceName = params.path end
if params.serviceName then result.grpc_serviceName = params.serviceName end
result.grpc_mode = params.mode or "gun"
end
if params.type == 'xhttp' then
if result.type ~= "Xray" then
result.error_msg = i18n.translatef("Please replace %s to support %s transmission method.", "Xray", "xhttp")
end
result.xhttp_host = params.host
result.xhttp_path = params.path
result.xhttp_mode = params.mode or "auto"
result.use_xhttp_extra = (params.extra and params.extra ~= "") and "1" or nil
result.xhttp_extra = (params.extra and params.extra ~= "") and api.base64Encode(params.extra) or nil
local success, Data = pcall(jsonParse, params.extra)
if success and Data then
local address = (Data.extra and Data.extra.downloadSettings and Data.extra.downloadSettings.address)
or (Data.downloadSettings and Data.downloadSettings.address)
result.download_address = (address and address ~= "") and address:gsub("^%[", ""):gsub("%]$", "") or nil
end
end
result.tls = "0"
if params.security == "tls" or params.security == "reality" then
result.tls = "1"
result.tls_serverName = (params.sni and params.sni ~= "") and params.sni or params.host
result.alpn = params.alpn
if params.fp and params.fp ~= "" then
result.utls = "1"
result.fingerprint = params.fp
end
if params.ech and params.ech ~= "" then
result.ech = "1"
result.ech_config = params.ech
end
result.tls_pinSHA256 = params.pcs
result.tls_CertByName = params.vcn
if params.security == "reality" then
result.reality = "1"
result.reality_publicKey = params.pbk or nil
result.reality_shortId = params.sid or nil
result.reality_spiderX = params.spx or nil
result.use_mldsa65Verify = (params.pqv and params.pqv ~= "") and "1" or nil
result.reality_mldsa65Verify = params.pqv or nil
end
end
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (sub_allowinsecure and "1" or "0")
result.uot = params.udp
else
result.error_msg = i18n.translatef("Please replace Xray or Sing-Box to support more transmission methods in Shadowsocks.")
end
end
if params["shadow-tls"] then
if result.type ~= "sing-box" and result.type ~= "SS-Rust" then
result.error_msg = sub_ss_type .. " " .. i18n.translatef("unsupport %s plugin.", "shadow-tls")
else
-- Parsing SS Shadow-TLS plugin parameters
local function parseShadowTLSParams(b64str, out)
local ok, data = pcall(jsonParse, base64Decode(b64str))
if not ok or type(data) ~= "table" then return "" end
if type(out) == "table" then
for k, v in pairs(data) do out[k] = v end
end
local t = {}
if data.version then t[#t+1] = "v" .. data.version .. "=1" end
if data.password then t[#t+1] = "passwd=" .. data.password end
for k, v in pairs(data) do
if k ~= "version" and k ~= "password" then
t[#t+1] = k .. "=" .. tostring(v)
end
end
return table.concat(t, ";")
end
if result.type == "SS-Rust" then
result.plugin_enabled = "1"
result.plugin = "shadow-tls"
result.plugin_opts = parseShadowTLSParams(params["shadow-tls"])
elseif result.type == "sing-box" then
local shadowtlsOpt = {}
parseShadowTLSParams(params["shadow-tls"], shadowtlsOpt)
if next(shadowtlsOpt) then
result.shadowtls = "1"
result.shadowtls_version = shadowtlsOpt.version or "1"
result.shadowtls_password = shadowtlsOpt.password
result.shadowtls_serverName = shadowtlsOpt.host
if shadowtlsOpt.fingerprint then
result.shadowtls_utls = "1"
result.shadowtls_fingerprint = shadowtlsOpt.fingerprint or "chrome"
end
end
end
end
end
end
elseif szType == "trojan" then
if sub_trojan_type == "sing-box" and has_singbox then
result.type = 'sing-box'
result.protocol = 'trojan'
elseif sub_trojan_type == "xray" and has_xray then
result.type = 'Xray'
result.protocol = 'trojan'
else
log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", "Trojan", "Trojan"))
return nil
end
local alias = ""
if content:find("#") then
local idx_sp = content:find("#")
alias = content:sub(idx_sp + 1, -1)
content = content:sub(0, idx_sp - 1)
end
result.remarks = UrlDecode(alias)
if content:find("@") then
local Info = split(content, "@")
result.password = UrlDecode(Info[1])
local port = "443"
Info[2] = (Info[2] or ""):gsub("/%?", "?")
local query = split(Info[2], "%?")
local host_port = query[1]
local params = {}
for _, v in pairs(split(query[2], '&')) do
local s = v:find("=", 1, true)
if s and s > 1 then
params[v:sub(1, s - 1)] = UrlDecode(v:sub(s + 1))
end
end
-- [2001:4860:4860::8888]:443
-- 8.8.8.8:443
if host_port:find(":") then
local sp = split(host_port, ":")
port = sp[#sp]
if api.is_ipv6addrport(host_port) then
result.address = api.get_ipv6_only(host_port)
else
result.address = sp[1]
end
else
result.address = host_port
end
result.port = port
params.security = params.security or "tls"
if params.security == "tls" or params.security == "reality" then
result.tls = '1'
result.tls_serverName = params.peer or params.sni or ""
result.alpn = params.alpn
if params.fp and params.fp ~= "" then
result.utls = "1"
result.fingerprint = params.fp
end
if params.security == "reality" then
result.reality = "1"
result.reality_publicKey = params.pbk or nil
result.reality_shortId = params.sid or nil
end
result.tls_pinSHA256 = params.pcs
result.tls_CertByName = params.vcn
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (sub_allowinsecure and "1" or "0")
end
if not params.type then params.type = "tcp" end
params.type = string.lower(params.type)
if result.type == "sing-box" and params.type == "raw" then
params.type = "tcp"
elseif result.type == "Xray" and params.type == "tcp" then
params.type = "raw"
end
if params.type == "h2" or params.type == "http" then
params.type = "http"
result.transport = (result.type == "Xray") and "xhttp" or "http"
else
result.transport = params.type
end
if params.type == 'ws' then
result.ws_host = params.host
result.ws_path = params.path
if result.type == "sing-box" and params.path then
local ws_path_dat = split(params.path, "%?")
local ws_path = ws_path_dat[1]
local ws_path_params = {}
for _, v in pairs(split(ws_path_dat[2], '&')) do
local t = split(v, '=')
ws_path_params[t[1]] = t[2]
end
if ws_path_params.ed and tonumber(ws_path_params.ed) then
result.ws_path = ws_path
result.ws_enableEarlyData = "1"
result.ws_maxEarlyData = tonumber(ws_path_params.ed)
result.ws_earlyDataHeaderName = "Sec-WebSocket-Protocol"
end
end
end
if params.type == "http" then
if result.type == "sing-box" then
result.transport = "http"
result.http_host = (params.host and params.host ~= "") and { params.host } or nil
result.http_path = params.path
elseif result.type == "Xray" then
result.transport = "xhttp"
result.xhttp_mode = "stream-one"
result.xhttp_host = params.host
result.xhttp_path = params.path
end
end
if params.type == 'raw' or params.type == 'tcp' then
result.tcp_guise = params.headerType or "none"
result.tcp_guise_http_host = (params.host and params.host ~= "") and { params.host } or nil
result.tcp_guise_http_path = (params.path and params.path ~= "") and { params.path } or nil
end
if params.type == 'kcp' or params.type == 'mkcp' then
result.transport = "mkcp"
result.mkcp_guise = params.headerType or "none"
result.mkcp_seed = params.seed
end
if params.type == 'quic' then
result.quic_guise = params.headerType or "none"
result.quic_key = params.key
result.quic_security = params.quicSecurity or "none"
end
if params.type == 'grpc' then
result.grpc_serviceName = params.serviceName or params.path
result.grpc_mode = params.mode or "gun"
end
if params.type == 'xhttp' then
result.xhttp_host = params.host
result.xhttp_path = params.path
end
if params.type == 'httpupgrade' then
result.httpupgrade_host = params.host
result.httpupgrade_path = params.path
end
result.tcp_fast_open = params.tfo
result.use_finalmask = (params.fm and params.fm ~= "") and "1" or nil
result.finalmask = (params.fm and params.fm ~= "") and api.base64Encode(params.fm) or nil
if result.type == "sing-box" and (result.transport == "mkcp" or result.transport == "xhttp") then
log(2, i18n.translatef("Skip node: %s. Because Sing-Box does not support the %s protocol's %s transmission method, Xray needs to be used instead.", result.remarks, szType, result.transport))
return nil
end
end
elseif szType == "ssd" then
result = set_ss_implementation(sub_ss_type, result)
if not result then return nil end
result.address = content.server
result.port = content.port
result.password = content.password
result.method = content.encryption
result.plugin = content.plugin
result.plugin_opts = content.plugin_options
result.group = content.airport
result.remarks = content.remarks
elseif szType == "vless" then
if sub_vless_type == "sing-box" and has_singbox then
result.type = 'sing-box'
elseif sub_vless_type == "xray" and has_xray then
result.type = "Xray"
else
log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", "VLESS", "VLESS"))
return nil
end
result.protocol = "vless"
local alias = ""
if content:find("#") then
local idx_sp = content:find("#")
alias = content:sub(idx_sp + 1, -1)
content = content:sub(0, idx_sp - 1)
end
result.remarks = UrlDecode(alias)
if content:find("@") then
local Info = split(content, "@")
result.uuid = UrlDecode(Info[1])
local port = "443"
Info[2] = (Info[2] or ""):gsub("/%?", "?")
local query = split(Info[2], "%?")
local host_port = query[1]
local params = {}
for _, v in pairs(split(query[2], '&')) do
local s = v:find("=", 1, true)
if s and s > 1 then
params[v:sub(1, s - 1)] = UrlDecode(v:sub(s + 1))
end
end
-- [2001:4860:4860::8888]:443
-- 8.8.8.8:443
if host_port:find(":") then
local sp = split(host_port, ":")
port = sp[#sp]
if api.is_ipv6addrport(host_port) then
result.address = api.get_ipv6_only(host_port)
else
result.address = sp[1]
end
else
result.address = host_port
end
if not params.type then params.type = "tcp" end
params.type = string.lower(params.type)
if ({ xhttp=true, kcp=true, mkcp=true })[params.type] and result.type ~= "Xray" and has_xray then
result.type = "Xray"
end
if result.type == "sing-box" and params.type == "raw" then
params.type = "tcp"
elseif result.type == "Xray" and params.type == "tcp" then
params.type = "raw"
end
if params.type == "h2" or params.type == "http" then
params.type = "http"
result.transport = (result.type == "Xray") and "xhttp" or "http"
else
result.transport = params.type
end
if params.type == 'ws' then
result.ws_host = params.host
result.ws_path = params.path
if result.type == "sing-box" and params.path then
local ws_path_dat = split(params.path, "%?")
local ws_path = ws_path_dat[1]
local ws_path_params = {}
for _, v in pairs(split(ws_path_dat[2], '&')) do
local t = split(v, '=')
ws_path_params[t[1]] = t[2]
end
if ws_path_params.ed and tonumber(ws_path_params.ed) then
result.ws_path = ws_path
result.ws_enableEarlyData = "1"
result.ws_maxEarlyData = tonumber(ws_path_params.ed)
result.ws_earlyDataHeaderName = "Sec-WebSocket-Protocol"
end
end
end
if params.type == "http" then
if result.type == "sing-box" then
result.transport = "http"
result.http_host = (params.host and params.host ~= "") and { params.host } or nil
result.http_path = params.path
elseif result.type == "Xray" then
result.transport = "xhttp"
result.xhttp_mode = "stream-one"
result.xhttp_host = params.host
result.xhttp_path = params.path
end
end
if params.type == 'raw' or params.type == 'tcp' then
result.tcp_guise = params.headerType or "none"
result.tcp_guise_http_host = (params.host and params.host ~= "") and { params.host } or nil
result.tcp_guise_http_path = (params.path and params.path ~= "") and { params.path } or nil
end
if params.type == 'kcp' or params.type == 'mkcp' then
result.transport = "mkcp"
result.mkcp_guise = params.headerType or "none"
result.mkcp_seed = params.seed
end
if params.type == 'quic' then
result.quic_guise = params.headerType or "none"
result.quic_key = params.key
result.quic_security = params.quicSecurity or "none"
end
if params.type == 'grpc' then
if params.path then result.grpc_serviceName = params.path end
if params.serviceName then result.grpc_serviceName = params.serviceName end
result.grpc_mode = params.mode or "gun"
end
if params.type == 'xhttp' or params.type == 'splithttp' then
result.xhttp_host = params.host
result.xhttp_path = params.path
result.xhttp_mode = params.mode or "auto"
result.use_xhttp_extra = (params.extra and params.extra ~= "") and "1" or nil
result.xhttp_extra = (params.extra and params.extra ~= "") and api.base64Encode(params.extra) or nil
local success, Data = pcall(jsonParse, params.extra)
if success and Data then
local address = (Data.extra and Data.extra.downloadSettings and Data.extra.downloadSettings.address)
or (Data.downloadSettings and Data.downloadSettings.address)
result.download_address = (address and address ~= "") and address:gsub("^%[", ""):gsub("%]$", "") or nil
end
end
if params.type == 'httpupgrade' then
result.httpupgrade_host = params.host
result.httpupgrade_path = params.path
end
result.encryption = params.encryption or "none"
result.flow = params.flow
if (not params.security or params.security == "") and params.flow then
params.security = "tls"
end
result.tls = "0"
if params.security == "tls" or params.security == "reality" then
result.tls = "1"
result.tls_serverName = (params.sni and params.sni ~= "") and params.sni or params.host
result.alpn = params.alpn
if params.fp and params.fp ~= "" then
result.utls = "1"
result.fingerprint = params.fp
end
if params.ech and params.ech ~= "" then
result.ech = "1"
result.ech_config = params.ech
end
result.tls_pinSHA256 = params.pcs
result.tls_CertByName = params.vcn
if params.security == "reality" then
result.reality = "1"
result.reality_publicKey = params.pbk or nil
result.reality_shortId = params.sid or nil
result.reality_spiderX = params.spx or nil
result.use_mldsa65Verify = (params.pqv and params.pqv ~= "") and "1" or nil
result.reality_mldsa65Verify = params.pqv or nil
end
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (sub_allowinsecure and "1" or "0")
end
result.port = port
result.tcp_fast_open = params.tfo
result.use_finalmask = (params.fm and params.fm ~= "") and "1" or nil
result.finalmask = (params.fm and params.fm ~= "") and api.base64Encode(params.fm) or nil
if result.type == "sing-box" and (result.transport == "mkcp" or result.transport == "xhttp") then
log(2, i18n.translatef("Skip node: %s. Because Sing-Box does not support the %s protocol's %s transmission method, Xray needs to be used instead.", result.remarks, szType, result.transport))
return nil
end
end
elseif szType == 'hysteria' then
if has_singbox then
result.type = 'sing-box'
result.protocol = "hysteria"
else
log(2, i18n.translatef("Skip the %s node because the %s core program is not installed.", "Hysteria", "Hysteria", "Sing-Box"))
return nil
end
local alias = ""
if content:find("#") then
local idx_sp = content:find("#")
alias = content:sub(idx_sp + 1, -1)
content = content:sub(0, idx_sp - 1)
end
result.remarks = UrlDecode(alias)
local query = split(content:gsub("/%?", "?"), '%?')
local host_port = query[1]
local params = {}
for _, v in pairs(split(query[2], '&')) do
local s = v:find("=", 1, true)
if s and s > 1 then
params[v:sub(1, s - 1)] = v:sub(s + 1)
end
end
-- [2001:4860:4860::8888]:443
-- 8.8.8.8:443
if host_port:find(":") then
local sp = split(host_port, ":")
result.port = sp[#sp]
if api.is_ipv6addrport(host_port) then
result.address = api.get_ipv6_only(host_port)
else
result.address = sp[1]
end
else
result.address = host_port
end
result.hysteria_obfs = params.obfsParam
result.hysteria_auth_type = "string"
result.hysteria_auth_password = params.auth
result.tls_serverName = params.peer or params.sni or ""
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (sub_allowinsecure and "1" or "0")
result.alpn = params.alpn
result.hysteria_up_mbps = params.upmbps or sub_hy_up_mbps
result.hysteria_down_mbps = params.downmbps or sub_hy_down_mbps
result.hysteria_hop = params.mport
elseif szType == 'hysteria2' or szType == 'hy2' then
local alias = ""
if content:find("#") then
local idx_sp = content:find("#")
alias = content:sub(idx_sp + 1, -1)
content = content:sub(0, idx_sp - 1)
end
result.remarks = UrlDecode(alias)
local Info = content
if content:find("@") then
local contents = split(content, "@")
result.hysteria2_auth_password = UrlDecode(contents[1])
Info = (contents[2] or ""):gsub("/%?", "?")
end
local query = split(Info, "%?")
local host_port = query[1]
local params = {}
for _, v in pairs(split(query[2], '&')) do
local s = v:find("=", 1, true)
if s and s > 1 then
params[v:sub(1, s - 1):lower()] = UrlDecode(v:sub(s + 1))
end
end
-- [2001:4860:4860::8888]:443
-- 8.8.8.8:443
if host_port:find(":") then
local sp = split(host_port, ":")
result.port = sp[#sp]
if api.is_ipv6addrport(host_port) then
result.address = api.get_ipv6_only(host_port)
else
result.address = sp[1]
end
else
result.address = host_port
end
result.tls_serverName = params.sni
result.tls_pinSHA256 = params.pcs or params.pinsha256
result.tls_CertByName = params.vcn
local insecure = params.allowinsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (sub_allowinsecure and "1" or "0")
result.hysteria2_up_mbps = params.upmbps or sub_hy_up_mbps
result.hysteria2_down_mbps = params.downmbps or sub_hy_down_mbps
result.hysteria2_hop = params.mport
if params["obfs-password"] or params["obfs_password"] then
result.hysteria2_obfs_type = params.obfs or "salamander"
result.hysteria2_obfs_password = params["obfs-password"] or params["obfs_password"]
end
if params.obfs == "gecko" then
result.hysteria2_obfs_MinPacketSize = params.minpacketsize or "512"
result.hysteria2_obfs_MaxPacketSize = params.maxpacketsize or "1200"
end
if (sub_hysteria2_type == "sing-box" and has_singbox) or (sub_hysteria2_type == "xray" and has_xray) then
local is_singbox = sub_hysteria2_type == "sing-box" and has_singbox
result.type = is_singbox and 'sing-box' or 'Xray'
result.protocol = "hysteria2"
result.use_finalmask = (params.fm and params.fm ~= "") and "1" or nil
result.finalmask = (params.fm and params.fm ~= "") and api.base64Encode(params.fm) or nil
elseif has_hysteria2 then
result.type = "Hysteria2"
else
log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", "Hysteria2", "Hysteria2"))
return nil
end
elseif szType == 'tuic' then
if has_singbox then
result.type = 'sing-box'
result.protocol = "tuic"
else
log(2, i18n.translatef("Skip the %s node because the %s core program is not installed.", "Tuic", "Tuic", "Sing-Box"))
return nil
end
local alias = ""
if content:find("#") then
local idx_sp = content:find("#")
alias = content:sub(idx_sp + 1, -1)
content = content:sub(0, idx_sp - 1)
end
result.remarks = UrlDecode(alias)
local Info = content
if content:find("@", 1, true) then
local contents = split(content, "@")
local auth = contents[1] or ""
local idx = auth:find(":", 1, true)
if not idx then -- Fix for some links will encode the ':' between the UUID and password.
auth = UrlDecode(auth)
idx = auth:find(":", 1, true)
end
if idx then
result.uuid = UrlDecode(auth:sub(1, idx - 1))
result.password = UrlDecode(auth:sub(idx + 1))
end
Info = (contents[2] or ""):gsub("/%?", "?")
end
local query = split(Info, "%?")
local host_port = query[1]
local params = {}
for _, v in pairs(split(query[2], '&')) do
local s = v:find("=", 1, true)
if s and s > 1 then
params[v:sub(1, s - 1):lower()] = UrlDecode(v:sub(s + 1))
end
end
if host_port:find(":") then
local sp = split(host_port, ":")
result.port = sp[#sp]
if api.is_ipv6addrport(host_port) then
result.address = api.get_ipv6_only(host_port)
else
result.address = sp[1]
end
else
result.address = host_port
end
result.tls_serverName = params.sni
result.tls_disable_sni = params.disable_sni
result.tuic_alpn = params.alpn or "h3"
result.tuic_congestion_control = params.congestion_control or "cubic"
result.tuic_udp_relay_mode = params.udp_relay_mode or "native"
local insecure = params.allowinsecure or params.insecure or params.allow_insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (sub_allowinsecure and "1" or "0")
elseif szType == "anytls" then
if has_singbox then
result.type = 'sing-box'
result.protocol = "anytls"
else
log(2, i18n.translatef("Skip the %s node because the %s core program is not installed.", "AnyTLS", "AnyTLS", "Sing-Box 1.12"))
return nil
end
local alias = ""
if content:find("#") then
local idx_sp = content:find("#")
alias = content:sub(idx_sp + 1, -1)
content = content:sub(0, idx_sp - 1)
end
result.remarks = UrlDecode(alias)
if content:find("@") then
local Info = split(content, "@")
result.password = UrlDecode(Info[1])
local port = "443"
Info[2] = (Info[2] or ""):gsub("/%?", "?")
local query = split(Info[2], "%?")
local host_port = query[1]
local params = {}
for _, v in pairs(split(query[2], '&')) do
local s = v:find("=", 1, true)
if s and s > 1 then
params[v:sub(1, s - 1):lower()] = UrlDecode(v:sub(s + 1))
end
end
-- [2001:4860:4860::8888]:443
-- 8.8.8.8:443
if host_port:find(":") then
local sp = split(host_port, ":")
port = sp[#sp]
if api.is_ipv6addrport(host_port) then
result.address = api.get_ipv6_only(host_port)
else
result.address = sp[1]
end
else
result.address = host_port
end
result.tls = "0"
if not params.security or params.security == "" then
params.security = "tls"
end
if params.security == "tls" or params.security == "reality" then
result.tls = "1"
result.tls_serverName = params.sni
result.alpn = params.alpn
if params.fp and params.fp ~= "" then
result.utls = "1"
result.fingerprint = params.fp
end
if params.security == "reality" then
result.reality = "1"
result.reality_publicKey = params.pbk or nil
result.reality_shortId = params.sid or nil
end
if params.ech and params.ech ~= "" then
result.ech = "1"
result.ech_config = params.ech
end
end
result.port = port
local insecure = params.allowinsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (sub_allowinsecure and "1" or "0")
end
elseif szType == 'naive+https' or szType == 'naive+quic' then
if has_singbox then
result.type = 'sing-box'
result.protocol = "naive"
else
log(2, i18n.translatef("Skip the %s node because the %s core program is not installed.", "NaïveProxy", "Sing-Box"))
return nil
end
local alias = ""
if content:find("#") then
local idx_sp = content:find("#")
alias = content:sub(idx_sp + 1, -1)
content = content:sub(0, idx_sp - 1)
end
result.remarks = UrlDecode(alias)
local Info = content
if content:find("@", 1, true) then
local contents = split(content, "@")
local auth = contents[1] or ""
local idx = auth:find(":", 1, true)
if not idx then -- Fixed some links that encode the colon between username and password.
auth = UrlDecode(auth)
idx = auth:find(":", 1, true)
end
if idx then
result.username = UrlDecode(auth:sub(1, idx - 1))
result.password = UrlDecode(auth:sub(idx + 1))
end
Info = (contents[2] or ""):gsub("/%?", "?")
end
local query = split(Info, "%?")
local host_port = query[1]
local params = {}
for _, v in pairs(split(query[2], '&')) do
local s = v:find("=", 1, true)
if s and s > 1 then
params[v:sub(1, s - 1)] = UrlDecode(v:sub(s + 1))
end
end
if host_port:find(":") then
local sp = split(host_port, ":")
result.port = sp[#sp]
if api.is_ipv6addrport(host_port) then
result.address = api.get_ipv6_only(host_port)
else
result.address = sp[1]
end
else
result.address = host_port
end
result.tls_serverName = params.sni
result.uot = params.uot
result.naive_insecure_concurrency = params["insecure-concurrency"] or "0"
if params.ech and params.ech ~= "" then
result.ech = "1"
result.ech_config = params.ech
end
if szType == "naive+quic" then
result.naive_quic = "1"
result.naive_congestion_control = params.congestion_control or "bbr"
end
else
log(2, i18n.translatef("%s type node subscriptions are not currently supported, skip this node.", szType))
return nil
end
if not result.remarks or result.remarks == "" then
if result.address and result.port then
result.remarks = result.address .. ':' .. result.port
else
result.remarks = "NULL"
end
end
return result
end
local function curl(url, file, ua, mode, hwid)
if not url or url == "" then return 22, 404 end
local curl_args = {
"-fskL", "-w %{http_code}", "--retry 3", "--connect-timeout 3", "-H 'Accept-Encoding: identity'"
}
if ua and ua ~= "" and ua ~= "curl" then
ua = (ua == "passwall2") and ("passwall2/" .. api.get_version()) or ua
curl_args[#curl_args + 1] = '--user-agent "' .. ua .. '"'
end
if hwid == "1" then
curl_args[#curl_args + 1] = get_headers()
end
local return_code, result
if mode == "direct" then
return_code, result = api.curl_direct(url, file, curl_args)
elseif mode == "proxy" then
return_code, result = api.curl_proxy(url, file, curl_args)
else
return_code, result = api.curl_auto(url, file, curl_args)
end
return return_code, tonumber(result)
end
function get_headers()
local cache_file = "/tmp/etc/" .. appname .. "_tmp/sub_curl_headers"
if fs.access(cache_file) then
return luci.sys.exec("cat " .. cache_file)
end
local headers = {}
local function readfile(path)
local f = io.open(path, "r")
if not f then return nil end
local c = f:read("*a")
f:close()
return api.trim(c)
end
headers[#headers + 1] = "x-device-os: OpenWrt"
local rel = readfile("/etc/openwrt_release")
local os_ver = rel and rel:match("DISTRIB_RELEASE='([^']+)'")
if os_ver then
headers[#headers + 1] = "x-ver-os: " .. os_ver
end
local model = readfile("/tmp/sysinfo/model")
if model then
headers[#headers + 1] = "x-device-model: " .. model
end
local mac = readfile("/sys/class/net/eth0/address")
if mac and model then
local raw = mac .. "-" .. model
local p = io.popen("printf '%s' '" .. raw:gsub("'", "'\\''") .. "' | sha256sum")
if p then
local hash = p:read("*l")
p:close()
hash = hash and hash:match("^%w+")
if hash then
headers[#headers + 1] = "x-hwid: " .. hash
end
end
end
local out = {}
for i = 1, #headers do
out[i] = "-H '" .. headers[i]:gsub("'", "'\\''") .. "'"
end
local headers_str = table.concat(out, " ")
local f = io.open(cache_file, "w"); if f then f:write(headers_str); f:close() end
return headers_str
end
local function truncate_nodes(group)
for _, config in pairs(CONFIG) do
if config.currentNodes and #config.currentNodes > 0 then
local newNodes = {}
local removeNodesSet = {}
for k, v in pairs(config.currentNodes) do
if v.currentNode and v.currentNode.add_mode == "2" then
if (not group) or (group:lower() == (v.currentNode.group or ""):lower()) then
removeNodesSet[v.currentNode[".name"]] = true
end
end
end
for _, value in ipairs(config.currentNodes) do
if not removeNodesSet[value.currentNode[".name"]] then
newNodes[#newNodes + 1] = value.currentNode[".name"]
end
end
if config.set then
config.set(config, newNodes)
end
else
if config.currentNode and config.currentNode.add_mode == "2" then
if (not group) or (group:lower() == (config.currentNode.group or ""):lower()) then
if config.delete then
config.delete(config)
elseif config.set then
config.set(config, "")
end
end
end
end
end
uci:foreach(appname, "nodes", function(node)
if node.add_mode == "2" then
if (not group) or (group:lower() == (node.group or ""):lower()) then
uci:delete(appname, node['.name'])
end
end
end)
uci:foreach(appname, "subscribe_list", function(o)
if (not group) or (group:lower() == (o.remark or ""):lower()) then
uci:delete(appname, o['.name'], "md5")
end
end)
api.uci_save(uci, appname, true)
end
local function select_node(nodes, config, parentConfig)
local log_level = 1
if parentConfig then
log_level = log_level + 1
end
if config.currentNode then
local server
-- Load balancing, keeping the original ID of the Socks [port] node in urltest.
if config.currentNode["Socks"] then
server = config.currentNode.Socks
end
-- Special priority: cfgid
if config.currentNode[".name"] then
for index, node in pairs(nodes) do
if node[".name"] == config.currentNode[".name"] then
if config.log == nil or config.log == true then
log(log_level, i18n.translatef("Update [%s]", config.remarks) .. " " .. i18n.translatef("Matching node:") .. " " .. node.remarks)
end
server = node[".name"]
break
end
end
end
-- First priority: Type + Notes + IP + Port + Group
if not server then
for index, node in pairs(nodes) do
if config.currentNode.type and config.currentNode.remarks and config.currentNode.address and config.currentNode.port then
if node.type and node.remarks and node.address and node.port then
if node.type == config.currentNode.type and node.remarks == config.currentNode.remarks and (node.address .. ':' .. node.port == config.currentNode.address .. ':' .. config.currentNode.port) and node.group == config.currentNode.group then
if config.log == nil or config.log == true then
log(log_level, i18n.translatef("Update [%s]", config.remarks) .. " " .. i18n.translatef("First Matching node:") .. " " .. node.remarks)
end
server = node[".name"]
break
end
end
end
end
end
-- Second priority: Type + IP + Port + Group
if not server then
for index, node in pairs(nodes) do
if config.currentNode.type and config.currentNode.address and config.currentNode.port then
if node.type and node.address and node.port then
if node.type == config.currentNode.type and (node.address .. ':' .. node.port == config.currentNode.address .. ':' .. config.currentNode.port) and node.group == config.currentNode.group then
if config.log == nil or config.log == true then
log(log_level, i18n.translatef("Update [%s]", config.remarks) .. " " .. i18n.translatef("Second Matching node:") .. " " .. node.remarks)
end
server = node[".name"]
break
end
end
end
end
end
-- Third priority: IP + Port + Group
if not server then
for index, node in pairs(nodes) do
if config.currentNode.address and config.currentNode.port then
if node.address and node.port then
if node.address .. ':' .. node.port == config.currentNode.address .. ':' .. config.currentNode.port and node.group == config.currentNode.group then
if config.log == nil or config.log == true then
log(log_level, i18n.translatef("Update [%s]", config.remarks) .. " " .. i18n.translatef("Third Matching node:") .. " " .. node.remarks)
end
server = node[".name"]
break
end
end
end
end
end
-- Fourth priority: IP + Group
if not server then
for index, node in pairs(nodes) do
if config.currentNode.address then
if node.address then
if node.address == config.currentNode.address and node.group == config.currentNode.group then
if config.log == nil or config.log == true then
log(log_level, i18n.translatef("Update [%s]", config.remarks) .. " " .. i18n.translatef("Fourth Matching node:") .. " " .. node.remarks)
end
server = node[".name"]
break
end
end
end
end
end
-- Fifth priority: remarks + Group
if not server then
for index, node in pairs(nodes) do
if config.currentNode.remarks then
if node.remarks then
if node.remarks == config.currentNode.remarks and node.group == config.currentNode.group then
if config.log == nil or config.log == true then
log(log_level, i18n.translatef("Update [%s]", config.remarks) .. " " .. i18n.translatef("Fifth Matching node:") .. " " .. node.remarks)
end
server = node[".name"]
break
end
end
end
end
end
if not parentConfig then
-- If that doesn't work, just find one.
if not server then
if #nodes_table > 0 then
if config.log == nil or config.log == true then
log(log_level, i18n.translatef("Update [%s]", config.remarks) .. " " .. i18n.translatef("Unable to find the best matching node, now replaced with:") .. " " .. nodes_table[1].remarks)
end
server = nodes_table[1][".name"]
end
end
end
if server then
if parentConfig then
config.set(parentConfig, server)
else
config.set(config, server)
end
end
else
if not parentConfig then
config.set(config, "")
end
end
end
local function update_node(manual)
if next(nodeResult) == nil then
log(1, i18n.translatef("No node information updates are available."))
return
end
local group = {}
for _, v in ipairs(nodeResult) do
group[v["remark"]:lower()] = true
end
if manual == 0 and next(group) then
uci:foreach(appname, "nodes", function(node)
-- Do not delete nodes if no new nodes are found or nodes were manually imported...
if node.add_mode == "2" and (node.group and group[node.group:lower()] == true) then
uci:delete(appname, node['.name'])
end
end)
end
for _, v in ipairs(nodeResult) do
local remark = v["remark"]
local list = v["list"]
local sub_cfg = v["sub_cfg"]
local domain_resolver, domain_resolver_dns, domain_resolver_dns_https, domain_strategy
local preproxy_node_group, to_node_group, outbound_iface_group, chain_node_type = "", "", "", ""
-- Subscription Group Chain Agent
local function valid_chain_node(node)
if not node then return "" end
local cp = uci:get(appname, node, "chain_proxy") or ""
local am = uci:get(appname, node, "add_mode") or "0"
chain_node_type = (cp == "" and am ~= "2") and (uci:get(appname, node, "type") or "") or ""
if chain_node_type ~= "Xray" and chain_node_type ~= "sing-box" then
chain_node_type = ""
return ""
end
return node
end
if sub_cfg then
domain_resolver = sub_cfg.domain_resolver
domain_resolver_dns = sub_cfg.domain_resolver_dns
domain_resolver_dns_https = sub_cfg.domain_resolver_dns_https
domain_strategy = (sub_cfg.domain_strategy == "UseIPv4" or sub_cfg.domain_strategy == "UseIPv6") and sub_cfg.domain_strategy or nil
preproxy_node_group = (sub_cfg.chain_proxy == "1") and valid_chain_node(sub_cfg.preproxy_node) or ""
to_node_group = (sub_cfg.chain_proxy == "2") and valid_chain_node(sub_cfg.to_node) or ""
outbound_iface_group = (sub_cfg.chain_proxy == "3") and sub_cfg.outbound_iface or ""
chain_node_type = (outbound_iface_group ~= "") and "iface" or chain_node_type
end
for _, vv in ipairs(list) do
local cfgid = uci:section(appname, "nodes", api.gen_short_uuid())
for kkk, vvv in pairs(vv) do
if type(vvv) == "table" and next(vvv) ~= nil then
uci:set_list(appname, cfgid, kkk, vvv)
else
if kkk ~= "group" or vvv ~= "default" then
uci:set(appname, cfgid, kkk, vvv)
end
-- Sing-Box Node Domain resolver
if kkk == "type" and (vvv == "Xray" or vvv == "sing-box") then
if domain_resolver then
uci:set(appname, cfgid, "domain_resolver", domain_resolver)
if domain_resolver_dns then
uci:set(appname, cfgid, "domain_resolver_dns", domain_resolver_dns)
elseif domain_resolver_dns_https then
uci:set(appname, cfgid, "domain_resolver_dns_https", domain_resolver_dns_https)
end
end
if domain_strategy then
if vvv == "sing-box" then
domain_strategy = (domain_strategy == "UseIPv4" and "ipv4_only") or (domain_strategy == "UseIPv6" and "ipv6_only") or domain_strategy
end
uci:set(appname, cfgid, "domain_strategy", domain_strategy)
end
end
-- Subscription Group Chain Agent
if chain_node_type ~= "" and kkk == "type" and (vvv == "Xray" or vvv == "sing-box") then
if preproxy_node_group ~="" then
uci:set(appname, cfgid, "chain_proxy", "1")
uci:set(appname, cfgid, "preproxy_node", preproxy_node_group)
elseif to_node_group ~= "" then
uci:set(appname, cfgid, "chain_proxy", "2")
uci:set(appname, cfgid, "to_node", to_node_group)
elseif outbound_iface_group ~= "" then
uci:set(appname, cfgid, "chain_proxy", "3")
uci:set(appname, cfgid, "outbound_iface", outbound_iface_group)
end
end
end
end
end
end
-- Update subscription information
for cfgid, info in pairs(subscribe_info) do
for key, value in pairs(info) do
if value ~= "" then
uci:set(appname, cfgid, key, value)
else
uci:delete(appname, cfgid, key)
end
end
end
if next(CONFIG) then
local nodes = {}
uci:foreach(appname, "nodes", function(node)
nodes[#nodes + 1] = node
end)
for _, config in pairs(CONFIG) do
if config.currentNodes and #config.currentNodes > 0 then
if config.remarks and config.currentNodes[1].log ~= false then
log(1, i18n.translatef("Update [%s]", config.remarks))
end
for kk, vv in pairs(config.currentNodes) do
select_node(nodes, vv, config)
end
config.set(config)
if not config.newNodes or #config.newNodes == 0 then
log(1, i18n.translatef("[%s]", config.remarks) .. " " .. i18n.translate("Unable to find a new node. Please confirm and process manually."))
end
else
select_node(nodes, config)
end
end
end
api.uci_save(uci, appname, true)
if arg[3] == "cron" then
if not fs.access("/var/lock/" .. appname .. ".lock") then
luci.sys.call("touch /tmp/lock/" .. appname .. "_cron.lock")
end
end
if manual ~= 1 then
luci.sys.call("/etc/init.d/" .. appname .. " restart > /dev/null 2>&1 &")
end
end
local function parse_link(raw, add_mode, group, sub_cfg)
if raw and #raw > 0 then
local cfgid
if sub_cfg then
cfgid = sub_cfg[".name"]
end
local nodes, szType, clashTable
local node_list = {}
-- ssd appear to be in this format, starting with ssd://.
if raw:find('ssd://') then
szType = 'ssd'
local nEnd = select(2, raw:find('ssd://'))
nodes = base64Decode(raw:sub(nEnd + 1, #raw))
nodes = jsonParse(nodes)
local extra = {
airport = nodes.airport,
port = nodes.port,
encryption = nodes.encryption,
password = nodes.password
}
local servers = {}
-- SS is wrapped inside, so let's just like this.
for _, server in ipairs(nodes.servers) do
tinsert(servers, setmetatable(server, { __index = extra }))
end
nodes = servers
else
-- Try parseYAML, if success, is clash.
local yamlTable = lyaml.load(raw)
if yamlTable and type(yamlTable) == "table" then
-- clash
szType = "clash"
clashTable = yamlTable
else
-- Formats other than ssd
if add_mode == "1" then
nodes = split(raw, "\n")
else
nodes = split(base64Decode(raw):gsub("\r\n", "\n"), "\n")
end
end
end
function nodeFilter(node)
if node then
if node.error_msg then
log(2, i18n.translatef("Discard node: %s, Reason:", node.remarks) .. " " .. node.error_msg)
elseif not node.type then
log(2, i18n.translatef("Discard node: %s, Reason:", node.remarks) .. " " .. i18n.translatef("No usable binary was found."))
elseif (add_mode == "2" and is_filter_keyword(sub_cfg, node.remarks)) or not node.address or node.remarks == "NULL" or node.address == "127.0.0.1" or
(not datatypes.hostname(node.address) and not (api.is_ip(node.address))) then
log(2, i18n.translatef("Discard filter nodes: %s type node %s", node.type, node.remarks))
else
tinsert(node_list, node)
end
if add_mode == "2" then
get_subscribe_info(cfgid, node.remarks)
end
end
end
if szType == "clash" and clashTable then
nodes = {}
local clashNodes = processClashData(clashTable, add_mode, group, sub_cfg)
for _, v in ipairs(clashNodes) do
nodeFilter(v)
end
end
for _, v in ipairs(nodes) do
if v and (szType == 'ssd' or not string.match(v, "^%s*$")) then
xpcall(function ()
local result
if szType == 'ssd' then
result = processData(szType, v, add_mode, group, sub_cfg)
elseif not szType then
local node = api.trim(v)
local dat = split(node, "://")
if dat and dat[1] and dat[2] then
if dat[1] == 'vmess' or dat[1] == 'ssr' then
local link = api.trim(dat[2]:gsub("#.*$", ""))
result = processData(dat[1], base64Decode(link), add_mode, group, sub_cfg)
else
local link = dat[2]:gsub("&amp;", "&"):gsub("%s*#%s*", "#") -- Some odd links use "&" as "&", and include spaces before and after "#".
result = processData(dat[1], link, add_mode, group, sub_cfg)
end
end
else
log(2, i18n.translatef("Skip unknown types:") .. " " .. szType)
end
-- log(2, result)
nodeFilter(result)
end, function (err)
--log(2, err)
log(2, v, i18n.translatef("Parsing error, skip this node."))
end
)
end
end
if #node_list > 0 then
nodeResult[#nodeResult + 1] = {
remark = group,
list = node_list,
sub_cfg = sub_cfg
}
end
log(2, i18n.translatef("Successfully resolved the [%s] node, number: %s", group, #node_list))
else
if add_mode == "2" then
log(2, i18n.translatef("Get subscription content for [%s] is empty. This may be due to an invalid subscription address or a network problem. Please diagnose the issue!", group))
end
end
end
local execute = function()
do
local subscribe_list = {}
local fail_list = {}
if arg[2] ~= "all" then
string.gsub(arg[2], '[^' .. "," .. ']+', function(w)
subscribe_list[#subscribe_list + 1] = uci:get_all(appname, w) or {}
end)
else
uci:foreach(appname, "subscribe_list", function(o)
subscribe_list[#subscribe_list + 1] = o
end)
end
local manual_sub = arg[3] == "manual"
for index, value in ipairs(subscribe_list) do
local cfgid = value[".name"]
local remark = value.remark or ""
local url = value.url or ""
local url_is_local
if fs.access(url) then
-- debug, reads local files.
log(1, i18n.translatef("Start subscribing: %s", '' .. remark .. '' .. url))
url_is_local = true
tmp_file = url
else
local ua = value.user_agent
local access_mode = value.access_mode
local result = (not access_mode) and i18n.translatef("Auto") or (access_mode == "direct" and i18n.translatef("Direct") or (access_mode == "proxy" and i18n.translatef("Proxy") or i18n.translatef("Auto")))
log(1, i18n.translatef("Start subscribing: %s", '' .. remark .. '' .. url .. ' [' .. result .. ']'))
tmp_file = "/tmp/" .. cfgid
local return_code
return_code, value.http_code = curl(url, tmp_file, ua, access_mode, value.hwid)
if return_code ~= 0 then
fail_list[#fail_list + 1] = value
luci.sys.call("rm -f " .. tmp_file)
end
end
if fs.access(tmp_file) then
if luci.sys.call("[ -f " .. tmp_file .. " ] && sed -i -e '/^[ \t]*$/d' -e '/^[ \t]*\r$/d' " .. tmp_file) == 0 then
local f = io.open(tmp_file, "r")
local stdout = f:read("*all")
f:close()
local raw_data = api.trim(stdout)
local old_md5 = value.md5 or ""
local new_md5 = luci.sys.exec("md5sum " .. tmp_file .. " 2>/dev/null | awk '{print $1}'"):gsub("\n", "")
if not manual_sub and old_md5 == new_md5 then
log(1, i18n.translatef("Subscription: [%s] No changes, no update required.", remark))
else
parse_link(raw_data, "2", remark, value)
uci:set(appname, cfgid, "md5", new_md5)
end
else
fail_list[#fail_list + 1] = value
end
if url_is_local then
value.http_code = 0
else
luci.sys.call("rm -f " .. tmp_file)
end
end
end
if #fail_list > 0 then
for index, value in ipairs(fail_list) do
log(1, i18n.translatef("[%s] Subscription failed. This could be due to an invalid subscription address or a network issue. Please diagnose the problem! [%s]", value.remark, tostring(value.http_code)))
end
end
update_node(0)
end
end
local function check_instance(action)
local sub_lock = "/var/lock/" .. appname .. "_subscribe.lock"
local rule_lock = "/var/lock/" .. appname .. "_rule_update.lock"
if action == "start" then
math.randomseed(os.time() + math.floor(os.clock() * 1000))
api.nixio.nanosleep(0, math.random(100, 1000) * 1000000)
if fs.access(sub_lock) then
log(0, i18n.translatef("[Subscription] instance is running; please try again later.") .. "\n")
os.exit(0)
else
luci.sys.call("touch " .. sub_lock)
uci:revert(appname)
end
elseif action == "end" then
luci.sys.call("rm -f " .. sub_lock)
return
end
if fs.access(rule_lock) then
log(0, i18n.translatef("[Rule Update] instance is running; [Subscription] queue and wait.") .. "\n")
end
while fs.access(rule_lock) do
api.nixio.nanosleep(2, 0)
end
end
if arg[1] then
check_instance("start")
if arg[1] == "start" then
log(0, i18n.translatef("Start subscribing..."))
xpcall(execute, function(e)
log(1, e)
log(1, debug.traceback())
log(1, i18n.translatef("Error, restoring service."))
end)
log(0, i18n.translatef("Subscription complete...") .. "\n")
elseif arg[1] == "add" then
local f = assert(io.open("/tmp/links.conf", 'r'))
local raw = f:read('*all')
f:close()
parse_link(raw, "1", arg[2])
update_node(1)
luci.sys.call("rm -f /tmp/links.conf")
elseif arg[1] == "truncate" then
truncate_nodes(arg[2])
end
check_instance("end")
end