module("luci.model.cbi.passwall.api.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 = "passwall" curl = "/usr/bin/curl" curl_args = {"-skfL", "--connect-timeout 3", "--retry 3", "-m 60"} command_timeout = 300 LEDE_BOARD = nil DISTRIB_TARGET = nil 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 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 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") 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_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") 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") 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 "%s" "%s"' % {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/passwall_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/passwall_tmp/" .. md5) then return sys.exec("echo -n $(cat /tmp/etc/passwall_tmp/%s)" % md5) else local version = sys.exec(string.format("echo -n $(%s %s)", file, cmd)) sys.call("echo '" .. version .. "' > " .. "/tmp/etc/passwall_tmp/" .. md5) return version 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_trojan_go_path() local path = uci_get_type("global_app", "trojan_go_file") return path end function get_trojan_go_version(file) if file == nil then file = get_trojan_go_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 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 json_content = luci.sys.exec(curl .. " " .. _unpack(curl_args) .. " " .. url) if json_content == "" then return {} end return jsonc.parse(json_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