mirror of
https://github.com/xiaorouji/openwrt-passwall2.git
synced 2025-05-02 03:32:41 +08:00
1114 lines
32 KiB
Lua
Executable File
1114 lines
32 KiB
Lua
Executable File
#!/usr/bin/lua
|
||
|
||
------------------------------------------------
|
||
-- @author William Chan <root@williamchan.me>
|
||
------------------------------------------------
|
||
require 'nixio'
|
||
require 'luci.model.uci'
|
||
require 'luci.util'
|
||
require 'luci.jsonc'
|
||
require 'luci.sys'
|
||
local appname = 'passwall2'
|
||
local api = require ("luci.model.cbi." .. appname .. ".api.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 jsonParse, jsonStringify = luci.jsonc.parse, luci.jsonc.stringify
|
||
local base64Decode = api.base64Decode
|
||
local uci = luci.model.uci.cursor()
|
||
uci:revert(appname)
|
||
|
||
local has_ss = api.is_finded("ss-redir")
|
||
local has_ss_rust = api.is_finded("sslocal")
|
||
local has_v2ray = api.is_finded("v2ray")
|
||
local has_xray = api.is_finded("xray")
|
||
local allowInsecure_default = true
|
||
local ss_aead_type_default = uci:get(appname, "@global_subscribe[0]", "ss_aead_type") or "shadowsocks-libev"
|
||
-- 判断是否过滤节点关键字
|
||
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 debug = false
|
||
|
||
local log = function(...)
|
||
local result = os.date("%Y-%m-%d %H:%M:%S: ") .. table.concat({...}, " ")
|
||
if debug == true then
|
||
print(result)
|
||
else
|
||
local f, err = io.open("/tmp/log/" .. appname .. ".log", "a")
|
||
if f and err == nil then
|
||
f:write(result .. "\n")
|
||
f:close()
|
||
end
|
||
end
|
||
end
|
||
|
||
-- 获取各项动态配置的当前服务器,可以用 get 和 set, get必须要获取到节点表
|
||
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 = "节点",
|
||
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 node_id = t[option]
|
||
CONFIG[#CONFIG + 1] = {
|
||
log = true,
|
||
id = t[".name"],
|
||
remarks = "Socks节点列表[" .. 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
|
||
|
||
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 = "访问控制列表[" .. 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
|
||
|
||
local node_table = uci:get(appname, "@auto_switch[0]", "node")
|
||
if node_table then
|
||
local nodes = {}
|
||
local new_nodes = {}
|
||
for k,node_id in ipairs(node_table) do
|
||
if node_id then
|
||
local currentNode = uci:get_all(appname, node_id) or nil
|
||
if currentNode then
|
||
if currentNode.protocol and (currentNode.protocol == "_balancing" or currentNode.protocol == "_shunt") then
|
||
currentNode = nil
|
||
end
|
||
nodes[#nodes + 1] = {
|
||
log = true,
|
||
remarks = "备用节点的列表[" .. k .. "]",
|
||
currentNode = currentNode,
|
||
set = function(o, server)
|
||
for kk, vv in pairs(CONFIG) do
|
||
if (vv.remarks == "备用节点的列表") then
|
||
table.insert(vv.new_nodes, server)
|
||
end
|
||
end
|
||
end
|
||
}
|
||
end
|
||
end
|
||
end
|
||
CONFIG[#CONFIG + 1] = {
|
||
remarks = "备用节点的列表",
|
||
nodes = nodes,
|
||
new_nodes = new_nodes,
|
||
set = function(o)
|
||
for kk, vv in pairs(CONFIG) do
|
||
if (vv.remarks == "备用节点的列表") then
|
||
--log("刷新自动切换的备用节点的列表")
|
||
uci:set_list(appname, "@auto_switch[0]", "node", vv.new_nodes)
|
||
end
|
||
end
|
||
end
|
||
}
|
||
end
|
||
|
||
uci:foreach(appname, "nodes", function(node)
|
||
if node.protocol and node.protocol == '_shunt' then
|
||
local node_id = node[".name"]
|
||
|
||
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 = "默认"
|
||
})
|
||
table.insert(rules, {
|
||
[".name"] = "main_node",
|
||
remarks = "默认前置"
|
||
})
|
||
|
||
for k, e in pairs(rules) do
|
||
local _node_id = node[e[".name"]] or nil
|
||
CONFIG[#CONFIG + 1] = {
|
||
log = false,
|
||
currentNode = _node_id and uci:get_all(appname, _node_id) or nil,
|
||
remarks = "分流" .. e.remarks .. "节点",
|
||
set = function(o, server)
|
||
uci:set(appname, node_id, e[".name"], server)
|
||
o.newNodeId = server
|
||
end
|
||
}
|
||
end
|
||
elseif node.protocol and node.protocol == '_balancing' then
|
||
local node_id = node[".name"]
|
||
local nodes = {}
|
||
local new_nodes = {}
|
||
if node.balancing_node then
|
||
for k, node in pairs(node.balancing_node) do
|
||
nodes[#nodes + 1] = {
|
||
log = false,
|
||
node = node,
|
||
currentNode = node and uci:get_all(appname, node) or nil,
|
||
remarks = node,
|
||
set = function(o, server)
|
||
for kk, vv in pairs(CONFIG) do
|
||
if (vv.remarks == "负载均衡节点列表" .. node_id) then
|
||
table.insert(vv.new_nodes, server)
|
||
end
|
||
end
|
||
end
|
||
}
|
||
end
|
||
end
|
||
CONFIG[#CONFIG + 1] = {
|
||
remarks = "负载均衡节点列表" .. node_id,
|
||
nodes = nodes,
|
||
new_nodes = new_nodes,
|
||
set = function(o)
|
||
for kk, vv in pairs(CONFIG) do
|
||
if (vv.remarks == "负载均衡节点列表" .. node_id) then
|
||
--log("刷新负载均衡节点列表")
|
||
uci:foreach(appname, "nodes", function(node2)
|
||
if node2[".name"] == node[".name"] then
|
||
local index = node2[".index"]
|
||
uci:set_list(appname, "@nodes[" .. index .. "]", "balancing_node", vv.new_nodes)
|
||
end
|
||
end)
|
||
end
|
||
end
|
||
end
|
||
}
|
||
end
|
||
end)
|
||
|
||
for k, v in pairs(CONFIG) do
|
||
if v.nodes and type(v.nodes) == "table" then
|
||
for kk, vv in pairs(v.nodes) do
|
||
if vv.currentNode == nil then
|
||
CONFIG[k].nodes[kk] = nil
|
||
end
|
||
end
|
||
else
|
||
if v.currentNode == nil then
|
||
CONFIG[k] = nil
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
-- 分割字符串
|
||
local function split(full, sep)
|
||
if full then
|
||
full = full:gsub("%z", "") -- 这里不是很清楚 有时候结尾带个\0
|
||
local off, result = 1, {}
|
||
while true do
|
||
local nStart, nEnd = full:find(sep, off)
|
||
if not nEnd then
|
||
local res = ssub(full, off, slen(full))
|
||
if #res > 0 then -- 过滤掉 \0
|
||
tinsert(result, res)
|
||
end
|
||
break
|
||
else
|
||
tinsert(result, ssub(full, off, nStart - 1))
|
||
off = nEnd + 1
|
||
end
|
||
end
|
||
return result
|
||
end
|
||
return {}
|
||
end
|
||
-- urlencode
|
||
-- local function get_urlencode(c) return sformat("%%%02X", sbyte(c)) end
|
||
|
||
-- local function urlEncode(szText)
|
||
-- local str = szText:gsub("([^0-9a-zA-Z ])", get_urlencode)
|
||
-- str = str:gsub(" ", "+")
|
||
-- return str
|
||
-- end
|
||
|
||
local function get_urldecode(h) return schar(tonumber(h, 16)) end
|
||
local function UrlDecode(szText)
|
||
return (szText and szText:gsub("+", " "):gsub("%%(%x%x)", get_urldecode)) or nil
|
||
end
|
||
|
||
-- trim
|
||
local function trim(text)
|
||
if not text or text == "" then return "" end
|
||
return (sgsub(text, "^%s*(.-)%s*$", "%1"))
|
||
end
|
||
|
||
-- 处理数据
|
||
local function processData(szType, content, add_mode, add_from)
|
||
--log(content, add_mode, add_from)
|
||
local result = {
|
||
timeout = 60,
|
||
add_mode = add_mode, --0为手动配置,1为导入,2为订阅
|
||
add_from = add_from
|
||
}
|
||
if szType == 'ssr' then
|
||
local dat = split(content, "/%?")
|
||
local hostInfo = split(dat[1], ':')
|
||
result.type = "SSR"
|
||
result.address = ""
|
||
for i=1,#hostInfo-5,1 do
|
||
result.address = result.address .. hostInfo[i] .. ":"
|
||
end
|
||
result.address = string.sub(result.address, 0, #result.address-1)
|
||
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 group = base64Decode(params.group)
|
||
if group then result.group = group end
|
||
result.remarks = base64Decode(params.remarks)
|
||
elseif szType == 'vmess' then
|
||
local info = jsonParse(content)
|
||
result.type = 'V2ray'
|
||
if has_xray then
|
||
result.type = 'Xray'
|
||
end
|
||
result.address = info.add
|
||
result.port = info.port
|
||
result.protocol = 'vmess'
|
||
result.uuid = info.id
|
||
result.remarks = info.ps
|
||
-- result.mux = 1
|
||
-- result.mux_concurrency = 8
|
||
info.net = string.lower(info.net)
|
||
if info.net == 'ws' then
|
||
result.ws_host = info.host
|
||
result.ws_path = info.path
|
||
end
|
||
if info.net == 'h2' then
|
||
result.h2_host = info.host
|
||
result.h2_path = info.path
|
||
end
|
||
if 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
|
||
result.tcp_guise_http_path = info.path
|
||
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
|
||
result.transport = info.net
|
||
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
|
||
result.tls_allowInsecure = allowInsecure_default and "1" or "0"
|
||
else
|
||
result.tls = "0"
|
||
end
|
||
elseif szType == "ss" then
|
||
local idx_sp = 0
|
||
local alias = ""
|
||
if content:find("#") then
|
||
idx_sp = content:find("#")
|
||
alias = content:sub(idx_sp + 1, -1)
|
||
end
|
||
result.remarks = UrlDecode(alias)
|
||
local info = content:sub(1, idx_sp - 1)
|
||
local hostInfo = split(base64Decode(info), "@")
|
||
local hostInfoLen = #hostInfo
|
||
local host = nil
|
||
local userinfo = nil
|
||
if hostInfoLen > 2 then
|
||
host = split(hostInfo[hostInfoLen], ":")
|
||
userinfo = {}
|
||
for i = 1, hostInfoLen - 1 do
|
||
tinsert(userinfo, hostInfo[i])
|
||
end
|
||
userinfo = table.concat(userinfo, '@')
|
||
else
|
||
host = split(hostInfo[2], ":")
|
||
userinfo = base64Decode(hostInfo[1])
|
||
end
|
||
local method = userinfo:sub(1, userinfo:find(":") - 1)
|
||
local password = userinfo:sub(userinfo:find(":") + 1, #userinfo)
|
||
result.type = "SS"
|
||
result.address = host[1]
|
||
if host[2] and host[2]:find("/%?") then
|
||
local query = split(host[2], "/%?")
|
||
result.port = query[1]
|
||
local params = {}
|
||
for _, v in pairs(split(query[2], '&')) do
|
||
local t = split(v, '=')
|
||
params[t[1]] = t[2]
|
||
end
|
||
if params.plugin then
|
||
local plugin_info = UrlDecode(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
|
||
else
|
||
result.port = host[2]
|
||
end
|
||
result.method = method
|
||
result.password = password
|
||
|
||
local aead = false
|
||
for k, v in ipairs({"aes-128-gcm", "aes-256-gcm", "chacha20-poly1305", "chacha20-ietf-poly1305"}) do
|
||
if method:lower() == v:lower() then
|
||
aead = true
|
||
end
|
||
end
|
||
if aead then
|
||
if ss_aead_type_default == "shadowsocks-libev" and has_ss then
|
||
result.type = "SS"
|
||
elseif ss_aead_type_default == "shadowsocks-rust" and has_ss_rust then
|
||
result.type = 'SS-Rust'
|
||
if method:lower() == "chacha20-poly1305" then
|
||
result.method = "chacha20-ietf-poly1305"
|
||
end
|
||
elseif ss_aead_type_default == "v2ray" and has_v2ray and not result.plugin then
|
||
result.type = 'V2ray'
|
||
result.protocol = 'shadowsocks'
|
||
result.transport = 'tcp'
|
||
if method:lower() == "chacha20-ietf-poly1305" then
|
||
result.method = "chacha20-poly1305"
|
||
end
|
||
elseif ss_aead_type_default == "xray" and has_xray and not result.plugin then
|
||
result.type = 'Xray'
|
||
result.protocol = 'shadowsocks'
|
||
result.transport = 'tcp'
|
||
if method:lower() == "chacha20-ietf-poly1305" then
|
||
result.method = "chacha20-poly1305"
|
||
end
|
||
end
|
||
end
|
||
elseif szType == "trojan" 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)
|
||
result.type = 'V2ray'
|
||
if has_xray then
|
||
result.type = 'Xray'
|
||
end
|
||
result.protocol = 'trojan'
|
||
if content:find("@") then
|
||
local Info = split(content, "@")
|
||
result.password = UrlDecode(Info[1])
|
||
local port = "443"
|
||
Info[2] = (Info[2] or ""):gsub("/%?", "?")
|
||
local hostInfo = nil
|
||
if Info[2]:find(":") then
|
||
hostInfo = split(Info[2], ":")
|
||
result.address = hostInfo[1]
|
||
local idx_port = 2
|
||
if hostInfo[2]:find("?") then
|
||
hostInfo = split(hostInfo[2], "?")
|
||
idx_port = 1
|
||
end
|
||
if hostInfo[idx_port] ~= "" then port = hostInfo[idx_port] end
|
||
else
|
||
if Info[2]:find("?") then
|
||
hostInfo = split(Info[2], "?")
|
||
end
|
||
result.address = hostInfo and hostInfo[1] or Info[2]
|
||
end
|
||
local peer, sni = nil, ""
|
||
local allowInsecure = allowInsecure_default
|
||
local query = split(Info[2], "?")
|
||
local params = {}
|
||
for _, v in pairs(split(query[2], '&')) do
|
||
local t = split(v, '=')
|
||
params[string.lower(t[1])] = UrlDecode(t[2])
|
||
end
|
||
if params.allowinsecure then
|
||
allowInsecure = params.allowinsecure
|
||
end
|
||
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
|
||
result.tls_allowInsecure = allowInsecure and "1" or "0"
|
||
end
|
||
elseif szType == "ssd" then
|
||
result.type = "SS"
|
||
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
|
||
result.type = 'V2ray'
|
||
if has_xray then
|
||
result.type = 'Xray'
|
||
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 hostInfo = nil
|
||
if Info[2]:find(":") then
|
||
hostInfo = split(Info[2], ":")
|
||
result.address = hostInfo[1]
|
||
local idx_port = 2
|
||
if hostInfo[2]:find("?") then
|
||
hostInfo = split(hostInfo[2], "?")
|
||
idx_port = 1
|
||
end
|
||
if hostInfo[idx_port] ~= "" then port = hostInfo[idx_port] end
|
||
else
|
||
if Info[2]:find("?") then
|
||
hostInfo = split(Info[2], "?")
|
||
end
|
||
result.address = hostInfo and hostInfo[1] or Info[2]
|
||
end
|
||
|
||
local query = split(Info[2], "?")
|
||
local params = {}
|
||
for _, v in pairs(split(query[2], '&')) do
|
||
local t = split(v, '=')
|
||
params[t[1]] = UrlDecode(t[2])
|
||
end
|
||
|
||
params.type = string.lower(params.type)
|
||
if params.type == 'ws' then
|
||
result.ws_host = params.host
|
||
result.ws_path = params.path
|
||
end
|
||
if params.type == 'h2' then
|
||
result.h2_host = params.host
|
||
result.h2_path = params.path
|
||
end
|
||
if params.type == 'tcp' then
|
||
result.tcp_guise = params.headerType or "none"
|
||
result.tcp_guise_http_host = params.host
|
||
result.tcp_guise_http_path = params.path
|
||
end
|
||
if params.type == 'kcp' or params.type == 'mkcp' then
|
||
params.type = "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
|
||
end
|
||
result.transport = params.type
|
||
|
||
result.encryption = params.encryption or "none"
|
||
|
||
result.tls = "0"
|
||
if params.security == "tls" then
|
||
result.tls = "1"
|
||
result.tlsflow = params.flow or nil
|
||
result.tls_serverName = (params.sni and params.sni ~= "") and params.sni or params.host
|
||
result.fingerprint = (params.fp and params.fp ~= "") and params.fp or nil
|
||
end
|
||
|
||
result.port = port
|
||
result.tls_allowInsecure = allowInsecure_default and "1" or "0"
|
||
end
|
||
elseif szType == 'hysteria' 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)
|
||
result.type = "Hysteria"
|
||
|
||
local dat = split(content, '%?')
|
||
local hostInfo = split(dat[1], ':')
|
||
result.address = hostInfo[1]
|
||
result.port = hostInfo[2]
|
||
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
|
||
result.protocol = params.protocol
|
||
result.hysteria_obfs = params.obfsParam
|
||
result.hysteria_auth_type = "string"
|
||
result.hysteria_auth_password = params.auth
|
||
result.tls_serverName = params.peer
|
||
if params.insecure and params.insecure == "1" then
|
||
result.tls_allowInsecure = "1"
|
||
end
|
||
result.hysteria_alpn = params.alpn
|
||
result.hysteria_up_mbps = params.upmbps
|
||
result.hysteria_down_mbps = params.downmbps
|
||
else
|
||
log('暂时不支持' .. 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
|
||
|
||
-- curl
|
||
local function curl(url, file, ua)
|
||
if not ua or ua == "" then
|
||
ua = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36"
|
||
end
|
||
local stdout = ""
|
||
local cmd = string.format('curl -skL --user-agent "%s" --retry 3 --connect-timeout 3 "%s"', ua, url)
|
||
if file then
|
||
cmd = cmd .. " -o " .. file
|
||
stdout = luci.sys.call(cmd .. " > /dev/null")
|
||
return stdout
|
||
else
|
||
stdout = luci.sys.exec(cmd)
|
||
return trim(stdout)
|
||
end
|
||
|
||
if not stdout or #stdout <= 0 then
|
||
if uci:get(appname, "@global_subscribe[0]", "subscribe_proxy") or "0" == "1" and uci:get(appname, "@global[0]", "enabled") or "0" == "1" then
|
||
log('通过代理订阅失败,尝试关闭代理订阅。')
|
||
luci.sys.call("/etc/init.d/" .. appname .. " stop > /dev/null")
|
||
stdout = luci.sys.exec(string.format('curl -skL --user-agent "%s" -k --retry 3 --connect-timeout 3 "%s"', ua, url))
|
||
end
|
||
end
|
||
return trim(stdout)
|
||
end
|
||
|
||
local function truncate_nodes(add_from)
|
||
for _, config in pairs(CONFIG) do
|
||
if config.nodes and type(config.nodes) == "table" then
|
||
for kk, vv in pairs(config.nodes) do
|
||
if vv.currentNode.add_mode == "2" then
|
||
else
|
||
vv.set(vv, vv.currentNode[".name"])
|
||
end
|
||
end
|
||
config.set(config)
|
||
else
|
||
if config.currentNode.add_mode == "2" then
|
||
if add_from then
|
||
if config.currentNode.add_from and config.currentNode.add_from == add_from then
|
||
config.set(config, "nil")
|
||
end
|
||
else
|
||
config.set(config, "nil")
|
||
end
|
||
if config.id then
|
||
uci:delete(appname, config.id)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
uci:foreach(appname, "nodes", function(node)
|
||
if node.add_mode == "2" then
|
||
if add_from then
|
||
if node.add_from and node.add_from == add_from then
|
||
uci:delete(appname, node['.name'])
|
||
end
|
||
else
|
||
uci:delete(appname, node['.name'])
|
||
end
|
||
end
|
||
end)
|
||
uci:commit(appname)
|
||
end
|
||
|
||
local function select_node(nodes, config)
|
||
local server
|
||
if config.currentNode then
|
||
-- 特别优先级 分流 + 备注
|
||
if config.currentNode.protocol and config.currentNode.protocol == '_shunt' then
|
||
for index, node in pairs(nodes) do
|
||
if node.remarks == config.currentNode.remarks then
|
||
log('更新【' .. config.remarks .. '】分流匹配节点:' .. node.remarks)
|
||
server = node[".name"]
|
||
break
|
||
end
|
||
end
|
||
end
|
||
-- 特别优先级 负载均衡 + 备注
|
||
if config.currentNode.protocol and config.currentNode.protocol == '_balancing' then
|
||
for index, node in pairs(nodes) do
|
||
if node.remarks == config.currentNode.remarks then
|
||
log('更新【' .. config.remarks .. '】负载均衡匹配节点:' .. node.remarks)
|
||
server = node[".name"]
|
||
break
|
||
end
|
||
end
|
||
end
|
||
-- 第一优先级 类型 + IP + 端口
|
||
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('更新【' .. config.remarks .. '】第一匹配节点:' .. node.remarks)
|
||
end
|
||
server = node[".name"]
|
||
break
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
-- 第二优先级 IP + 端口
|
||
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('更新【' .. config.remarks .. '】第二匹配节点:' .. node.remarks)
|
||
end
|
||
server = node[".name"]
|
||
break
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
-- 第三优先级 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('更新【' .. config.remarks .. '】第三匹配节点:' .. node.remarks)
|
||
end
|
||
server = node[".name"]
|
||
break
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
-- 第四优先级备注
|
||
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('更新【' .. config.remarks .. '】第四匹配节点:' .. node.remarks)
|
||
end
|
||
server = node[".name"]
|
||
break
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
-- 还不行 随便找一个
|
||
if not server then
|
||
server = uci:get_all(appname, '@' .. "nodes" .. '[0]')
|
||
if server then
|
||
if config.log == nil or config.log == true then
|
||
log('【' .. config.remarks .. '】' .. '无法找到最匹配的节点,当前已更换为:' .. server.remarks)
|
||
end
|
||
server = server[".name"]
|
||
end
|
||
end
|
||
if server then
|
||
config.set(config, server)
|
||
end
|
||
end
|
||
|
||
local function update_node(manual)
|
||
if next(nodeResult) == nil then
|
||
log("更新失败,没有可用的节点信息")
|
||
return
|
||
end
|
||
|
||
local group = ""
|
||
for _, v in ipairs(nodeResult) do
|
||
group = group .. v["remark"]
|
||
end
|
||
|
||
if manual == 0 and #group > 0 then
|
||
uci:foreach(appname, "nodes", function(node)
|
||
-- 如果是未发现新节点或手动导入的节点就不要删除了...
|
||
if (node.add_from and group:find(node.add_from, 1, true)) and node.add_mode == "2" 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_uuid())
|
||
for kkk, vvv in pairs(vv) do
|
||
uci:set(appname, cfgid, kkk, vvv)
|
||
end
|
||
end
|
||
end
|
||
uci:commit(appname)
|
||
|
||
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.nodes and type(config.nodes) == "table" then
|
||
for kk, vv in pairs(config.nodes) do
|
||
select_node(nodes, vv)
|
||
end
|
||
config.set(config)
|
||
else
|
||
select_node(nodes, config)
|
||
end
|
||
end
|
||
|
||
--[[
|
||
for k, v in pairs(CONFIG) do
|
||
if type(v.new_nodes) == "table" and #v.new_nodes > 0 then
|
||
local new_node_list = ""
|
||
for kk, vv in pairs(v.new_nodes) do
|
||
new_node_list = new_node_list .. vv .. " "
|
||
end
|
||
if new_node_list ~= "" then
|
||
print(v.remarks, new_node_list)
|
||
end
|
||
else
|
||
print(v.remarks, v.newNodeId)
|
||
end
|
||
end
|
||
]]--
|
||
|
||
uci:commit(appname)
|
||
end
|
||
luci.sys.call("/etc/init.d/" .. appname .. " restart > /dev/null 2>&1 &")
|
||
end
|
||
|
||
local function parse_link(raw, add_mode, add_from)
|
||
if raw and #raw > 0 then
|
||
local nodes, szType
|
||
local node_list = {}
|
||
-- SSD 似乎是这种格式 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里面包着 干脆直接这样
|
||
for _, server in ipairs(nodes.servers) do
|
||
tinsert(servers, setmetatable(server, { __index = extra }))
|
||
end
|
||
nodes = servers
|
||
else
|
||
-- ssd 外的格式
|
||
if add_mode == "1" then
|
||
nodes = split(raw:gsub(" ", "\n"), "\n")
|
||
else
|
||
nodes = split(base64Decode(raw):gsub(" ", "\n"), "\n")
|
||
end
|
||
end
|
||
|
||
for _, v in ipairs(nodes) do
|
||
if v then
|
||
local result
|
||
if szType == 'ssd' then
|
||
result = processData(szType, v, add_mode, add_from)
|
||
elseif not szType then
|
||
local node = trim(v)
|
||
local dat = split(node, "://")
|
||
if dat and dat[1] and dat[2] then
|
||
if dat[1] == 'ss' or dat[1] == 'trojan' then
|
||
result = processData(dat[1], dat[2], add_mode, add_from)
|
||
else
|
||
result = processData(dat[1], base64Decode(dat[2]), add_mode, add_from)
|
||
end
|
||
end
|
||
else
|
||
log('跳过未知类型: ' .. szType)
|
||
end
|
||
-- log(result)
|
||
if result then
|
||
if not result.type then
|
||
log('丢弃节点:' .. result.remarks .. ",找不到可使用二进制.")
|
||
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 (datatypes.ipmask4(result.address) or datatypes.ipmask6(result.address))) then
|
||
log('丢弃过滤节点: ' .. result.type .. ' 节点, ' .. result.remarks)
|
||
else
|
||
tinsert(node_list, result)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
if #node_list > 0 then
|
||
nodeResult[#nodeResult + 1] = {
|
||
remark = add_from,
|
||
list = node_list
|
||
}
|
||
end
|
||
log('成功解析【' .. add_from .. '】节点数量: ' .. #node_list)
|
||
else
|
||
if add_mode == "2" then
|
||
log('获取到的【' .. add_from .. '】订阅内容为空,可能是订阅地址失效,或是网络问题,请请检测。')
|
||
end
|
||
end
|
||
end
|
||
|
||
local execute = function()
|
||
do
|
||
local subscribe_list = {}
|
||
local retry = {}
|
||
if arg[2] 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
|
||
|
||
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_aead_type = value.ss_aead_type or "global"
|
||
if ss_aead_type ~= "global" then
|
||
ss_aead_type_default = ss_aead_type
|
||
end
|
||
local ua = value.user_agent
|
||
log('正在订阅:【' .. remark .. '】' .. url)
|
||
local raw = curl(url, "/tmp/" .. cfgid, ua)
|
||
if raw == 0 then
|
||
local f = io.open("/tmp/" .. cfgid, "r")
|
||
local stdout = f:read("*all")
|
||
f:close()
|
||
raw = trim(stdout)
|
||
os.remove("/tmp/" .. cfgid)
|
||
parse_link(raw, "2", remark)
|
||
else
|
||
retry[#retry + 1] = value
|
||
end
|
||
allowInsecure_default = true
|
||
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_aead_type_default = uci:get(appname, "@global_subscribe[0]", "ss_aead_type") or "shadowsocks-libev"
|
||
end
|
||
|
||
if #retry > 0 then
|
||
for index, value in ipairs(retry) do
|
||
if (uci:get(appname, "@global_subscribe[0]", "subscribe_proxy") or "0") == "1" and (uci:get(appname, "@global[0]", "enabled") or "0") == "1" then
|
||
log(value.remark .. '订阅失败,请尝试关闭代理后再订阅。')
|
||
else
|
||
log(value.remark .. '订阅失败,可能是订阅地址失效,或是网络问题,请诊断!')
|
||
end
|
||
end
|
||
end
|
||
update_node(0)
|
||
end
|
||
end
|
||
|
||
if arg[1] then
|
||
if arg[1] == "start" then
|
||
log('开始订阅...')
|
||
xpcall(execute, function(e)
|
||
log(e)
|
||
log(debug.traceback())
|
||
log('发生错误, 正在恢复服务')
|
||
end)
|
||
log('订阅完毕...')
|
||
elseif arg[1] == "add" then
|
||
local f = assert(io.open("/tmp/links.conf", 'r'))
|
||
local content = f:read('*all')
|
||
f:close()
|
||
local nodes = split(content:gsub(" ", "\n"), "\n")
|
||
for _, raw in ipairs(nodes) do
|
||
parse_link(raw, "1", "导入")
|
||
end
|
||
update_node(1)
|
||
luci.sys.call("rm -f /tmp/links.conf")
|
||
elseif arg[1] == "truncate" then
|
||
truncate_nodes(arg[2])
|
||
end
|
||
end
|