mirror of
https://github.com/coolsnowwolf/lede.git
synced 2025-04-16 04:13:31 +00:00

* luci-app-ssr-plus:Code optimization and Fix Bug * luci-app-ssr-plus:Adjust reference data location
498 lines
14 KiB
Lua
498 lines
14 KiB
Lua
#!/usr/bin/lua
|
||
------------------------------------------------
|
||
-- This file is part of the luci-app-ssr-plus subscribe.lua
|
||
-- @author William Chan <root@williamchan.me>
|
||
------------------------------------------------
|
||
require 'nixio'
|
||
require 'luci.util'
|
||
require 'luci.jsonc'
|
||
require 'luci.sys'
|
||
require 'uci'
|
||
-- 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 b64decode = nixio.bin.b64decode
|
||
local nodeResult = {} -- update result
|
||
local application = 'shadowsocksr'
|
||
local uciType = 'servers'
|
||
local ucic2 = uci.cursor()
|
||
local proxy = ucic2:get(application, '@server_subscribe[0]', 'proxy') or '0'
|
||
local switch = ucic2:get(application, '@server_subscribe[0]', 'switch') or '1'
|
||
local subscribe_url = ucic2:get(application, '@server_subscribe[0]', 'subscribe_url') or {}
|
||
local filter_words = ucic2:get(application, '@server_subscribe[0]', 'filter_words') or '过期时间/剩余流量'
|
||
ucic2:revert(application)
|
||
|
||
local log = function(...)
|
||
print(os.date("%Y-%m-%d %H:%M:%S ") .. table.concat({ ... }, " "))
|
||
end
|
||
|
||
-- 获取各项动态配置的当前服务器,可以用 get 和 set, get必须要获取到节点表
|
||
local CONFIG = {
|
||
GLOBAL_SERVER = {
|
||
remarks = '主节点',
|
||
type = "global", option = "global_server",
|
||
set = function(server)
|
||
ucic2:set(application, '@global[0]', "global_server", server)
|
||
end
|
||
}
|
||
}
|
||
do
|
||
for k, v in pairs(CONFIG) do
|
||
local currentNode
|
||
if v.get then
|
||
currentNode = v.get()
|
||
else
|
||
local cfgid = ucic2:get(application, '@' .. v.type .. '[0]', v.option)
|
||
if cfgid then
|
||
currentNode = ucic2:get_all(application, cfgid)
|
||
end
|
||
end
|
||
if currentNode then
|
||
CONFIG[k].currentNode = currentNode
|
||
else
|
||
CONFIG[k] = nil
|
||
end
|
||
end
|
||
end
|
||
|
||
-- 分割字符串
|
||
local function split(full, sep)
|
||
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
|
||
-- 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:gsub("+", " "):gsub("%%(%x%x)", get_urldecode)
|
||
end
|
||
|
||
-- trim
|
||
local function trim(text)
|
||
if not text or text == "" then
|
||
return ""
|
||
end
|
||
return (sgsub(text, "^%s*(.-)%s*$", "%1"))
|
||
end
|
||
|
||
-- base64
|
||
local function base64Decode(text)
|
||
local raw = text
|
||
if not text then return '' end
|
||
text = text:gsub("%z", "")
|
||
text = text:gsub("_", "/")
|
||
text = text:gsub("-", "+")
|
||
local mod4 = #text % 4
|
||
text = text .. string.sub('====', mod4 + 1)
|
||
local result = b64decode(text)
|
||
if result then
|
||
return result:gsub("%z", "")
|
||
else
|
||
return raw
|
||
end
|
||
end
|
||
-- 处理数据
|
||
local function processData(szType, content)
|
||
local result = {
|
||
type = szType,
|
||
local_port = 1234,
|
||
kcp_param = '--nocomp',
|
||
isSubscribe = 1,
|
||
}
|
||
if szType == 'ssr' then
|
||
local dat = split(content, "/%?")
|
||
local hostInfo = split(dat[1], ':')
|
||
result.server = hostInfo[1]
|
||
result.server_port = hostInfo[2]
|
||
result.protocol = hostInfo[3]
|
||
result.encrypt_method = hostInfo[4]
|
||
result.obfs = hostInfo[5]
|
||
result.password = base64Decode(hostInfo[6])
|
||
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.alias = "[" .. group .. "] "
|
||
end
|
||
result.alias = result.alias .. base64Decode(params.remarks)
|
||
elseif szType == 'vmess' then
|
||
local info = jsonParse(content)
|
||
result.type = 'v2ray'
|
||
result.server = info.add
|
||
result.server_port = info.port
|
||
result.transport = info.net
|
||
result.alter_id = info.aid
|
||
result.vmess_id = info.id
|
||
result.alias = info.ps
|
||
result.insecure = 1
|
||
-- result.mux = 1
|
||
-- result.concurrency = 8
|
||
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
|
||
result.tcp_guise = info.type
|
||
result.http_host = info.host
|
||
result.http_path = info.path
|
||
end
|
||
if info.net == 'kcp' then
|
||
result.kcp_guise = info.type
|
||
result.mtu = 1350
|
||
result.tti = 50
|
||
result.uplink_capacity = 5
|
||
result.downlink_capacity = 20
|
||
result.read_buffer_size = 2
|
||
result.write_buffer_size = 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.security then
|
||
result.security = info.security
|
||
end
|
||
if info.tls == "tls" or info.tls == "1" then
|
||
result.tls = "1"
|
||
result.tls_host = info.host
|
||
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
|
||
local info = content:sub(1, idx_sp - 1)
|
||
local hostInfo = split(base64Decode(info), "@")
|
||
local host = split(hostInfo[2], ":")
|
||
local userinfo = base64Decode(hostInfo[1])
|
||
local method = userinfo:sub(1, userinfo:find(":") - 1)
|
||
local password = userinfo:sub(userinfo:find(":") + 1, #userinfo)
|
||
result.alias = UrlDecode(alias)
|
||
result.type = "ss"
|
||
result.server = host[1]
|
||
if host[2]:find("/%?") then
|
||
local query = split(host[2], "/%?")
|
||
result.server_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
|
||
else
|
||
result.server_port = host[2]
|
||
end
|
||
result.encrypt_method_ss = method
|
||
result.password = password
|
||
elseif szType == "ssd" then
|
||
result.type = "ss"
|
||
result.server = content.server
|
||
result.server_port = content.port
|
||
result.password = content.password
|
||
result.encrypt_method_ss = content.encryption
|
||
result.plugin = content.plugin
|
||
result.plugin_opts = content.plugin_options
|
||
result.alias = "[" .. content.airport .. "] " .. content.remarks
|
||
elseif szType == "trojan" then
|
||
local idx_sp = 0
|
||
local alias = ""
|
||
if content:find("#") then
|
||
idx_sp = content:find("#")
|
||
alias = content:sub(idx_sp + 1, -1)
|
||
end
|
||
local info = content:sub(1, idx_sp - 1)
|
||
local hostInfo = split(info, "@")
|
||
local host = split(hostInfo[2], ":")
|
||
local userinfo = hostInfo[1]
|
||
local password = userinfo
|
||
result.alias = UrlDecode(alias)
|
||
result.type = "trojan"
|
||
result.server = host[1]
|
||
-- 按照官方的建议 默认验证ssl证书
|
||
result.insecure = "0"
|
||
result.tls = "1"
|
||
if host[2]:find("?") then
|
||
local query = split(host[2], "?")
|
||
result.server_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.peer then
|
||
-- 未指定peer(sni)默认使用remote addr
|
||
result.tls_host = params.peer
|
||
end
|
||
|
||
if params.allowInsecure == "1" then
|
||
result.insecure = "1"
|
||
else
|
||
result.insecure = "0"
|
||
end
|
||
else
|
||
result.server_port = host[2]
|
||
end
|
||
result.password = password
|
||
end
|
||
if not result.alias then
|
||
result.alias = result.server .. ':' .. result.server_port
|
||
end
|
||
return result
|
||
end
|
||
-- wget
|
||
local function wget(url)
|
||
local stdout = luci.sys.exec('wget-ssl -q --user-agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36" --no-check-certificate -t 3 -T 10 -O- "' .. url .. '"')
|
||
return trim(stdout)
|
||
end
|
||
|
||
local function check_filer(result)
|
||
do
|
||
local filter_word = split(filter_words, "/")
|
||
for i, v in pairs(filter_word) do
|
||
if result.alias:find(v) then
|
||
log('订阅节点关键字过滤:“' .. v ..'” ,该节点被丢弃')
|
||
return true
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
local function select_node(nodes, config)
|
||
local server
|
||
-- 第一优先级 IP + 端口
|
||
for id, node in pairs(nodes) do
|
||
if node.server .. ':' .. node.server_port == config.currentNode.server .. ':' .. config.currentNode.server_port then
|
||
log('选择【' .. config.remarks .. '】第一匹配节点:' .. node.alias)
|
||
server = id
|
||
break
|
||
end
|
||
end
|
||
-- 第二优先级 IP
|
||
if not server then
|
||
for id, node in pairs(nodes) do
|
||
if node.server == config.currentNode.server then
|
||
log('选择【' .. config.remarks .. '】第二匹配节点:' .. node.alias)
|
||
server = id
|
||
break
|
||
end
|
||
end
|
||
end
|
||
-- 第三优先级备注
|
||
if not server then
|
||
for id, node in pairs(nodes) do
|
||
if node.alias == config.currentNode.alias then
|
||
log('选择【' .. config.remarks .. '】第三匹配节点:' .. node.alias)
|
||
server = id
|
||
break
|
||
end
|
||
end
|
||
end
|
||
-- 第四 cfgid
|
||
if not server then
|
||
for id, node in pairs(nodes) do
|
||
if id == config.currentNode['.name'] then
|
||
log('选择【' .. config.remarks .. '】第四匹配节点:' .. node.alias)
|
||
server = id
|
||
break
|
||
end
|
||
end
|
||
end
|
||
-- 还不行 随便找一个
|
||
if not server then
|
||
server = ucic2:get(application, '@'.. uciType .. '[0]')
|
||
if server then
|
||
log('无法找到最匹配的节点,当前已更换为' .. ucic2:get_all(application, server).alias)
|
||
end
|
||
end
|
||
if server then
|
||
config.set(server)
|
||
end
|
||
end
|
||
|
||
local execute = function()
|
||
-- exec
|
||
do
|
||
if proxy == '0' then -- 不使用代理更新的话先暂停
|
||
log('服务正在暂停')
|
||
luci.sys.init.stop(application)
|
||
end
|
||
for k, url in ipairs(subscribe_url) do
|
||
local raw = wget(url)
|
||
if #raw > 0 then
|
||
local nodes, szType
|
||
local all_odes = {}
|
||
tinsert(nodeResult, all_odes)
|
||
local index = #nodeResult
|
||
-- 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 外的格式
|
||
nodes = split(base64Decode(raw):gsub(" ", "\n"), "\n")
|
||
end
|
||
for _, v in ipairs(nodes) do
|
||
if v then
|
||
local result
|
||
if szType == 'ssd' then
|
||
result = processData(szType, v)
|
||
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])
|
||
else
|
||
result = processData(dat[1], base64Decode(dat[2]))
|
||
end
|
||
end
|
||
else
|
||
log('跳过未知类型: ' .. szType)
|
||
end
|
||
-- log(result)
|
||
if result then
|
||
if
|
||
not result.server or
|
||
not result.server_port or
|
||
check_filer(result) or
|
||
result.server:match("[^0-9a-zA-Z%-%.%s]") -- 中文做地址的 也没有人拿中文域名搞,就算中文域也有Puny Code SB 机场
|
||
then
|
||
log('丢弃无效节点: ' .. result.type ..' 节点, ' .. result.alias)
|
||
else
|
||
log('成功解析: ' .. result.type ..' 节点, ' .. result.alias)
|
||
tinsert(all_odes, result)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
log('成功解析节点数量: ' ..#nodes)
|
||
else
|
||
log(url .. ': 获取内容为空')
|
||
end
|
||
end
|
||
end
|
||
-- diff
|
||
do
|
||
assert(next(nodeResult), '更新失败,没有可用的节点信息')
|
||
-- delete all for subscribe nodes
|
||
ucic2:foreach(application, uciType, function(node)
|
||
if node.isSubscribe or node.hashkey then -- 兼容之前的hashkey
|
||
ucic2:delete(application, node['.name'])
|
||
end
|
||
end)
|
||
for _, v in ipairs(nodeResult) do
|
||
for _, vv in ipairs(v) do
|
||
vv.switch_enable = switch
|
||
local cfgid = ucic2:add(application, uciType)
|
||
for kkk, vvv in pairs(vv) do
|
||
ucic2:set(application, cfgid, kkk, vvv)
|
||
end
|
||
end
|
||
end
|
||
ucic2:commit(application)
|
||
local ucic3 = uci.cursor()
|
||
-- repair configuration
|
||
if next(CONFIG) then
|
||
local nodes = {}
|
||
ucic3:foreach(application, uciType, function(node)
|
||
if node.server and node.server_port and node.alias then
|
||
nodes[node['.name']] = node
|
||
end
|
||
end)
|
||
for _, config in pairs(CONFIG) do
|
||
select_node(nodes, config)
|
||
end
|
||
ucic3:commit(application)
|
||
end
|
||
-- select first server
|
||
local globalServer = ucic3:get(application, '@global[0]', 'global_server') or ''
|
||
if not globalServer or not ucic3:get_all(application, globalServer) then
|
||
ucic3:set(application, '@global[0]', 'global_server', select(2, ucic3:get(application, '@' .. uciType .. '[0]')))
|
||
ucic3:commit(application)
|
||
log('当前没有主节点,自动选择第一个节点开启服务。')
|
||
end
|
||
luci.sys.call("/etc/init.d/" .. application .." restart > /dev/null 2>&1 &") -- 不加&的话日志会出现的更早
|
||
log('订阅更新成功')
|
||
end
|
||
end
|
||
|
||
if subscribe_url and #subscribe_url > 0 then
|
||
xpcall(execute, function(e)
|
||
log(e)
|
||
log(debug.traceback())
|
||
log('发生错误, 正在恢复服务')
|
||
if CONFIG.GLOBAL_SERVER and CONFIG.GLOBAL_SERVER.currentNode then
|
||
luci.sys.call("/etc/init.d/" .. application .." restart > /dev/null 2>&1 &") -- 不加&的话日志会出现的更早
|
||
log('重启服务成功')
|
||
else
|
||
luci.sys.call("/etc/init.d/" .. application .." stop > /dev/null 2>&1 &") -- 不加&的话日志会出现的更早
|
||
log('停止服务成功')
|
||
end
|
||
end)
|
||
end
|