Files
openwrt_packages/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua
2025-12-03 00:14:03 +08:00

2050 lines
69 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 uci = api.uci
local fs = api.fs
local log = api.log
local i18n = api.i18n
uci:revert(appname)
local has_ss = api.is_finded("ss-redir")
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 allowInsecure_default = true
-- Nodes should be retrieved using the core type (if not set on the node subscription page, the default type will be used automatically).
local function get_core(field, candidates)
local v = uci:get(appname, "@global_subscribe[0]", field)
if not v or v == "" then
for _, c in ipairs(candidates) do
if c[1] then return c[2] end
end
end
return v
end
local ss_type_default = get_core("ss_type", {{has_ss,"shadowsocks-libev"},{has_ss_rust,"shadowsocks-rust"},{has_singbox,"sing-box"},{has_xray,"xray"}})
local trojan_type_default = get_core("trojan_type", {{has_singbox,"sing-box"},{has_xray,"xray"}})
local vmess_type_default = get_core("vmess_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
local vless_type_default = get_core("vless_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
local hysteria2_type_default = get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{has_singbox,"sing-box"}})
local domain_strategy_default = uci:get(appname, "@global_subscribe[0]", "domain_strategy") or ""
local domain_strategy_node = ""
local preproxy_node_group, to_node_group, chain_node_type = "", "", ""
-- Determine whether to filter node keywords
local filter_keyword_mode_default = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0"
local filter_keyword_discard_list_default = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {}
local filter_keyword_keep_list_default = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {}
local function is_filter_keyword(value)
if filter_keyword_mode_default == "1" then
for k,v in ipairs(filter_keyword_discard_list_default) do
if value:find(v, 1, true) then
return true
end
end
elseif filter_keyword_mode_default == "2" then
local result = true
for k,v in ipairs(filter_keyword_keep_list_default) do
if value:find(v, 1, true) then
result = false
end
end
return result
elseif filter_keyword_mode_default == "3" then
local result = false
for k,v in ipairs(filter_keyword_discard_list_default) do
if value:find(v, 1, true) then
result = true
end
end
for k,v in ipairs(filter_keyword_keep_list_default) do
if value:find(v, 1, true) then
result = false
end
end
return result
elseif filter_keyword_mode_default == "4" then
local result = true
for k,v in ipairs(filter_keyword_keep_list_default) do
if value:find(v, 1, true) then
result = false
end
end
for k,v in ipairs(filter_keyword_discard_list_default) 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)
end
end)
table.insert(rules, {
[".name"] = "default_node",
remarks = i18n.translatef("Default")
})
table.insert(rules, {
[".name"] = "main_node",
remarks = i18n.translatef("Default Preproxy")
})
for k, e in pairs(rules) do
local _node_id = node[e[".name"]] or nil
if _node_id and api.parseURL(_node_id) then
else
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 = node and uci:get_all(appname, node) or nil,
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 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 = node and uci:get_all(appname, node) or nil,
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 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 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
local function UrlEncode(szText)
return szText:gsub("([^%w%-_%.%~])", function(c)
return string.format("%%%02X", string.byte(c))
end)
end
local function UrlDecode(szText)
return szText and szText:gsub("+", " "):gsub("%%(%x%x)", function(h)
return string.char(tonumber(h, 16))
end) or nil
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 expired_date = value:match("套餐到期:(.+)")
local rem_traffic = value:match("剩余流量:(.+)")
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(result)
if ss_type_default == "shadowsocks-libev" and has_ss then
result.type = "SS"
elseif ss_type_default == "shadowsocks-rust" and has_ss_rust then
result.type = 'SS-Rust'
elseif ss_type_default == "xray" and has_xray then
result.type = 'Xray'
result.protocol = 'shadowsocks'
result.transport = 'raw'
elseif ss_type_default == "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
-- Processing data
local function processData(szType, content, add_mode, group)
--log(2, content, add_mode, group)
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, "/%?")
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 t = split(v, '=')
params[t[1]] = t[2]
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 vmess_type_default == "sing-box" and has_singbox then
result.type = 'sing-box'
elseif vmess_type_default == "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.alter_id = info.aid
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
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_mtu = 1350
result.mkcp_tti = 50
result.mkcp_uplinkCapacity = 5
result.mkcp_downlinkCapacity = 20
result.mkcp_readBufferSize = 2
result.mkcp_writeBufferSize = 2
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
if not info.security then result.security = "auto" end
if info.tls == "tls" or info.tls == "1" then
result.tls = "1"
result.tls_serverName = (info.sni and info.sni ~= "") and info.sni or info.host
info.allowinsecure = info.allowinsecure or info.insecure
if info.allowinsecure and (info.allowinsecure == "1" or info.allowinsecure == "0") then
result.tls_allowInsecure = info.allowinsecure
else
result.tls_allowInsecure = allowInsecure_default and "1" or "0"
end
else
result.tls = "0"
end
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(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 t = split(v, '=')
if #t >= 2 then params[t[1]] = UrlDecode(t[2]) 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
if has_xray and (result.type ~= 'Xray' and result.type ~= 'sing-box' and params.type) then
result.type = 'Xray'
result.protocol = 'shadowsocks'
elseif has_singbox and (result.type ~= 'Xray' and result.type ~= 'sing-box' and params.type) 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("Xray unsupport %s plugin.", 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
else
result.plugin_enabled = "1"
end
end
if result.type == "SS" then
local aead2022_methods = { "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305" }
local aead2022 = false
for k, v in ipairs(aead2022_methods) do
if method:lower() == v:lower() then
aead2022 = true
end
end
if aead2022 then
-- shadowsocks-libev does not support 2022 encryption.
result.error_msg = i18n.translatef("shadowsocks-libev unsupport 2022 encryption.")
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" and result.type ~= "SS" 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_mtu = 1350
result.mkcp_tti = 50
result.mkcp_uplinkCapacity = 5
result.mkcp_downlinkCapacity = 20
result.mkcp_readBufferSize = 2
result.mkcp_writeBufferSize = 2
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
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
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
params.allowinsecure = params.allowinsecure or params.insecure
if params.allowinsecure and (params.allowinsecure == "1" or params.allowinsecure == "0") then
result.tls_allowInsecure = params.allowinsecure
else
result.tls_allowInsecure = allowInsecure_default and "1" or "0"
end
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 = ss_type_default .. " " .. 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 trojan_type_default == "sing-box" and has_singbox then
result.type = 'sing-box'
result.protocol = 'trojan'
elseif trojan_type_default == "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 t = split(v, '=')
if #t > 1 then
params[string.lower(t[1])] = UrlDecode(t[2])
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
local peer, sni = nil, ""
if params.peer then peer = params.peer end
sni = params.sni and params.sni or ""
if params.ws and params.ws == "1" then
result.trojan_transport = "ws"
if params.wshost then result.ws_host = params.wshost end
if params.wspath then result.ws_path = params.wspath end
if sni == "" and params.wshost then sni = params.wshost end
end
result.port = port
result.tls = '1'
result.tls_serverName = peer and peer or sni
params.allowinsecure = params.allowinsecure or params.insecure
if params.allowinsecure then
if params.allowinsecure == "1" or params.allowinsecure == "0" then
result.tls_allowInsecure = params.allowinsecure
else
result.tls_allowInsecure = string.lower(params.allowinsecure) == "true" and "1" or "0"
end
else
result.tls_allowInsecure = allowInsecure_default 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_mtu = 1350
result.mkcp_tti = 50
result.mkcp_uplinkCapacity = 5
result.mkcp_downlinkCapacity = 20
result.mkcp_readBufferSize = 2
result.mkcp_writeBufferSize = 2
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
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.alpn = params.alpn
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(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 vless_type_default == "sing-box" and has_singbox then
result.type = 'sing-box'
elseif vless_type_default == "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 t = split(v, '=')
params[t[1]] = UrlDecode(t[2])
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_mtu = 1350
result.mkcp_tti = 50
result.mkcp_uplinkCapacity = 5
result.mkcp_downlinkCapacity = 20
result.mkcp_readBufferSize = 2
result.mkcp_writeBufferSize = 2
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
else
result.download_address = 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 and params.flow:gsub("-udp443", "") or nil
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
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
result.port = port
params.allowinsecure = params.allowinsecure or params.insecure
if params.allowinsecure and (params.allowinsecure == "1" or params.allowinsecure == "0") then
result.tls_allowInsecure = params.allowinsecure
else
result.tls_allowInsecure = allowInsecure_default and "1" or "0"
end
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 dat = split(content:gsub("/%?", "?"), '%?')
local host_port = dat[1]
local params = {}
for _, v in pairs(split(dat[2], '&')) do
local t = split(v, '=')
if #t > 0 then
params[t[1]] = t[2]
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
params.allowinsecure = params.allowinsecure or params.insecure
if params.allowinsecure and (params.allowinsecure == "1" or params.allowinsecure == "0") then
result.tls_allowInsecure = params.allowinsecure
else
result.tls_allowInsecure = allowInsecure_default and "1" or "0"
end
result.hysteria_alpn = params.alpn
result.hysteria_up_mbps = params.upmbps
result.hysteria_down_mbps = params.downmbps
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 t = split(v, '=')
if #t > 1 then
params[string.lower(t[1])] = UrlDecode(t[2])
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
params.allowinsecure = params.allowinsecure or params.insecure
if params.allowinsecure and (params.allowinsecure == "1" or params.allowinsecure == "0") then
result.tls_allowInsecure = params.allowinsecure
else
result.tls_allowInsecure = allowInsecure_default and "1" or "0"
end
result.hysteria2_tls_pinSHA256 = params.pinSHA256
result.hysteria2_hop = params.mport
if hysteria2_type_default == "sing-box" and has_singbox then
result.type = 'sing-box'
result.protocol = "hysteria2"
if params["obfs-password"] or params["obfs_password"] then
result.hysteria2_obfs_type = "salamander"
result.hysteria2_obfs_password = params["obfs-password"] or params["obfs_password"]
end
elseif has_hysteria2 then
result.type = "Hysteria2"
if params["obfs-password"] or params["obfs_password"] then
result.hysteria2_obfs = params["obfs-password"] or params["obfs_password"]
end
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("@") then
local contents = split(content, "@")
if contents[1]:find(":") then
local userinfo = split(contents[1], ":")
result.uuid = UrlDecode(userinfo[1])
result.password = UrlDecode(userinfo[2])
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 t = split(v, '=')
if #t > 1 then
params[string.lower(t[1])] = UrlDecode(t[2])
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.tuic_alpn = params.alpn or "default"
result.tuic_congestion_control = params.congestion_control or "cubic"
result.tuic_udp_relay_mode = params.udp_relay_mode or "native"
params.allowinsecure = params.allowinsecure or params.insecure
if params.allowinsecure then
if params.allowinsecure == "1" or params.allowinsecure == "0" then
result.tls_allowInsecure = params.allowinsecure
else
result.tls_allowInsecure = string.lower(params.allowinsecure) == "true" and "1" or "0"
end
else
result.tls_allowInsecure = allowInsecure_default and "1" or "0"
end
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 t = split(v, '=')
params[t[1]] = UrlDecode(t[2])
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 == "") and params.sni and params.sni ~= "" 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
end
result.port = port
params.allowinsecure = params.allowinsecure or params.insecure
if params.allowinsecure and (params.allowinsecure == "1" or params.allowinsecure == "0") then
result.tls_allowInsecure = params.allowinsecure
else
result.tls_allowInsecure = allowInsecure_default and "1" or "0"
end
local singbox_version = api.get_app_version("sing-box")
local version_ge_1_12 = api.compare_versions(singbox_version:match("[^v]+"), ">=", "1.12.0")
if not has_singbox or not version_ge_1_12 then
log(2, i18n.translatef("Skip the %s node, as %s type nodes require Sing-Box version 1.12 or higher.", result.remarks, szType))
return nil
end
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)
local curl_args = {
"-skL", "-w %{http_code}", "--retry 3", "--connect-timeout 3"
}
if ua and ua ~= "" and ua ~= "curl" then
curl_args[#curl_args + 1] = '--user-agent "' .. ua .. '"'
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 tonumber(result)
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
-- 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
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) 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
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) 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
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 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
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 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
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 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"]
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 Domain Strategy
if kkk == "type" and vvv == "sing-box" then
uci:set(appname, cfgid, "domain_strategy", domain_strategy_node)
end
-- Subscription Group Chain Agent
if chain_node_type ~= "" and kkk == "type" and vvv == chain_node_type 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)
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
api.uci_save(uci, appname, true)
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
api.uci_save(uci, appname, true)
end
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, cfgid)
if raw and #raw > 0 then
local nodes, szType
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
-- 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
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)
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)
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)
end
end
else
log(2, i18n.translatef("Skip unknown types:") .. " " .. szType)
end
-- log(2, result)
if result then
if result.error_msg then
log(2, i18n.translatef("Discard node: %s, Reason:", result.remarks) .. " " .. result.error_msg)
elseif not result.type then
log(2, i18n.translatef("Discard node: %s, Reason:", result.remarks) .. " " .. i18n.translatef("No usable binary was found."))
elseif (add_mode == "2" and is_filter_keyword(result.remarks)) or not result.address or result.remarks == "NULL" or result.address == "127.0.0.1" or
(not datatypes.hostname(result.address) and not (api.is_ip(result.address))) then
log(2, i18n.translatef("Discard filter nodes: %s type node %s", result.type, result.remarks))
else
tinsert(node_list, result)
end
if add_mode == "2" then
get_subscribe_info(cfgid, result.remarks)
end
end
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
}
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
local url = value.url
if value.allowInsecure and value.allowInsecure ~= "1" then
allowInsecure_default = nil
end
local filter_keyword_mode = value.filter_keyword_mode or "5"
if filter_keyword_mode == "0" then
filter_keyword_mode_default = "0"
elseif filter_keyword_mode == "1" then
filter_keyword_mode_default = "1"
filter_keyword_discard_list_default = value.filter_discard_list or {}
elseif filter_keyword_mode == "2" then
filter_keyword_mode_default = "2"
filter_keyword_keep_list_default = value.filter_keep_list or {}
elseif filter_keyword_mode == "3" then
filter_keyword_mode_default = "3"
filter_keyword_keep_list_default = value.filter_keep_list or {}
filter_keyword_discard_list_default = value.filter_discard_list or {}
elseif filter_keyword_mode == "4" then
filter_keyword_mode_default = "4"
filter_keyword_keep_list_default = value.filter_keep_list or {}
filter_keyword_discard_list_default = value.filter_discard_list or {}
end
local ss_type = value.ss_type or "global"
if ss_type ~= "global" then
ss_type_default = ss_type
end
local trojan_type = value.trojan_type or "global"
if trojan_type ~= "global" then
trojan_type_default = trojan_type
end
local vmess_type = value.vmess_type or "global"
if vmess_type ~= "global" then
vmess_type_default = vmess_type
end
local vless_type = value.vless_type or "global"
if vless_type ~= "global" then
vless_type_default = vless_type
end
local hysteria2_type = value.hysteria2_type or "global"
if hysteria2_type ~= "global" then
hysteria2_type_default = hysteria2_type
end
local domain_strategy = value.domain_strategy or "global"
if domain_strategy ~= "global" then
domain_strategy_node = domain_strategy
else
domain_strategy_node = domain_strategy_default
end
-- 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
preproxy_node_group = (value.chain_proxy == "1") and valid_chain_node(value.preproxy_node) or ""
to_node_group = (value.chain_proxy == "2") and valid_chain_node(value.to_node) or ""
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 .. ']'))
local tmp_file = "/tmp/" .. cfgid
value.http_code = curl(url, tmp_file, ua, access_mode)
if value.http_code ~= 200 then
fail_list[#fail_list + 1] = value
else
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, cfgid)
uci:set(appname, cfgid, "md5", new_md5)
end
else
fail_list[#fail_list + 1] = value
end
end
allowInsecure_default = true
luci.sys.call("rm -f " .. tmp_file)
filter_keyword_mode_default = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0"
filter_keyword_discard_list_default = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {}
filter_keyword_keep_list_default = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {}
ss_type_default = uci:get(appname, "@global_subscribe[0]", "ss_type") or "shadowsocks-libev"
trojan_type_default = uci:get(appname, "@global_subscribe[0]", "trojan_type") or "sing-box"
vmess_type_default = uci:get(appname, "@global_subscribe[0]", "vmess_type") or "xray"
vless_type_default = uci:get(appname, "@global_subscribe[0]", "vless_type") or "xray"
hysteria2_type_default = uci:get(appname, "@global_subscribe[0]", "hysteria2_type") or "hysteria2"
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
if arg[1] then
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
end