op-packages/luci-app-easytier/luasrc/controller/easytier.lua
github-actions[bot] 1d77fe1b27 🤞 Sync 2026-05-16 20:23:58
2026-05-16 20:23:58 +08:00

1274 lines
45 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.controller.easytier", package.seeall)
local i18n = require "luci.i18n"
-- 安全执行命令并返回结果
local function safe_exec(cmd)
local handle = io.popen(cmd)
if not handle then return "" end
local result = handle:read("*all") or ""
handle:close()
return result:gsub("[\r\n]+$", "")
end
-- 安全读取文件内容
local function safe_read_file(path)
local file = io.open(path, "r")
if not file then return nil end
local content = file:read("*all")
file:close()
return content
end
-- 计算运行时长
local function calc_uptime(start_time_file)
local content = safe_read_file(start_time_file)
if not content or content == "" then return "" end
local start_time = tonumber(content:match("%d+"))
if not start_time then return "" end
local now = os.time()
local elapsed = now - start_time
local days = math.floor(elapsed / 86400)
local hours = math.floor((elapsed % 86400) / 3600)
local mins = math.floor((elapsed % 3600) / 60)
local secs = elapsed % 60
local result = ""
if days > 0 then result = days .. "" end
result = result .. string.format("%02d小时%02d分%02d秒", hours, mins, secs)
return result
end
function index()
if not nixio.fs.access("/etc/config/easytier") then
return
end
entry({"admin", "vpn"}, firstchild(), "VPN", 45).dependent = false
entry({"admin", "vpn", "easytier"}, firstchild(),_("EasyTier"), 46).dependent = true
entry({"admin", "vpn", "easytier", "status"}, cbi("easytier_status"),_("Status"), 1).leaf = true
entry({"admin", "vpn", "easytier", "config"}, cbi("easytier"),_("EasyTier Core"), 2).leaf = true
entry({"admin", "vpn", "easytier", "webconsole"}, template("easytier/easytier_web"),_("EasyTier Web"), 3).leaf = true
entry({"admin", "vpn", "easytier", "log"}, template("easytier/easytier_log"),_("Logs"), 4).leaf = true
entry({"admin", "vpn", "easytier", "upload"}, template("easytier/easytier_upload"),_("Upload Program"), 5).leaf = true
entry({"admin", "vpn", "easytier", "upload_binary"}, call("upload_binary")).leaf = true
entry({"admin", "vpn", "easytier", "get_upload_config"}, call("get_upload_config")).leaf = true
entry({"admin", "vpn", "easytier", "save_upload_config"}, call("save_upload_config")).leaf = true
entry({"admin", "vpn", "easytier", "get_disk_space"}, call("get_disk_space")).leaf = true
entry({"admin", "vpn", "easytier", "get_tun_info"}, call("get_tun_info")).leaf = true
entry({"admin", "vpn", "easytier", "get_log"}, call("get_log")).leaf = true
entry({"admin", "vpn", "easytier", "get_log_size"}, call("get_log_size")).leaf = true
entry({"admin", "vpn", "easytier", "clear_log"}, call("clear_log")).leaf = true
entry({"admin", "vpn", "easytier", "get_wlog"}, call("get_wlog")).leaf = true
entry({"admin", "vpn", "easytier", "get_wlog_size"}, call("get_wlog_size")).leaf = true
entry({"admin", "vpn", "easytier", "clear_wlog"}, call("clear_wlog")).leaf = true
entry({"admin", "vpn", "easytier", "clear_version_cache"}, call("clear_version_cache")).leaf = true
entry({"admin", "vpn", "easytier", "get_web_config"}, call("get_web_config")).leaf = true
entry({"admin", "vpn", "easytier", "save_web_config"}, call("save_web_config")).leaf = true
entry({"admin", "vpn", "easytier", "reset_database"}, call("reset_database")).leaf = true
entry({"admin", "vpn", "easytier", "check_web_status"}, call("check_web_status")).leaf = true
entry({"admin", "vpn", "easytier", "api_status"}, call("act_status")).leaf = true
entry({"admin", "vpn", "easytier", "api_conninfo"}, call("act_conninfo")).leaf = true
entry({"admin", "vpn", "easytier", "restart_service"}, call("restart_service")).leaf = true
entry({"admin", "vpn", "easytier", "toggle_core"}, call("toggle_core")).leaf = true
entry({"admin", "vpn", "easytier", "toggle_web"}, call("toggle_web")).leaf = true
entry({"admin", "vpn", "easytier", "download_easytier"}, call("download_easytier")).leaf = true
entry({"admin", "vpn", "easytier", "download_progress"}, call("download_progress")).leaf = true
entry({"admin", "vpn", "easytier", "cancel_download"}, call("cancel_download")).leaf = true
end
function act_status()
local e = {}
local sys = require "luci.sys"
local uci = require "luci.model.uci".cursor()
local port = tonumber(uci:get_first("easytier", "easytier", "web_html_port"))
e.crunning = luci.sys.call("pgrep easytier-core >/dev/null") == 0
e.wrunning = luci.sys.call("pgrep easytier-web >/dev/null") == 0
e.port = (port or 0)
e.cenabled = uci:get_first("easytier", "easytier", "enabled") == "1"
e.wenabled = uci:get_first("easytier", "easytier", "web_enabled") == "1"
-- 使用 Lua 原生计算运行时长
e.etsta = calc_uptime("/tmp/easytier_time")
e.etwebsta = calc_uptime("/tmp/easytierweb_time")
-- 获取 CPU 和内存使用率(使用原始命令)
local command2 = io.popen('test ! -z "`pidof easytier-core`" && (top -b -n1 | grep -E "$(pidof easytier-core)" 2>/dev/null | grep -v grep | awk \'{for (i=1;i<=NF;i++) {if ($i ~ /easytier-core/) break; else cpu=i}} END {print $cpu}\')')
e.etcpu = command2:read("*all")
command2:close()
local command3 = io.popen("test ! -z `pidof easytier-core` && (cat /proc/$(pidof easytier-core | awk '{print $NF}')/status | grep -w VmRSS | awk '{printf \"%.2f MB\", $2/1024}')")
e.etram = command3:read("*all")
command3:close()
local command4 = io.popen('test ! -z "`pidof easytier-web`" && (top -b -n1 | grep -E "$(pidof easytier-web)" 2>/dev/null | grep -v grep | awk \'{for (i=1;i<=NF;i++) {if ($i ~ /easytier-web/) break; else cpu=i}} END {print $cpu}\')')
e.etwebcpu = command4:read("*all")
command4:close()
local command5 = io.popen("test ! -z `pidof easytier-web` && (cat /proc/$(pidof easytier-web | awk '{print $NF}')/status | grep -w VmRSS | awk '{printf \"%.2f MB\", $2/1024}')")
e.etwebram = command5:read("*all")
command5:close()
-- 获取版本信息
local cached_newtag = safe_read_file("/tmp/easytiernew.tag")
if cached_newtag and cached_newtag ~= "" then
e.etnewtag = cached_newtag:gsub("[\r\n]+", "")
else
e.etnewtag = safe_exec("curl -L -k -s --connect-timeout 3 --user-agent 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36' https://api.github.com/repos/EasyTier/EasyTier/releases/latest | grep tag_name | sed 's/[^0-9.]*//g'")
if e.etnewtag ~= "" then
local f = io.open("/tmp/easytiernew.tag", "w")
if f then f:write(e.etnewtag); f:close() end
end
end
local cached_tag = safe_read_file("/tmp/easytier.tag")
if cached_tag and cached_tag ~= "" then
e.ettag = cached_tag:gsub("[\r\n]+", "")
else
local easytierbin = uci:get_first("easytier", "easytier", "easytierbin") or "/usr/bin/easytier-core"
e.ettag = safe_exec(easytierbin .. " -V 2>/dev/null | sed 's/^[^0-9]*//'")
if e.ettag == "" or e.ettag == nil then e.ettag = "unknown" end
local f = io.open("/tmp/easytier.tag", "w")
if f then f:write(e.ettag); f:close() end
end
local cached_webtag = safe_read_file("/tmp/easytierweb.tag")
if cached_webtag and cached_webtag ~= "" then
e.etwebtag = cached_webtag:gsub("[\r\n]+", "")
else
local easytierwebbin = uci:get_first("easytier", "easytier", "webbin") or "/usr/bin/easytier-web"
e.etwebtag = safe_exec(easytierwebbin .. " -V 2>/dev/null | sed 's/^[^0-9]*//'")
if e.etwebtag == "" or e.etwebtag == nil then e.etwebtag = "unknown" end
local f = io.open("/tmp/easytierweb.tag", "w")
if f then f:write(e.etwebtag); f:close() end
end
e.no_tun = uci:get_first("easytier", "easytier", "no_tun") == "1"
e.dev_name = uci:get_first("easytier", "easytier", "dev_name") or "tun0"
luci.http.prepare_content("application/json")
luci.http.write_json(e)
end
function get_upload_config()
local uci = require "luci.model.uci".cursor()
local http = require "luci.http"
http.prepare_content("application/json")
local config = {
easytierbin = uci:get_first("easytier", "easytier", "easytierbin") or "/usr/bin/easytier-core",
webbin = uci:get_first("easytier", "easytier", "webbin") or "/usr/bin/easytier-web",
github_proxys = {},
fallback_version = uci:get_first("easytier", "easytier", "fallback_version") or "v2.6.4"
}
-- 读取代理列表list 类型)
uci:foreach("easytier", "easytier", function(s)
local proxys = s.github_proxys
if proxys then
if type(proxys) == "table" then
config.github_proxys = proxys
else
table.insert(config.github_proxys, proxys)
end
end
end)
http.write_json(config)
end
function save_upload_config()
local uci = require "luci.model.uci".cursor()
local http = require "luci.http"
local json = require "luci.jsonc"
http.prepare_content("application/json")
-- 读取 POST 数据
local content = http.content()
local data = json.parse(content)
if not data then
http.write_json({success = false, message = "Invalid JSON data"})
return
end
-- 保存配置
uci:set("easytier", "@easytier[0]", "easytierbin", data.easytierbin or "/usr/bin/easytier-core")
uci:set("easytier", "@easytier[0]", "webbin", data.webbin or "/usr/bin/easytier-web")
uci:set("easytier", "@easytier[0]", "fallback_version", data.fallback_version or "v2.6.4")
-- 删除旧的代理列表
uci:delete("easytier", "@easytier[0]", "github_proxys")
-- 保存代理列表list 类型)
if data.github_proxys and type(data.github_proxys) == "table" then
for _, proxy in ipairs(data.github_proxys) do
if proxy and proxy ~= "" then
local current = uci:get("easytier", "@easytier[0]", "github_proxys") or {}
if type(current) ~= "table" then
current = {current}
end
table.insert(current, proxy)
uci:set("easytier", "@easytier[0]", "github_proxys", current)
end
end
end
uci:commit("easytier")
http.write_json({success = true, message = "Configuration saved successfully"})
end
function get_disk_space()
local http = require "luci.http"
local json = require "luci.jsonc"
local nixio = require "nixio"
http.prepare_content("application/json")
local content = http.content()
local data = json.parse(content)
if not data or not data.path then
http.write_json({success = false, message = "Invalid path"})
return
end
local path = data.path
local full_path = data.full_path
local input_type = data.type
local df_output = luci.sys.exec("df -h '" .. path .. "' 2>/dev/null | tail -n 1")
local df_kb = luci.sys.exec("df -k '" .. path .. "' 2>/dev/null | tail -n 1")
if df_output and df_output ~= "" and df_kb and df_kb ~= "" then
local available = df_output:match("%S+%s+%S+%s+%S+%s+(%S+)")
local available_kb = df_kb:match("%S+%s+%S+%s+%S+%s+(%d+)")
local available_mb = available_kb and math.floor(tonumber(available_kb) / 1024) or 0
if available and available_mb then
local result = {
success = true,
available = available,
available_mb = available_mb
}
if input_type == "easytierbin" and full_path then
if nixio.fs.access(full_path) then
local size = luci.sys.exec("ls -lh '" .. full_path .. "' 2>/dev/null | awk '{print $5}'")
if size and size ~= "" then
result.core_size = size:gsub("[\r\n]+$", "")
end
end
local dir = full_path:match("(.*/)")
local cli_path = (dir or "/usr/bin/") .. "easytier-cli"
if nixio.fs.access(cli_path) then
local size = luci.sys.exec("ls -lh '" .. cli_path .. "' 2>/dev/null | awk '{print $5}'")
if size and size ~= "" then
result.cli_size = size:gsub("[\r\n]+$", "")
end
end
elseif input_type == "webbin" and full_path then
if nixio.fs.access(full_path) then
local size = luci.sys.exec("ls -lh '" .. full_path .. "' 2>/dev/null | awk '{print $5}'")
if size and size ~= "" then
result.web_size = size:gsub("[\r\n]+$", "")
end
end
end
http.write_json(result)
return
end
end
http.write_json({success = false, message = "Unable to query disk space"})
end
function get_tun_info()
luci.http.prepare_content("application/json")
local ifname = luci.http.formvalue("ifname") or "tun0"
local exists = luci.sys.exec("ip link show " .. ifname .. " >/dev/null 2>&1 && echo 1 || echo 0")
if not exists:match("1") then
luci.http.write('{"success":false,"exists":false}')
return
end
local ifconfig_out = luci.sys.exec("ifconfig " .. ifname .. " 2>/dev/null")
local ip = ifconfig_out:match("inet addr:([%d%.]+)") or ifconfig_out:match("inet ([%d%.]+)")
local netmask = ""
local netmask_full = ifconfig_out:match("Mask:([%d%.]+)") or ifconfig_out:match("netmask ([%d%.]+)")
if not netmask_full or netmask_full == "" then
local ip_output = luci.sys.exec("ip -4 addr show " .. ifname .. " 2>/dev/null | grep 'inet ' | head -n1 | awk '{print $2}'")
local cidr = ip_output:match("/(%d+)")
if cidr then
cidr = tonumber(cidr)
local mask = 0xFFFFFFFF - (2 ^ (32 - cidr) - 1)
netmask = string.format("%d.%d.%d.%d",
math.floor(mask / 16777216) % 256,
math.floor(mask / 65536) % 256,
math.floor(mask / 256) % 256,
mask % 256)
end
else
netmask = netmask_full
end
local ipv6_cmd = luci.sys.exec("ip -6 addr show " .. ifname .. " 2>/dev/null | grep 'inet6' | head -n1 | awk '{print $2}'")
local ipv6 = ipv6_cmd:gsub("%s", "")
if ipv6 == "" then ipv6 = nil end
local mtu = ifconfig_out:match("MTU:(%d+)") or luci.sys.exec("ip link show " .. ifname .. " 2>/dev/null | head -n1 | sed -n 's/.*mtu \\([0-9]*\\).*/\\1/p'"):gsub("%s", "")
local state = "UNKNOWN"
if ifconfig_out:match("UP") then
state = "UP"
elseif ifconfig_out:match("DOWN") then
state = "DOWN"
end
local rx = luci.sys.exec("cat /sys/class/net/" .. ifname .. "/statistics/rx_bytes 2>/dev/null || echo 0"):gsub("%s", "")
local tx = luci.sys.exec("cat /sys/class/net/" .. ifname .. "/statistics/tx_bytes 2>/dev/null || echo 0"):gsub("%s", "")
local response = string.format('{"success":true,"exists":true,"ip":"%s","netmask":"%s","mtu":"%s","state":"%s","rx_bytes":%s,"tx_bytes":%s',
ip or "", netmask, mtu, state, rx, tx)
if ipv6 then
response = response .. ',"ipv6":"' .. ipv6 .. '"'
end
response = response .. '}'
luci.http.write(response)
end
function get_log()
local log = ""
local files = {"/tmp/easytier.log"}
for i, file in ipairs(files) do
if luci.sys.call("[ -f '" .. file .. "' ]") == 0 then
log = log .. luci.sys.exec("sed 's/\\x1b\\[[0-9;]*m//g' " .. file)
end
end
luci.http.write(log)
end
function get_log_size()
local size = luci.sys.exec("[ -f '/tmp/easytier.log' ] && stat -c%s /tmp/easytier.log 2>/dev/null || echo 0")
luci.http.prepare_content("application/json")
luci.http.write_json({size = tonumber(size) or 0})
end
function get_wlog_size()
local size = luci.sys.exec("[ -f '/tmp/easytierweb.log' ] && stat -c%s /tmp/easytierweb.log 2>/dev/null || echo 0")
luci.http.prepare_content("application/json")
luci.http.write_json({size = tonumber(size) or 0})
end
function clear_log()
luci.sys.call("echo '' >/tmp/easytier.log")
end
local function test_binary(path)
local handle = io.popen(path .. " -h 2>&1")
local output = handle:read("*a")
handle:close()
local line_count = 0
for _ in output:gmatch("[^\r\n]+") do
line_count = line_count + 1
end
return line_count >= 3 and output:lower():match("easytier")
end
local function cleanup_files(...)
for _, file in ipairs({...}) do
nixio.fs.remove(file)
end
end
function upload_binary()
local http = require "luci.http"
local uci = require "luci.model.uci".cursor()
local nixio = require "nixio"
local translate = i18n.translate
local fp
local filename = ""
local tmp_file = ""
http.setfilehandler(
function(meta, chunk, eof)
if meta and meta.file then
filename = meta.file
tmp_file = "/tmp/upload_" .. filename
end
if not fp and tmp_file ~= "" then
fp = io.open(tmp_file, "w")
end
if chunk and fp then
fp:write(chunk)
end
if eof and fp then
fp:close()
end
end
)
http.prepare_content("application/json")
if not http.formvalue("file") or tmp_file == "" then
http.write_json({success = false, message = translate("No file uploaded")})
return
end
local is_archive = filename:match("%.zip$") or filename:match("%.tar%.gz$") or filename:match("%.tgz$") or filename:match("%.tar$")
if is_archive then
local extract_dir = "/tmp/easytier_extract"
os.execute("rm -rf " .. extract_dir .. " && mkdir -p " .. extract_dir)
if filename:match("%.zip$") then
if os.execute("which unzip >/dev/null 2>&1") ~= 0 then
cleanup_files(tmp_file)
http.write_json({success = false, message = translate("System lacks unzip package, cannot extract zip archive")})
return
end
os.execute("unzip -o -q " .. tmp_file .. " -d " .. extract_dir)
else
os.execute("tar -xzf " .. tmp_file .. " -C " .. extract_dir .. " 2>/dev/null || tar -xf " .. tmp_file .. " -C " .. extract_dir)
end
os.execute("find " .. extract_dir .. "/easytier-linux-* -maxdepth 1 -type f -exec mv {} " .. extract_dir .. "/ \\; 2>/dev/null")
cleanup_files(tmp_file)
if nixio.fs.access(extract_dir .. "/easytier-web") then
nixio.fs.remove(extract_dir .. "/easytier-web")
end
local binaries = {"easytier-core", "easytier-cli", "easytier-web-embed"}
local valid_bins = {}
for _, bin in ipairs(binaries) do
local src = extract_dir .. "/" .. bin
if nixio.fs.access(src) then
nixio.fs.chmod(src, "755")
if test_binary(src) then
table.insert(valid_bins, {name = bin, path = src})
else
cleanup_files(src)
end
end
end
if #valid_bins == 0 then
os.execute("rm -rf " .. extract_dir)
http.write_json({success = false, message = translate("Not a valid EasyTier program or architecture mismatch")})
return
end
local core_path = uci:get_first("easytier", "easytier", "easytierbin") or "/usr/bin/easytier-core"
local web_path = uci:get_first("easytier", "easytier", "webbin") or "/usr/bin/easytier-web"
for _, bin in ipairs(valid_bins) do
local final_path
if bin.name == "easytier-web-embed" then
final_path = web_path
elseif bin.name == "easytier-core" then
final_path = core_path
elseif bin.name == "easytier-cli" then
local core_dir = core_path:match("(.*/)")
final_path = (core_dir or "/usr/bin/") .. "easytier-cli"
end
os.execute("rm -f " .. final_path)
if os.execute("mv " .. bin.path .. " " .. final_path) ~= 0 then
os.execute("rm -rf " .. extract_dir)
http.write_json({success = false, message = translate("Failed to move file. Insufficient space or permission denied")})
return
end
os.execute("chmod 755 " .. final_path)
if not test_binary(final_path) then
os.execute("rm -f " .. final_path)
os.execute("rm -rf " .. extract_dir)
http.write_json({success = false, message = translate("Not a valid EasyTier program or architecture mismatch")})
return
end
end
os.execute("rm -rf " .. extract_dir)
nixio.fs.remove("/tmp/easytier.tag")
nixio.fs.remove("/tmp/easytierweb.tag")
http.write_json({success = true, message = translate("Successfully installed") .. " " .. #valid_bins .. " " .. translate("binary file(s)")})
else
local valid_names = {["easytier-core"] = true, ["easytier-cli"] = true, ["easytier-web-embed"] = true, ["easytier-web"] = true}
if not valid_names[filename] then
cleanup_files(tmp_file)
http.write_json({success = false, message = translate("Not a valid EasyTier program")})
return
end
if filename == "easytier-web" then
cleanup_files(tmp_file)
http.write_json({success = false, message = translate("easytier-web is not used by this plugin, please upload easytier-web-embed")})
return
end
nixio.fs.chmod(tmp_file, "755")
if not test_binary(tmp_file) then
cleanup_files(tmp_file)
http.write_json({success = false, message = translate("Not a valid EasyTier program or architecture mismatch")})
return
end
local core_path = uci:get_first("easytier", "easytier", "easytierbin") or "/usr/bin/easytier-core"
local web_path = uci:get_first("easytier", "easytier", "webbin") or "/usr/bin/easytier-web"
local final_path
if filename == "easytier-web-embed" then
final_path = web_path
elseif filename == "easytier-core" then
final_path = core_path
elseif filename == "easytier-cli" then
local core_dir = core_path:match("(.*/)")
final_path = (core_dir or "/usr/bin/") .. "easytier-cli"
end
os.execute("rm -f " .. final_path)
if os.execute("mv " .. tmp_file .. " " .. final_path) ~= 0 then
cleanup_files(tmp_file)
http.write_json({success = false, message = translate("Failed to move file. Insufficient space or permission denied")})
return
end
os.execute("chmod 755 " .. final_path)
if not test_binary(final_path) then
os.execute("rm -f " .. final_path)
http.write_json({success = false, message = translate("Not a valid EasyTier program or architecture mismatch")})
return
end
nixio.fs.remove("/tmp/easytier.tag")
nixio.fs.remove("/tmp/easytierweb.tag")
http.write_json({success = true, message = translate("Binary uploaded successfully to") .. " " .. final_path})
end
end
function get_wlog()
local log = ""
local files = {"/tmp/easytierweb.log"}
for i, file in ipairs(files) do
if luci.sys.call("[ -f '" .. file .. "' ]") == 0 then
log = log .. luci.sys.exec("sed 's/\\x1b\\[[0-9;]*m//g' " .. file)
end
end
luci.http.write(log)
end
function clear_wlog()
luci.sys.call("echo '' >/tmp/easytierweb.log")
end
function clear_version_cache()
local type = luci.http.formvalue("type")
if type == "core" then
luci.sys.call("rm -f /tmp/easytiernew.tag /tmp/easytier.tag")
elseif type == "web" then
luci.sys.call("rm -f /tmp/easytiernew.tag /tmp/easytierweb.tag")
end
luci.http.write("OK")
end
function act_conninfo()
local e = {}
local uci = require "luci.model.uci".cursor()
local easytierbin = uci:get_first("easytier", "easytier", "easytierbin") or "/usr/bin/easytier-core"
local clibin = easytierbin:gsub("easytier%-core$", "easytier-cli")
local process_status = luci.sys.exec("pgrep easytier-core")
if process_status ~= "" then
-- 获取各类CLI信息
local function get_cli_output(cmd)
local handle = io.popen(clibin .. " " .. cmd .. " 2>&1")
if handle then
local result = handle:read("*all")
handle:close()
return result or ""
end
return ""
end
e.node = get_cli_output("node")
e.peer = get_cli_output("peer")
e.connector = get_cli_output("connector")
e.stun = get_cli_output("stun")
e.route = get_cli_output("route")
e.peer_center = get_cli_output("peer-center")
e.vpn_portal = get_cli_output("vpn-portal")
e.proxy = get_cli_output("proxy")
e.acl = get_cli_output("acl stats")
e.mapped_listener = get_cli_output("mapped-listener")
e.stats = get_cli_output("stats")
-- 获取启动参数
local cmdhandle = io.popen("cat /proc/$(pidof easytier-core)/cmdline 2>/dev/null | tr '\\0' ' '")
if cmdhandle then
e.cmdline = cmdhandle:read("*all") or ""
cmdhandle:close()
else
e.cmdline = ""
end
-- 检查是否使用配置文件启动
if e.cmdline:match("%-%-config%-file") or e.cmdline:match("%-c%s+/") then
e.config_file = safe_read_file("/etc/easytier/config.toml") or ""
else
e.config_file = ""
end
else
local errMsg = i18n.translate("Error: Program not running! Please start the program and refresh")
e.node = errMsg
e.peer = errMsg
e.connector = errMsg
e.stun = errMsg
e.route = errMsg
e.peer_center = errMsg
e.vpn_portal = errMsg
e.proxy = errMsg
e.acl = errMsg
e.mapped_listener = errMsg
e.stats = errMsg
e.cmdline = errMsg
end
luci.http.prepare_content("application/json")
luci.http.write_json(e)
end
function get_web_config()
luci.http.prepare_content("application/json")
local uci = require "luci.model.uci".cursor()
local config = {
web_enabled = uci:get_first("easytier", "easytier", "web_enabled") or "0",
web_db_path = uci:get_first("easytier", "easytier", "web_db_path") or "/etc/easytier/et.db",
web_protocol = uci:get_first("easytier", "easytier", "web_protocol") or "udp",
web_port = uci:get_first("easytier", "easytier", "web_port") or "22020",
web_fw_web = uci:get_first("easytier", "easytier", "web_fw_web") or "0",
web_api_port = uci:get_first("easytier", "easytier", "web_api_port") or "11211",
web_html_port = uci:get_first("easytier", "easytier", "web_html_port") or "11211",
web_api_addr = uci:get_first("easytier", "easytier", "web_api_addr") or "0.0.0.0",
web_html_addr = uci:get_first("easytier", "easytier", "web_html_addr") or "0.0.0.0",
web_fw_api = uci:get_first("easytier", "easytier", "web_fw_api") or "0",
web_api_host = uci:get_first("easytier", "easytier", "web_api_host") or "",
web_geoip_db = uci:get_first("easytier", "easytier", "web_geoip_db") or "",
web_disable_registration = uci:get_first("easytier", "easytier", "web_disable_registration") or "0",
web_allow_auto_create_user = uci:get_first("easytier", "easytier", "web_allow_auto_create_user") or "0",
web_weblog = uci:get_first("easytier", "easytier", "web_weblog") or "off",
-- OIDC配置
web_oidc_enabled = uci:get_first("easytier", "easytier", "web_oidc_enabled") or "0",
web_oidc_issuer_url = uci:get_first("easytier", "easytier", "web_oidc_issuer_url") or "",
web_oidc_client_id = uci:get_first("easytier", "easytier", "web_oidc_client_id") or "",
web_oidc_client_secret = uci:get_first("easytier", "easytier", "web_oidc_client_secret") or "",
web_oidc_redirect_url = uci:get_first("easytier", "easytier", "web_oidc_redirect_url") or "",
web_oidc_username_claim = uci:get_first("easytier", "easytier", "web_oidc_username_claim") or "preferred_username",
web_oidc_scopes = uci:get_first("easytier", "easytier", "web_oidc_scopes") or "openid profile",
web_oidc_frontend_base_url = uci:get_first("easytier", "easytier", "web_oidc_frontend_base_url") or "",
web_oidc_disable_pkce = uci:get_first("easytier", "easytier", "web_oidc_disable_pkce") or "0",
-- Webhook配置
web_webhook_enabled = uci:get_first("easytier", "easytier", "web_webhook_enabled") or "0",
web_webhook_url = uci:get_first("easytier", "easytier", "web_webhook_url") or "",
web_webhook_secret = uci:get_first("easytier", "easytier", "web_webhook_secret") or "",
web_internal_auth_token = uci:get_first("easytier", "easytier", "web_internal_auth_token") or "",
web_web_instance_id = uci:get_first("easytier", "easytier", "web_web_instance_id") or "",
web_web_instance_api_base_url = uci:get_first("easytier", "easytier", "web_web_instance_api_base_url") or ""
}
luci.http.write_json(config)
end
function save_web_config()
local uci = require "luci.model.uci".cursor()
local http = require "luci.http"
local web_enabled = http.formvalue("web_enabled") or "0"
local web_db_path = http.formvalue("web_db_path") or "/etc/easytier/et.db"
local web_protocol = http.formvalue("web_protocol") or "udp"
local web_port = http.formvalue("web_port") or "22020"
local web_fw_web = http.formvalue("web_fw_web") or "0"
local web_api_port = http.formvalue("web_api_port") or "11211"
local web_html_port = http.formvalue("web_html_port") or "11211"
local web_api_addr = http.formvalue("web_api_addr") or "0.0.0.0"
local web_html_addr = http.formvalue("web_html_addr") or "0.0.0.0"
local web_fw_api = http.formvalue("web_fw_api") or "0"
local web_api_host = http.formvalue("web_api_host") or ""
local web_geoip_db = http.formvalue("web_geoip_db") or ""
local web_disable_registration = http.formvalue("web_disable_registration") or "0"
local web_allow_auto_create_user = http.formvalue("web_allow_auto_create_user") or "0"
local web_weblog = http.formvalue("web_weblog") or "off"
-- OIDC
local web_oidc_enabled = http.formvalue("web_oidc_enabled") or "0"
local web_oidc_issuer_url = http.formvalue("web_oidc_issuer_url") or ""
local web_oidc_client_id = http.formvalue("web_oidc_client_id") or ""
local web_oidc_client_secret = http.formvalue("web_oidc_client_secret") or ""
local web_oidc_redirect_url = http.formvalue("web_oidc_redirect_url") or ""
local web_oidc_username_claim = http.formvalue("web_oidc_username_claim") or "preferred_username"
local web_oidc_scopes = http.formvalue("web_oidc_scopes") or "openid profile"
local web_oidc_frontend_base_url = http.formvalue("web_oidc_frontend_base_url") or ""
local web_oidc_disable_pkce = http.formvalue("web_oidc_disable_pkce") or "0"
-- Webhook
local web_webhook_enabled = http.formvalue("web_webhook_enabled") or "0"
local web_webhook_url = http.formvalue("web_webhook_url") or ""
local web_webhook_secret = http.formvalue("web_webhook_secret") or ""
local web_internal_auth_token = http.formvalue("web_internal_auth_token") or ""
local web_web_instance_id = http.formvalue("web_web_instance_id") or ""
local web_web_instance_api_base_url = http.formvalue("web_web_instance_api_base_url") or ""
uci:set("easytier", "@easytier[0]", "web_enabled", web_enabled)
uci:set("easytier", "@easytier[0]", "web_db_path", web_db_path)
uci:set("easytier", "@easytier[0]", "web_protocol", web_protocol)
uci:set("easytier", "@easytier[0]", "web_port", web_port)
uci:set("easytier", "@easytier[0]", "web_fw_web", web_fw_web)
uci:set("easytier", "@easytier[0]", "web_api_port", web_api_port)
uci:set("easytier", "@easytier[0]", "web_html_port", web_html_port)
uci:set("easytier", "@easytier[0]", "web_api_addr", web_api_addr)
uci:set("easytier", "@easytier[0]", "web_html_addr", web_html_addr)
uci:set("easytier", "@easytier[0]", "web_fw_api", web_fw_api)
uci:set("easytier", "@easytier[0]", "web_api_host", web_api_host)
uci:set("easytier", "@easytier[0]", "web_geoip_db", web_geoip_db)
uci:set("easytier", "@easytier[0]", "web_disable_registration", web_disable_registration)
uci:set("easytier", "@easytier[0]", "web_allow_auto_create_user", web_allow_auto_create_user)
uci:set("easytier", "@easytier[0]", "web_weblog", web_weblog)
-- OIDC
uci:set("easytier", "@easytier[0]", "web_oidc_enabled", web_oidc_enabled)
uci:set("easytier", "@easytier[0]", "web_oidc_issuer_url", web_oidc_issuer_url)
uci:set("easytier", "@easytier[0]", "web_oidc_client_id", web_oidc_client_id)
uci:set("easytier", "@easytier[0]", "web_oidc_client_secret", web_oidc_client_secret)
uci:set("easytier", "@easytier[0]", "web_oidc_redirect_url", web_oidc_redirect_url)
uci:set("easytier", "@easytier[0]", "web_oidc_username_claim", web_oidc_username_claim)
uci:set("easytier", "@easytier[0]", "web_oidc_scopes", web_oidc_scopes)
uci:set("easytier", "@easytier[0]", "web_oidc_frontend_base_url", web_oidc_frontend_base_url)
uci:set("easytier", "@easytier[0]", "web_oidc_disable_pkce", web_oidc_disable_pkce)
-- Webhook
uci:set("easytier", "@easytier[0]", "web_webhook_enabled", web_webhook_enabled)
uci:set("easytier", "@easytier[0]", "web_webhook_url", web_webhook_url)
uci:set("easytier", "@easytier[0]", "web_webhook_secret", web_webhook_secret)
uci:set("easytier", "@easytier[0]", "web_internal_auth_token", web_internal_auth_token)
uci:set("easytier", "@easytier[0]", "web_web_instance_id", web_web_instance_id)
uci:set("easytier", "@easytier[0]", "web_web_instance_api_base_url", web_web_instance_api_base_url)
uci:commit("easytier")
http.prepare_content("application/json")
http.write_json({success = true})
end
function reset_database()
luci.http.prepare_content("application/json")
local json = require "luci.jsonc"
local nixio = require "nixio"
-- 从请求中获取数据库路径
local req_data = json.parse(luci.http.content())
if not req_data or not req_data.db_path then
luci.http.write_json({success = false, message = "Missing db_path parameter"})
return
end
local db_path = req_data.db_path
-- 安全检查:路径必须是绝对路径且不能是根目录或系统关键目录
if not db_path:match("^/") or db_path == "/" or
db_path:match("^/bin") or db_path:match("^/sbin") or
db_path:match("^/usr/bin") or db_path:match("^/usr/sbin") or
db_path:match("^/lib") or db_path:match("^/boot") then
luci.http.write_json({success = false, message = "Invalid database path"})
return
end
-- 构建要删除的文件列表
local files_to_delete = {
db_path,
db_path .. "-wal",
db_path .. "-shm"
}
local deleted_files = {}
-- 删除文件
for _, file in ipairs(files_to_delete) do
if nixio.fs.access(file) then
local result = os.remove(file)
if result then
table.insert(deleted_files, file)
end
end
end
if #deleted_files > 0 then
luci.http.write_json({
success = true,
deleted_files = deleted_files,
message = "Database reset successfully"
})
else
luci.http.write_json({
success = false,
message = "No database files found or deletion failed"
})
end
end
function check_web_status()
local running = luci.sys.call("pgrep easytier-web >/dev/null") == 0
local uci = require "luci.model.uci".cursor()
local port = uci:get_first("easytier", "easytier", "web_html_port") or "11211"
luci.http.prepare_content("application/json")
luci.http.write_json({
running = running,
port = port
})
end
function restart_service()
luci.http.prepare_content("application/json")
luci.sys.exec("/etc/init.d/easytier restart >/dev/null 2>&1 &")
luci.http.write_json({success = true})
end
function toggle_core()
local enabled = luci.http.formvalue("enabled")
local uci = require "luci.model.uci".cursor()
uci:set("easytier", uci:get_first("easytier", "easytier"), "enabled", enabled)
uci:commit("easytier")
if enabled == "1" then
luci.sys.exec("/etc/init.d/easytier start >/dev/null 2>&1 &")
else
luci.sys.exec("/etc/init.d/easytier restart >/dev/null 2>&1 &")
end
luci.http.prepare_content("application/json")
luci.http.write_json({success = true})
end
function toggle_web()
local enabled = luci.http.formvalue("enabled")
local uci = require "luci.model.uci".cursor()
uci:set("easytier", uci:get_first("easytier", "easytier"), "web_enabled", enabled)
uci:commit("easytier")
if enabled == "1" then
luci.sys.exec("/etc/init.d/easytier start >/dev/null 2>&1 &")
else
luci.sys.exec("/etc/init.d/easytier restart >/dev/null 2>&1 &")
end
luci.http.prepare_content("application/json")
luci.http.write_json({success = true})
end
-- 检测CPU架构
local function detect_arch()
local cputype = safe_exec("uname -ms | tr ' ' '_' | tr '[A-Z]' '[a-z]'")
local cpucore = ""
if cputype:match("linux.*armv.*") then
cpucore = "arm"
end
if cputype:match("linux.*armv7.*") and safe_exec("cat /proc/cpuinfo | grep vfp") ~= "" then
cpucore = "armv7"
end
if cputype:match("linux.*aarch64.*") or cputype:match("linux.*armv8.*") then
cpucore = "aarch64"
end
if cputype:match("linux.*86.*") then
cpucore = "i386"
end
if cputype:match("linux.*86_64.*") then
cpucore = "x86_64"
end
if cputype:match("linux.*mips.*") then
local mipstype = safe_exec("echo -n I | hexdump -o 2>/dev/null | awk '{ print substr($2,6,1); exit}'")
if mipstype == "0" then
cpucore = "mips"
else
cpucore = "mipsel"
end
end
return cpucore
end
-- 检查依赖
local function check_dependencies()
local arch = detect_arch()
if arch == "" then
return false, i18n.translate("Unable to detect CPU architecture")
end
if arch == "i386" then
return false, i18n.translate("No x86-32bit program available")
end
if safe_exec("which unzip") == "" then
return false, i18n.translate("System lacks unzip, cannot download and extract")
end
return true, arch
end
-- 获取GitHub镜像列表
local function get_github_proxies()
local uci = require "luci.model.uci".cursor()
local proxies = {}
-- 从UCI配置读取代理列表
local proxy_list = uci:get("easytier", "@easytier[0]", "github_proxys")
if proxy_list then
if type(proxy_list) == "table" then
-- 多个代理
for _, proxy in ipairs(proxy_list) do
if proxy and proxy ~= "" then
table.insert(proxies, proxy)
end
end
else
-- 单个代理
if proxy_list ~= "" then
table.insert(proxies, proxy_list)
end
end
end
-- 如果没有配置代理,使用默认值
if #proxies == 0 then
proxies = {
"https://ghproxy.net/",
"https://gh-proxy.com/",
"https://cdn.gh-proxy.com/",
"https://ghfast.top/"
}
end
-- 添加不使用代理的选项(空字符串表示直连)
table.insert(proxies, "")
return proxies
end
-- 下载文件
local function download_file(url, output_path, progress_callback)
local download_tools = {"curl", "wget"}
for _, tool in ipairs(download_tools) do
if safe_exec("which " .. tool) ~= "" then
local cmd = ""
if tool == "curl" then
cmd = string.format("curl -L -k --connect-timeout 30 --max-time 300 -o '%s' '%s'", output_path, url)
elseif tool == "wget" then
cmd = string.format("wget --no-check-certificate --timeout=30 --tries=3 -O '%s' '%s'", output_path, url)
end
if progress_callback then
progress_callback(50, i18n.translate("Downloading with") .. " " .. tool .. "...")
end
local result = os.execute(cmd)
if result == 0 and nixio.fs.access(output_path) then
-- 检查文件大小,确保下载完整
local stat = nixio.fs.stat(output_path)
if stat and stat.size > 3 * 1024 * 1024 then -- 至少3MB
return true
end
end
end
end
return false
end
function download_easytier()
luci.http.prepare_content("application/json")
local json = require "luci.jsonc"
local progress_file = "/tmp/easytier_download_progress"
-- 检查是否已有下载任务
local existing_progress = safe_read_file(progress_file)
if existing_progress then
local progress_data = json.parse(existing_progress)
if progress_data and progress_data.progress > 0 and progress_data.progress < 100 and not progress_data.error then
luci.http.write_json({
success = true,
progress = progress_data.progress,
message = i18n.translate("Download task already exists"),
url = progress_data.url or ""
})
return
end
end
local req_data = json.parse(luci.http.content())
if not req_data or not req_data.version then
luci.http.write_json({success = false, message = i18n.translate("Missing version parameter")})
return
end
local version = req_data.version
-- 1. 检查依赖和架构
local ok, arch_or_error = check_dependencies()
if not ok then
luci.http.write_json({success = false, message = arch_or_error})
return
end
local arch = arch_or_error
local proxies = get_github_proxies()
local download_dir = "/tmp/easytier_download"
local zip_file = download_dir .. "/easytier-linux-" .. arch .. "-" .. version .. ".zip"
-- 创建下载目录
os.execute("mkdir -p " .. download_dir)
-- 创建取消标志文件
local cancel_file = "/tmp/easytier_download_cancel"
os.execute("rm -f " .. cancel_file)
-- 创建进度文件标记任务开始
local f = io.open(progress_file, "w")
if f then
f:write(json.stringify({
progress = 1,
message = "Downloading...",
url = "",
error = false
}))
f:close()
end
-- 检查是否被取消
local function check_cancelled()
return nixio.fs.access(cancel_file)
end
-- 2. 循环尝试不同的镜像地址下载、解压、验证
local download_success = false
local download_url = ""
local core_file, cli_file, web_file
for _, proxy in ipairs(proxies) do
-- 检查是否被取消
if check_cancelled() then
os.execute("rm -rf " .. download_dir)
os.execute("rm -f " .. progress_file)
luci.http.write_json({success = false, message = i18n.translate("Download cancelled")})
return
end
download_url = proxy .. "https://github.com/EasyTier/EasyTier/releases/download/" .. version .. "/easytier-linux-" .. arch .. "-" .. version .. ".zip"
-- 删除之前的失败文件
os.execute("rm -f " .. zip_file)
os.execute("rm -rf " .. download_dir .. "/extracted")
-- 下载
if download_file(download_url, zip_file) then
local stat = nixio.fs.stat(zip_file)
if stat and stat.size > 10240 then
-- 检查是否被取消
if check_cancelled() then
os.execute("rm -rf " .. download_dir)
os.execute("rm -f " .. progress_file)
luci.http.write_json({success = false, message = i18n.translate("Download cancelled")})
return
end
-- 解压
local extract_dir = download_dir .. "/extracted"
os.execute("mkdir -p " .. extract_dir)
local unzip_result = os.execute("cd " .. extract_dir .. " && unzip -o " .. zip_file .. " >/dev/null 2>&1")
if unzip_result == 0 then
-- 检查是否被取消
if check_cancelled() then
os.execute("rm -rf " .. download_dir)
os.execute("rm -f " .. progress_file)
luci.http.write_json({success = false, message = i18n.translate("Download cancelled")})
return
end
-- 查找解压后的目录(使用通配符)
local find_cmd = "find " .. extract_dir .. " -maxdepth 1 -type d -name 'easytier-linux-*' 2>/dev/null | head -1"
local handle = io.popen(find_cmd)
local subdir = handle:read("*a"):match("^%s*(.-)%s*$")
handle:close()
if subdir and subdir ~= "" then
core_file = subdir .. "/easytier-core"
cli_file = subdir .. "/easytier-cli"
web_file = subdir .. "/easytier-web-embed"
local files_ok = nixio.fs.access(core_file) and nixio.fs.access(cli_file)
if arch ~= "mips" and arch ~= "mipsel" then
files_ok = files_ok and nixio.fs.access(web_file)
end
if files_ok then
-- 检查是否被取消
if check_cancelled() then
os.execute("rm -rf " .. download_dir)
os.execute("rm -f " .. progress_file)
luci.http.write_json({success = false, message = i18n.translate("Download cancelled")})
return
end
-- 先赋予执行权限,再测试程序
local test_files = {core_file, cli_file}
if arch ~= "mips" and arch ~= "mipsel" then
table.insert(test_files, web_file)
end
local test_ok = true
for _, file in ipairs(test_files) do
os.execute("chmod +x " .. file)
if not test_binary(file) then
test_ok = false
break
end
end
if test_ok then
download_success = true
break
end
end
end
end
end
end
-- 删除失败的文件,继续下一个地址
os.execute("rm -f " .. zip_file)
end
if not download_success then
os.execute("rm -rf " .. download_dir)
os.execute("rm -f " .. progress_file)
luci.http.write_json({success = false, message = i18n.translate("All download attempts failed")})
return
end
-- 检查是否被取消(替换前)
if check_cancelled() then
os.execute("rm -rf " .. download_dir)
os.execute("rm -f " .. progress_file)
luci.http.write_json({success = false, message = i18n.translate("Download cancelled")})
return
end
-- 3. 从UCI获取路径并安装
local uci = require "luci.model.uci".cursor()
local easytierbin = uci:get_first("easytier", "easytier", "easytierbin") or "/usr/bin/easytier-core"
local easytierwebbin = uci:get_first("easytier", "easytier", "webbin") or "/usr/bin/easytier-web"
local core_dir = easytierbin:match("^(.*/)[^/]+$") or "/usr/bin/"
local programs = {
{src = core_file, dest = easytierbin},
{src = cli_file, dest = core_dir .. "easytier-cli"}
}
if arch ~= "mips" and arch ~= "mipsel" then
table.insert(programs, {src = web_file, dest = easytierwebbin})
end
-- 安装并验证
for _, prog in ipairs(programs) do
local result = os.execute("cp " .. prog.src .. " " .. prog.dest .. " 2>/dev/null")
if result ~= 0 then
os.execute("rm -rf " .. download_dir)
os.execute("rm -f " .. progress_file)
luci.http.write_json({success = false, message = i18n.translate("Failed to install program") .. ": " .. prog.dest})
return
end
os.execute("chmod +x " .. prog.dest)
-- 再次验证安装后的程序
if not test_binary(prog.dest) then
os.execute("rm -rf " .. download_dir)
os.execute("rm -f " .. progress_file)
luci.http.write_json({success = false, message = i18n.translate("Program is incomplete or architecture mismatch")})
return
end
end
-- 7. 清理临时文件和版本缓存
os.execute("rm -rf " .. download_dir)
os.execute("rm -f " .. cancel_file)
os.execute("rm -f " .. progress_file)
nixio.fs.remove("/tmp/easytier.tag")
nixio.fs.remove("/tmp/easytierweb.tag")
luci.http.write_json({
success = true,
progress = 100,
message = i18n.translate("Download and installation completed")
})
end
function download_progress()
luci.http.prepare_content("application/json")
local progress_file = "/tmp/easytier_download_progress"
local content = safe_read_file(progress_file)
if content then
local json = require "luci.jsonc"
local progress_data = json.parse(content)
if progress_data then
luci.http.write_json({
success = true,
progress = progress_data.progress or 0,
message = progress_data.message or "",
url = progress_data.url or "",
error = progress_data.error or false
})
return
end
end
luci.http.write_json({
success = true,
progress = 0,
message = i18n.translate("Preparing..."),
url = "",
error = false
})
end
function cancel_download()
luci.http.prepare_content("application/json")
-- 创建取消标志文件
os.execute("touch /tmp/easytier_download_cancel")
luci.http.write_json({success = true})
end