small-package/luci-app-passwall2/luasrc/passwall2/api.lua

697 lines
18 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

module("luci.passwall2.api", package.seeall)
fs = require "nixio.fs"
sys = require "luci.sys"
uci = require"luci.model.uci".cursor()
util = require "luci.util"
datatypes = require "luci.cbi.datatypes"
jsonc = require "luci.jsonc"
i18n = require "luci.i18n"
appname = "passwall2"
curl_args = {"-skfL", "--connect-timeout 3", "--retry 3", "-m 60"}
command_timeout = 300
LEDE_BOARD = nil
DISTRIB_TARGET = nil
LOG_FILE = "/tmp/log/passwall2.log"
function log(...)
local result = os.date("%Y-%m-%d %H:%M:%S: ") .. table.concat({...}, " ")
local f, err = io.open(LOG_FILE, "a")
if f and err == nil then
f:write(result .. "\n")
f:close()
end
end
function exec_call(cmd)
local process = io.popen(cmd .. '; echo -e "\n$?"')
local lines = {}
local result = ""
local return_code
for line in process:lines() do
lines[#lines + 1] = line
end
process:close()
if #lines > 0 then
return_code = lines[#lines]
for i = 1, #lines - 1 do
result = result .. lines[i] .. ((i == #lines - 1) and "" or "\n")
end
end
return tonumber(return_code), trim(result)
end
function base64Decode(text)
local raw = text
if not text then return '' end
text = text:gsub("%z", "")
text = text:gsub("%c", "")
text = text:gsub("_", "/")
text = text:gsub("-", "+")
local mod4 = #text % 4
text = text .. string.sub('====', mod4 + 1)
local result = nixio.bin.b64decode(text)
if result then
return result:gsub("%z", "")
else
return raw
end
end
function curl_base(url, file, args)
if not args then args = {} end
if file then
args[#args + 1] = "-o " .. file
end
local cmd = string.format('curl %s "%s"', table_join(args), url)
return exec_call(cmd)
end
function curl_proxy(url, file, args)
--使用代理
local socks_server = luci.sys.exec("[ -f /tmp/etc/passwall2/global_SOCKS_server ] && echo -n $(cat /tmp/etc/passwall2/global_SOCKS_server) || echo -n ''")
if socks_server ~= "" then
if not args then args = {} end
local tmp_args = clone(args)
tmp_args[#tmp_args + 1] = "-x socks5h://" .. socks_server
return curl_base(url, file, tmp_args)
end
return nil, nil
end
function curl_logic(url, file, args)
local return_code, result = curl_proxy(url, file, args)
if not return_code or return_code ~= 0 then
return_code, result = curl_base(url, file, args)
end
return return_code, result
end
function url(...)
local url = string.format("admin/services/%s", appname)
local args = { ... }
for i, v in pairs(args) do
if v ~= "" then
url = url .. "/" .. v
end
end
return require "luci.dispatcher".build_url(url)
end
function trim(s)
return (s:gsub("^%s*(.-)%s*$", "%1"))
end
function is_exist(table, value)
for index, k in ipairs(table) do
if k == value then
return true
end
end
return false
end
function repeat_exist(table, value)
local count = 0
for index, k in ipairs(table) do
if k:find("-") and k == value then
count = count + 1
end
end
if count > 1 then
return true
end
return false
end
function get_args(arg)
local var = {}
for i, arg_k in pairs(arg) do
if i > 0 then
local v = arg[i + 1]
if v then
if repeat_exist(arg, v) == false then
var[arg_k] = v
end
end
end
end
return var
end
function get_function_args(arg)
local var = nil
if arg and #arg > 1 then
local param = {}
for i = 2, #arg do
param[#param + 1] = arg[i]
end
var = get_args(param)
end
return var
end
function strToTable(str)
if str == nil or type(str) ~= "string" then
return {}
end
return loadstring("return " .. str)()
end
function is_normal_node(e)
if e and e.type and e.protocol and (e.protocol == "_balancing" or e.protocol == "_shunt" or e.protocol == "_iface") then
return false
end
return true
end
function is_special_node(e)
return is_normal_node(e) == false
end
function is_ip(val)
if is_ipv6(val) then
val = get_ipv6_only(val)
end
return datatypes.ipaddr(val)
end
function is_ipv6(val)
local str = val
local address = val:match('%[(.*)%]')
if address then
str = address
end
if datatypes.ip6addr(str) then
return true
end
return false
end
function is_ipv6addrport(val)
if is_ipv6(val) then
local address, port = val:match('%[(.*)%]:([^:]+)$')
if port then
return datatypes.port(port)
end
end
return false
end
function get_ipv6_only(val)
local result = ""
if is_ipv6(val) then
result = val
if val:match('%[(.*)%]') then
result = val:match('%[(.*)%]')
end
end
return result
end
function get_ipv6_full(val)
local result = ""
if is_ipv6(val) then
result = val
if not val:match('%[(.*)%]') then
result = "[" .. result .. "]"
end
end
return result
end
function get_ip_type(val)
if is_ipv6(val) then
return "6"
elseif datatypes.ip4addr(val) then
return "4"
end
return ""
end
function is_mac(val)
return datatypes.macaddr(val)
end
function ip_or_mac(val)
if val then
if get_ip_type(val) == "4" then
return "ip"
end
if is_mac(val) then
return "mac"
end
end
return ""
end
function iprange(val)
if val then
local ipStart, ipEnd = val:match("^([^/]+)-([^/]+)$")
if (ipStart and datatypes.ip4addr(ipStart)) and (ipEnd and datatypes.ip4addr(ipEnd)) then
return true
end
end
return false
end
function get_domain_from_url(url)
local domain = string.match(url, "//([^/]+)")
if domain then
return domain
end
return url
end
function get_valid_nodes()
local nodes_ping = uci_get_type("global_other", "nodes_ping") or ""
local nodes = {}
uci:foreach(appname, "nodes", function(e)
e.id = e[".name"]
if e.type and e.remarks then
if e.protocol and (e.protocol == "_balancing" or e.protocol == "_shunt" or e.protocol == "_iface") then
e["remark"] = "%s[%s] " % {i18n.translatef(e.type .. e.protocol), e.remarks}
e["node_type"] = "special"
nodes[#nodes + 1] = e
end
if e.port and e.address then
local address = e.address
if is_ip(address) or datatypes.hostname(address) then
local type = e.type
if (type == "V2ray" or type == "Xray") and e.protocol then
local protocol = e.protocol
if protocol == "vmess" then
protocol = "VMess"
elseif protocol == "vless" then
protocol = "VLESS"
else
protocol = protocol:gsub("^%l",string.upper)
end
type = type .. " " .. protocol
end
if is_ipv6(address) then address = get_ipv6_full(address) end
e["remark"] = "%s[%s]" % {type, e.remarks}
if nodes_ping:find("info") then
e["remark"] = "%s[%s] %s:%s" % {type, e.remarks, address, e.port}
end
e.node_type = "normal"
nodes[#nodes + 1] = e
end
end
end
end)
return nodes
end
function get_full_node_remarks(n)
local remarks = ""
if n then
if n.protocol and (n.protocol == "_balancing" or n.protocol == "_shunt" or n.protocol == "_iface") then
remarks = "%s[%s] " % {i18n.translatef(n.type .. n.protocol), n.remarks}
else
local type2 = n.type
if (n.type == "V2ray" or n.type == "Xray") and n.protocol then
local protocol = n.protocol
if protocol == "vmess" then
protocol = "VMess"
elseif protocol == "vless" then
protocol = "VLESS"
else
protocol = protocol:gsub("^%l",string.upper)
end
type2 = type2 .. " " .. protocol
end
remarks = "%s[%s] %s:%s" % {type2, n.remarks, n.address, n.port}
end
end
return remarks
end
function gen_uuid(format)
local uuid = sys.exec("echo -n $(cat /proc/sys/kernel/random/uuid)")
if format == nil then
uuid = string.gsub(uuid, "-", "")
end
return uuid
end
function uci_get_type(type, config, default)
local value = uci:get_first(appname, type, config, default) or sys.exec("echo -n $(uci -q get " .. appname .. ".@" .. type .."[0]." .. config .. ")")
if (value == nil or value == "") and (default and default ~= "") then
value = default
end
return value
end
function uci_get_type_id(id, config, default)
local value = uci:get(appname, id, config, default) or sys.exec("echo -n $(uci -q get " .. appname .. "." .. id .. "." .. config .. ")")
if (value == nil or value == "") and (default and default ~= "") then
value = default
end
return value
end
function chmod_755(file)
if file and file ~= "" then
if not fs.access(file, "rwx", "rx", "rx") then
fs.chmod(file, 755)
end
end
end
function get_customed_path(e)
return uci_get_type("global_app", e .. "_file")
end
function is_finded(e)
return luci.sys.exec('type -t -p "/bin/%s" -p "/usr/bin/%s" -p "%s" "%s"' % {e, e, get_customed_path(e), e}) ~= "" and true or false
end
function clone(org)
local function copy(org, res)
for k,v in pairs(org) do
if type(v) ~= "table" then
res[k] = v;
else
res[k] = {};
copy(v, res[k])
end
end
end
local res = {}
copy(org, res)
return res
end
function get_bin_version_cache(file, cmd)
sys.call("mkdir -p /tmp/etc/passwall2_tmp")
if fs.access(file) then
chmod_755(file)
local md5 = sys.exec("echo -n $(md5sum " .. file .. " | awk '{print $1}')")
if fs.access("/tmp/etc/passwall2_tmp/" .. md5) then
return sys.exec("echo -n $(cat /tmp/etc/passwall2_tmp/%s)" % md5)
else
local version = sys.exec(string.format("echo -n $(%s %s)", file, cmd))
if version and version ~= "" then
sys.call("echo '" .. version .. "' > " .. "/tmp/etc/passwall2_tmp/" .. md5)
return version
end
end
end
return ""
end
function get_v2ray_path()
local path = uci_get_type("global_app", "v2ray_file")
return path
end
function get_v2ray_version(file)
if file == nil then file = get_v2ray_path() end
local cmd = "version | awk '{print $2}' | sed -n 1P"
return get_bin_version_cache(file, cmd)
end
function get_xray_path()
local path = uci_get_type("global_app", "xray_file")
return path
end
function get_xray_version(file)
if file == nil then file = get_xray_path() end
local cmd = "-version | awk '{print $2}' | sed -n 1P"
return get_bin_version_cache(file, cmd)
end
function get_brook_path()
local path = uci_get_type("global_app", "brook_file")
return path
end
function get_brook_version(file)
if file == nil then file = get_brook_path() end
local cmd = "-v | awk '{print $3}'"
return get_bin_version_cache(file, cmd)
end
function get_hysteria_path()
local path = uci_get_type("global_app", "hysteria_file")
return path
end
function get_hysteria_version(file)
if file == nil then file = get_hysteria_path() end
local cmd = "-v | awk '{print $3}'"
return get_bin_version_cache(file, cmd)
end
function is_file(path)
if path and #path > 1 then
if sys.exec('[ -f "%s" ] && echo -n 1' % path) == "1" then
return true
end
end
return nil
end
function is_dir(path)
if path and #path > 1 then
if sys.exec('[ -d "%s" ] && echo -n 1' % path) == "1" then
return true
end
end
return nil
end
function get_final_dir(path)
if is_dir(path) then
return path
else
return get_final_dir(fs.dirname(path))
end
end
function get_free_space(dir)
if dir == nil then dir = "/" end
if sys.call("df -k " .. dir .. " >/dev/null 2>&1") == 0 then
return tonumber(sys.exec("echo -n $(df -k " .. dir .. " | awk 'NR>1' | awk '{print $4}')"))
end
return 0
end
function get_file_space(file)
if file == nil then return 0 end
if fs.access(file) then
return tonumber(sys.exec("echo -n $(du -k " .. file .. " | awk '{print $1}')"))
end
return 0
end
function _unpack(t, i)
i = i or 1
if t[i] ~= nil then return t[i], _unpack(t, i + 1) end
end
function table_join(t, s)
if not s then
s = " "
end
local str = ""
for index, value in ipairs(t) do
str = str .. t[index] .. (index == #t and "" or s)
end
return str
end
function exec(cmd, args, writer, timeout)
local os = require "os"
local nixio = require "nixio"
local fdi, fdo = nixio.pipe()
local pid = nixio.fork()
if pid > 0 then
fdo:close()
if writer or timeout then
local starttime = os.time()
while true do
if timeout and os.difftime(os.time(), starttime) >= timeout then
nixio.kill(pid, nixio.const.SIGTERM)
return 1
end
if writer then
local buffer = fdi:read(2048)
if buffer and #buffer > 0 then
writer(buffer)
end
end
local wpid, stat, code = nixio.waitpid(pid, "nohang")
if wpid and stat == "exited" then return code end
if not writer and timeout then nixio.nanosleep(1) end
end
else
local wpid, stat, code = nixio.waitpid(pid)
return wpid and stat == "exited" and code
end
elseif pid == 0 then
nixio.dup(fdo, nixio.stdout)
fdi:close()
fdo:close()
nixio.exece(cmd, args, nil)
nixio.stdout:close()
os.exit(1)
end
end
function compare_versions(ver1, comp, ver2)
local table = table
if not ver1 then ver1 = "" end
if not ver2 then ver2 = "" end
local av1 = util.split(ver1, "[%.%-]", nil, true)
local av2 = util.split(ver2, "[%.%-]", nil, true)
local max = table.getn(av1)
local n2 = table.getn(av2)
if (max < n2) then max = n2 end
for i = 1, max, 1 do
local s1 = tonumber(av1[i] or 0) or 0
local s2 = tonumber(av2[i] or 0) or 0
if comp == "~=" and (s1 ~= s2) then return true end
if (comp == "<" or comp == "<=") and (s1 < s2) then return true end
if (comp == ">" or comp == ">=") and (s1 > s2) then return true end
if (s1 ~= s2) then return false end
end
return not (comp == "<" or comp == ">")
end
function auto_get_arch()
local arch = nixio.uname().machine or ""
if fs.access("/usr/lib/os-release") then
LEDE_BOARD = sys.exec("echo -n $(grep 'LEDE_BOARD' /usr/lib/os-release | awk -F '[\\042\\047]' '{print $2}')")
end
if fs.access("/etc/openwrt_release") then
DISTRIB_TARGET = sys.exec("echo -n $(grep 'DISTRIB_TARGET' /etc/openwrt_release | awk -F '[\\042\\047]' '{print $2}')")
end
if arch == "mips" then
if LEDE_BOARD and LEDE_BOARD ~= "" then
if string.match(LEDE_BOARD, "ramips") == "ramips" then
arch = "ramips"
else
arch = sys.exec("echo '" .. LEDE_BOARD .. "' | grep -oE 'ramips|ar71xx'")
end
elseif DISTRIB_TARGET and DISTRIB_TARGET ~= "" then
if string.match(DISTRIB_TARGET, "ramips") == "ramips" then
arch = "ramips"
else
arch = sys.exec("echo '" .. DISTRIB_TARGET .. "' | grep -oE 'ramips|ar71xx'")
end
end
end
return util.trim(arch)
end
function get_file_info(arch)
local file_tree = ""
local sub_version = ""
if arch == "x86_64" then
file_tree = "amd64"
elseif arch == "aarch64" then
file_tree = "arm64"
elseif arch == "ramips" then
file_tree = "mipsle"
elseif arch == "ar71xx" then
file_tree = "mips"
elseif arch:match("^i[%d]86$") then
file_tree = "386"
elseif arch:match("^armv[5-8]") then
file_tree = "arm"
sub_version = arch:match("[5-8]")
if LEDE_BOARD and string.match(LEDE_BOARD, "bcm53xx") == "bcm53xx" then
sub_version = "5"
elseif DISTRIB_TARGET and string.match(DISTRIB_TARGET, "bcm53xx") ==
"bcm53xx" then
sub_version = "5"
end
sub_version = "5"
end
return file_tree, sub_version
end
function get_api_json(url)
local jsonc = require "luci.jsonc"
local return_code, content = curl_logic(url, nil, curl_args)
if return_code ~= 0 or content == "" then return {} end
return jsonc.parse(content) or {}
end
function common_to_check(api_url, local_version, match_file_name)
local json = get_api_json(api_url)
if #json > 0 then
json = json[1]
end
if json.tag_name == nil then
return {
code = 1,
error = i18n.translate("Get remote version info failed.")
}
end
local remote_version = json.tag_name
local has_update = compare_versions(local_version:match("[^v]+"), "<", remote_version:match("[^v]+"))
if not has_update then
return {
code = 0,
local_version = local_version,
remote_version = remote_version
}
end
local asset = {}
for _, v in ipairs(json.assets) do
if v.name and v.name:match(match_file_name) then
asset = v
break
end
end
if not asset.browser_download_url then
return {
code = 1,
local_version = local_version,
remote_version = remote_version,
html_url = json.html_url,
data = asset,
error = i18n.translate("New version found, but failed to get new version download url.")
}
end
return {
code = 0,
has_update = true,
local_version = local_version,
remote_version = remote_version,
html_url = json.html_url,
data = asset
}
end