2050 lines
69 KiB
Lua
Executable File
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("&", "&"):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
|