🍓 Sync 2026-04-16 20:36:17

This commit is contained in:
github-actions[bot] committed 2026-04-16 20:36:17 +08:00
1 parent ea416c7ab6
commit aa40804205
24 files changed
+6145 -969

No files matched your search

+9 -5
View File
@@ -2,8 +2,8 @@ include $(TOPDIR)/rules.mk
-include $(dir $(lastword $(MAKEFILE_LIST)))../version.mk
PKG_NAME:=easytier
PKG_VERSION:=$(or $(EASYTIER_VERSION),2.5.0)
PKG_RELEASE:=1
PKG_VERSION:=$(or $(EASYTIER_VERSION),2.6.0)
PKG_RELEASE:=2
ifeq ($(ARCH),mipsel)
APP_ARCH:=mipsel
@@ -54,15 +54,19 @@ define Package/$(PKG_NAME)/config
config EASYTIER_INCLUDE_WEBCONSOLE
bool "Include Web Console (easytier-web)"
depends on PACKAGE_$(PKG_NAME)
depends on !@(mips||mipsel)
depends on !(mips || mipsel)
default y
help
Install the easytier-web web console.
endef
define Build/Prepare
[ ! -f $(PKG_BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION).zip ] && wget https://github.com/EasyTier/EasyTier/releases/download/v$(PKG_VERSION)/$(PKG_NAME)-linux-$(APP_ARCH)-v$(PKG_VERSION).zip -O $(PKG_NAME)-$(PKG_VERSION).zip
unzip -j $(PKG_NAME)-$(PKG_VERSION).zip -d $(PKG_BUILD_DIR)
mkdir -p $(PKG_BUILD_DIR)
if [ ! -f $(PKG_BUILD_DIR)/easytier-core ]; then \
wget https://github.com/EasyTier/EasyTier/releases/download/v$(PKG_VERSION)/$(PKG_NAME)-linux-$(APP_ARCH)-v$(PKG_VERSION).zip -O $(PKG_BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION).zip; \
unzip -o -j $(PKG_BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION).zip -d $(PKG_BUILD_DIR); \
rm -f $(PKG_BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION).zip; \
fi
endef
define Build/Compile
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=hickory-dns
PKG_VERSION:=0.26.0-beta.4
PKG_RELEASE:=14
PKG_VERSION:=0.26.0-beta.2
PKG_RELEASE:=15
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/hickory-dns/hickory-dns/tar.gz/v$(PKG_VERSION)?
+2 -5
View File
@@ -7,8 +7,8 @@
include $(TOPDIR)/rules.mk
-include $(dir $(lastword $(MAKEFILE_LIST)))../version.mk
PKG_VERSION:=$(or $(EASYTIER_VERSION),2.5.0)
PKG_RELEASE:=1
PKG_VERSION:=$(or $(EASYTIER_VERSION),2.6.0)
PKG_RELEASE:=2
LUCI_TITLE:=LuCI support for EasyTier
LUCI_DEPENDS:=+kmod-tun +easytier +luci-compat
@@ -54,9 +54,6 @@ if [ -d /tmp/et_config_backup ] ; then
fi
endef
$(eval $(call Build/Template,Package/$(PKG_NAME)/prerm))
$(eval $(call Build/Template,Package/$(PKG_NAME)/postinst))
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
+554 -10
View File
@@ -1,5 +1,6 @@
module("luci.controller.easytier", package.seeall)
local i18n = require "luci.i18n"
-- 安全执行命令并返回结果
local function safe_exec(cmd)
@@ -45,17 +46,34 @@ function index()
if not nixio.fs.access("/etc/config/easytier") then
return
end
entry({"admin", "vpn", "easytier"}, alias("admin", "vpn", "easytier", "easytier"),_("EasyTier"), 46).dependent = true
entry({"admin", "vpn", "easytier", "easytier"}, cbi("easytier"),_("EasyTier"), 47).leaf = true
entry({"admin", "vpn", "easytier", "easytier_log"}, form("easytier_log"),_("core log"), 48).leaf = true
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", "easytierweb_log"}, form("easytierweb_log"),_("web log"), 49).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", "status"}, call("act_status")).leaf = true
entry({"admin", "vpn", "easytier", "conninfo"}, call("act_conninfo")).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", "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
end
function act_status()
@@ -66,6 +84,8 @@ function act_status()
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")
@@ -105,16 +125,229 @@ function act_status()
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 | sed 's/^[^0-9]*//'")
if e.ettag == "" then e.ettag = "?" end
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", "easytierwebbin") 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", "easytierwebbin") or "/usr/bin/easytier-web",
github_proxys = {},
fallback_version = uci:get_first("easytier", "easytier", "fallback_version") or "v2.6.0"
}
-- 读取代理列表(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]", "easytierwebbin", data.webbin or "/usr/bin/easytier-web")
uci:set("easytier", "@easytier[0]", "fallback_version", data.fallback_version or "v2.6.0")
-- 删除旧的代理列表
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 = ""
@@ -127,10 +360,212 @@ function get_log()
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", "easytierwebbin") 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", "easytierwebbin") 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"}
@@ -146,6 +581,16 @@ 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()
@@ -186,8 +631,15 @@ function act_conninfo()
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 = "错误:程序未运行!请启动程序后刷新"
local errMsg = i18n.translate("Error: Program not running! Please start the program and refresh")
e.node = errMsg
e.peer = errMsg
e.connector = errMsg
@@ -205,3 +657,95 @@ function act_conninfo()
luci.http.prepare_content("application/json")
luci.http.write_json(e)
end
function get_web_config()
local uci = require "luci.model.uci".cursor()
local config = {}
config.web_enabled = uci:get_first("easytier", "easytier", "web_enabled") or "0"
config.web_db_path = uci:get_first("easytier", "easytier", "web_db_path") or "/etc/easytier/et.db"
config.web_protocol = uci:get_first("easytier", "easytier", "web_protocol") or "udp"
config.web_port = uci:get_first("easytier", "easytier", "web_port") or "22020"
config.web_fw_web = uci:get_first("easytier", "easytier", "web_fw_web") or "0"
config.web_api_port = uci:get_first("easytier", "easytier", "web_api_port") or "11211"
config.web_html_port = uci:get_first("easytier", "easytier", "web_html_port") or "11211"
config.web_fw_api = uci:get_first("easytier", "easytier", "web_fw_api") or "0"
config.web_api_host = uci:get_first("easytier", "easytier", "web_api_host") or ""
config.web_geoip_db = uci:get_first("easytier", "easytier", "web_geoip_db") or ""
config.web_weblog = uci:get_first("easytier", "easytier", "web_weblog") or "off"
luci.http.prepare_content("application/json")
luci.http.write_json(config)
end
function save_web_config()
local uci = require "luci.model.uci".cursor()
uci:set("easytier", "@easytier[0]", "web_enabled", luci.http.formvalue("web_enabled") or "0")
uci:set("easytier", "@easytier[0]", "web_db_path", luci.http.formvalue("web_db_path") or "/etc/easytier/et.db")
uci:set("easytier", "@easytier[0]", "web_protocol", luci.http.formvalue("web_protocol") or "udp")
uci:set("easytier", "@easytier[0]", "web_port", luci.http.formvalue("web_port") or "22020")
uci:set("easytier", "@easytier[0]", "web_fw_web", luci.http.formvalue("web_fw_web") or "0")
uci:set("easytier", "@easytier[0]", "web_api_port", luci.http.formvalue("web_api_port") or "11211")
uci:set("easytier", "@easytier[0]", "web_html_port", luci.http.formvalue("web_html_port") or "11211")
uci:set("easytier", "@easytier[0]", "web_fw_api", luci.http.formvalue("web_fw_api") or "0")
uci:set("easytier", "@easytier[0]", "web_api_host", luci.http.formvalue("web_api_host") or "")
uci:set("easytier", "@easytier[0]", "web_geoip_db", luci.http.formvalue("web_geoip_db") or "")
uci:set("easytier", "@easytier[0]", "web_weblog", luci.http.formvalue("web_weblog") or "off")
uci:commit("easytier")
luci.sys.exec("/etc/init.d/easytier restart >/dev/null 2>&1 &")
luci.http.prepare_content("application/json")
luci.http.write_json({success = true})
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
+3 -212
View File
@@ -2,36 +2,17 @@ local http = luci.http
local nixio = require "nixio"
m = Map("easytier")
m.description = translate("A simple, secure, decentralized VPN solution for intranet penetration, implemented in Rust using the Tokio framework. "
.. "Project URL: <a href=\"https://github.com/EasyTier/EasyTier\" target=\"_blank\">github.com/EasyTier/EasyTier</a>&nbsp;&nbsp;"
.. "<a href=\"http://easytier.cn\" target=\"_blank\">Official Documentation</a>&nbsp;&nbsp;"
.. "<a href=\"http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=jhP2Z4UsEZ8wvfGPLrs0VwLKn_uz0Q_p&authKey=OGKSQLfg61YPCpVQuvx%2BxE7hUKBVBEVi9PljrDKbHlle6xqOXx8sOwPPTncMambK&noverify=0&group_code=949700262\" target=\"_blank\">QQ Group</a>&nbsp;&nbsp;")
m:section(SimpleSection).template = "easytier/easytier_status"
-- easytier-core
s=m:section(TypedSection, "easytier", translate("EasyTier Configuration"))
s=m:section(TypedSection, "easytier")
s.addremove=false
s.anonymous=true
s:tab("general", translate("General Settings"))
s:tab("privacy", translate("Advanced Settings"))
s:tab("webconsole", translate("Self-hosted Web Server"))
s:tab("infos", translate("Connection Info"))
s:tab("upload", translate("Upload Program"))
switch = s:taboption("general",Flag, "enabled", translate("Enable"))
switch.rmempty = false
btncq = s:taboption("general", Button, "btncq", translate("Restart"))
btncq.inputtitle = translate("Restart")
btncq.description = translate("Quickly restart once without modifying any parameters")
btncq.inputstyle = "apply"
btncq:depends("enabled", "1")
btncq.write = function()
luci.sys.call("rm -rf /tmp/easytier*.tag /tmp/easytier*.newtag >/dev/null 2>&1 &") -- 执行删除版本号信息
luci.sys.call("/etc/init.d/easytier restart >/dev/null 2>&1 &") -- 执行重启命令
end
etcmd = s:taboption("general", ListValue, "etcmd", translate("Startup Method"),
translate("Official Web Console: <a href=\"https://easytier.cn/web\" target=\"_blank\">https://easytier.cn/web</a><br>"
.. "Official Configuration File Generator: <a href=\"https://easytier.cn/web/index.html#/config_generator\" target=\"_blank\">"
@@ -113,8 +94,6 @@ peeradd = s:taboption("general", DynamicList, "peeradd", translate("Peer Nodes")
translate("Initial connected peer nodes (-p parameter)<br>"
.. "Public server status check: <a href='https://uptime.easytier.cn' target='_blank'>"
.. "Click here to check</a>"))
peeradd.placeholder = "tcp://public.easytier.top:11010"
peeradd:value("tcp://public.easytier.top:11010", translate("Official Server - tcp://public.easytier.top:11010"))
peeradd:depends("etcmd", "etcmd")
--[=[
@@ -471,8 +450,9 @@ et_forward:value("etfwlan", translate("Allow traffic from EasyTier virtual netwo
et_forward:value("etfwwan", translate("Allow traffic from EasyTier virtual network to WAN"))
et_forward:value("lanfwet", translate("Allow traffic from LAN to EasyTier virtual network"))
et_forward:value("wanfwet", translate("Allow traffic from WAN to EasyTier virtual network"))
et_forward.default = "etfwlan etfwwan lanfwet"
et_forward.rmempty = true
et_forward.optional = true
et_forward:depends("auto_config_firewall", "1")
check = s:taboption("privacy", Flag, "check", translate("Connectivity Check"),
translate("Enable connectivity check to specify remote device IPs; if all specified IPs fail to ping, "
@@ -492,193 +472,4 @@ for s = 1, 60 do
end
checktime:depends("check", "1")
local process_status = luci.sys.exec("ps | grep easytier-core| grep -v grep")
-- 连接信息 tab - 使用 HTM 模板展示
conninfo = s:taboption("infos", DummyValue, "_conninfo")
conninfo.template = "easytier/easytier_conninfo"
conninfo.rawhtml = true
btnrm = s:taboption("infos", Button, "btnrm")
btnrm.inputtitle = translate("Check for Updates")
btnrm.description = translate("Click the button to start checking for updates and refresh the version display in the status bar above")
btnrm.inputstyle = "apply"
btnrm.write = function()
os.execute("rm -rf /tmp/easytier*.tag /tmp/easytier*.newtag /tmp/easytier-core_*")
end
easytierbin = s:taboption("upload", Value, "easytierbin", translate("easytier-core Binary Path"),
translate("Customize the storage path for easytier-core. Make sure to provide the full path and filename. "
.. "If the specified path has insufficient space, it will automatically move to /tmp/easytier-core"))
easytierbin.placeholder = "/usr/bin/easytier-core"
easytierbin.default = "/usr/bin/easytier-core"
webbin = s:taboption("upload", Value, "webbin", translate("easytier-web Binary Path"),
translate("Customize the storage path for easytier-web. Make sure to provide the full path and filename, "
.. "then upload the installer"))
webbin.placeholder = "/usr/bin/easytier-web"
webbin.default = "/usr/bin/easytier-web"
github_proxys = s:taboption("upload", Value, "github_proxys", translate("GitHub Proxy URLs"),
translate("Space-separated list of GitHub proxy URLs for downloading binaries. "
.. "Leave empty to use default proxies."))
github_proxys.placeholder = "https://ghproxy.net/ https://gh-proxy.com/"
fallback_version = s:taboption("upload", Value, "fallback_version", translate("Fallback Version"),
translate("Fallback version to use when unable to fetch the latest version from GitHub."))
fallback_version.placeholder = "v2.5.0"
fallback_version.default = "v2.5.0"
local upload = s:taboption("upload", FileUpload, "upload_file")
upload.optional = true
upload.default = ""
upload.template = "easytier/other_upload"
upload.description = translate("You can directly upload the binary programs easytier-core and easytier-cli, or a compressed .zip archive. "
.. "Uploading a new version will automatically overwrite the old one. Download link: "
.. "<a href='https://github.com/EasyTier/EasyTier/releases' target='_blank'>github.com/EasyTier/EasyTier</a><br>"
.. "The uploaded files will be saved in the /tmp folder. If a custom program path is specified, "
.. "the program will be automatically moved to that path when started.<br>")
local um = s:taboption("upload",DummyValue, "", nil)
um.template = "easytier/other_dvalue"
local dir, fd, chunk
dir = "/tmp/"
nixio.fs.mkdir(dir)
http.setfilehandler(
function(meta, chunk, eof)
if not fd then
if not meta then return end
if meta and chunk then fd = nixio.open(dir .. meta.file, "w") end
if not fd then
um.value = translate("Error: Upload failed!")
return
end
end
if chunk and fd then
fd:write(chunk)
end
if eof and fd then
fd:close()
fd = nil
um.value = translate("File has been uploaded to") .. ' "/tmp/' .. meta.file .. '"'
if string.sub(meta.file, -4) == ".zip" then
local file_path = dir .. meta.file
os.execute("unzip -q " .. file_path .. " -d " .. dir)
local extracted_dir = "/tmp/easytier-linux-*/"
os.execute("mv " .. extracted_dir .. "easytier-cli /tmp/easytier-cli")
os.execute("mv " .. extracted_dir .. "easytier-core /tmp/easytier-core")
os.execute("mv " .. extracted_dir .. "easytier-web-embed /tmp/easytier-web-embed")
if nixio.fs.access("/tmp/easytier-cli") then
um.value = um.value .. "\n" .. translate("- Program /tmp/easytier-cli uploaded successfully, restart the plugin to take effect")
end
if nixio.fs.access("/tmp/easytier-core") then
um.value = um.value .. "\n" .. translate("- Program /tmp/easytier-core uploaded successfully, restart the plugin to take effect")
end
if nixio.fs.access("/tmp/easytier-web-embed") then
um.value = um.value .. "\n" .. translate("- Program /tmp/easytier-web uploaded successfully, restart the plugin to take effect")
end
end
if string.sub(meta.file, -7) == ".tar.gz" then
local file_path = dir .. meta.file
os.execute("tar -xzf " .. file_path .. " -C " .. dir)
local extracted_dir = "/tmp/easytier-linux-*/"
os.execute("mv " .. extracted_dir .. "easytier-cli /tmp/easytier-cli")
os.execute("mv " .. extracted_dir .. "easytier-core /tmp/easytier-core")
os.execute("mv " .. extracted_dir .. "easytier-web-embed /tmp/easytier-web-embed")
if nixio.fs.access("/tmp/easytier-cli") then
um.value = um.value .. "\n" .. translate("- Program /tmp/easytier-cli uploaded successfully, restart the plugin to take effect")
end
if nixio.fs.access("/tmp/easytier-core") then
um.value = um.value .. "\n" .. translate("- Program /tmp/easytier-core uploaded successfully, restart the plugin to take effect")
end
if nixio.fs.access("/tmp/easytier-web-embed") then
um.value = um.value .. "\n" .. translate("- Program /tmp/easytier-web uploaded successfully, restart the plugin to take effect")
end
end
os.execute("chmod +x /tmp/easytier-core")
os.execute("chmod +x /tmp/easytier-cli")
os.execute("chmod +x /tmp/easytier-web-embed")
end
end
)
if luci.http.formvalue("upload") then
local f = luci.http.formvalue("ulfile")
end
-- Self-hosted Web Console tab options
web_enabled = s:taboption("webconsole", Flag, "web_enabled", translate("Enable"))
web_enabled.rmempty = false
web_btncq = s:taboption("webconsole", Button, "web_btncq", translate("Restart"))
web_btncq.inputtitle = translate("Restart")
web_btncq.description = translate("Quickly restart once without modifying any parameters")
web_btncq.inputstyle = "apply"
web_btncq.write = function()
luci.sys.call("/etc/init.d/easytier restart >/dev/null 2>&1 &")
end
web_db_path = s:taboption("webconsole", Value, "web_db_path", translate("Database File Path"),
translate("Path to the sqlite3 database file used to store all data. (-d parameter)"))
web_db_path.default = "/etc/easytier/et.db"
web_protocol = s:taboption("webconsole", ListValue, "web_protocol", translate("Listening Protocol"),
translate("Configure the server's listening protocol for easytier-core to connect. (-p parameter)"))
web_protocol.default = "udp"
web_protocol:value("udp",translate("UDP"))
web_protocol:value("tcp",translate("TCP"))
web_protocol:value("ws",translate("WS"))
web_port = s:taboption("webconsole", Value, "web_port", translate("Server Port"),
translate("Configure the server's listening port for easytier-core to connect. (-c parameter)"))
web_port.datatype = "range(1,65535)"
web_port.placeholder = "22020"
web_port.default = "22020"
web_fw_web = s:taboption("webconsole", Flag, "web_fw_web", translate("WAN access to WEB"),
translate("Automatically add firewall rules to allow WAN access to this WEB console"))
web_api_port = s:taboption("webconsole", Value, "web_api_port", translate("API Port"),
translate("Listening port of the RESTful server, used as ApiHost by the web frontend. (-a parameter)"))
web_api_port.datatype = "range(1,65535)"
web_api_port.placeholder = "11211"
web_api_port.default = "11211"
web_html_port = s:taboption("webconsole", Value, "web_html_port", translate("Web Interface Port"),
translate("Frontend listening port for the web dashboard server. Leave empty to disable. (-l parameter)"))
web_html_port.datatype = "range(1,65535)"
web_html_port.default = "11211"
web_fw_api = s:taboption("webconsole", Flag, "web_fw_api", translate("WAN access to API"),
translate("Automatically add firewall rules to allow WAN access to the API control page"))
web_api_host = s:taboption("webconsole", Value, "web_api_host", translate("Default API Server URL"),
translate("The URL of the API server, used for connecting the web frontend. (--api-host parameter)<br>"
.. "Example: http://[current device IP or resolved domain name]:[API port]"))
web_geoip_db = s:taboption("webconsole", Value, "web_geoip_db", translate("GEOIP_DB Path"),
translate("GeoIP2 database file path used to locate the client. Defaults to an embedded file (country-level information only)."
.. "<br>Recommended: https://github.com/P3TERX/GeoLite.mmdb (--geoip-db parameter)"))
web_geoip_db.placeholder = "/etc/easytier/GeoLite.mmdb"
web_weblog = s:taboption("webconsole", ListValue, "web_weblog", translate("Program Log"),
translate("Runtime log located at /tmp/easytierweb.log, viewable in the log section above.<br>"
.. "Levels: Error < Warning < Info < Debug < Trace"))
web_weblog.default = "off"
web_weblog:value("off", translate("Off"))
web_weblog:value("error", translate("Error"))
web_weblog:value("warn", translate("Warning"))
web_weblog:value("info", translate("Info"))
web_weblog:value("debug", translate("Debug"))
web_weblog:value("trace", translate("Trace"))
return m
@@ -1,5 +0,0 @@
f = SimpleForm("easytier")
f.reset = false
f.submit = false
f:append(Template("easytier/easytier_log"))
return f
@@ -0,0 +1,16 @@
local http = luci.http
m = Map("easytier", translate("EasyTier Status"))
m.description = translate("A simple, secure, decentralized VPN solution for intranet penetration, implemented in Rust using the Tokio framework. "
.. "Project URL: <a href=\"https://github.com/EasyTier/EasyTier\" target=\"_blank\">github.com/EasyTier/EasyTier</a>&nbsp;&nbsp;"
.. "<a href=\"http://easytier.cn\" target=\"_blank\">Official Documentation</a>&nbsp;&nbsp;"
.. "<a href=\"http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=jhP2Z4UsEZ8wvfGPLrs0VwLKn_uz0Q_p&authKey=OGKSQLfg61YPCpVQuvx%2BxE7hUKBVBEVi9PljrDKbHlle6xqOXx8sOwPPTncMambK&noverify=0&group_code=949700262\" target=\"_blank\">QQ Group</a>&nbsp;&nbsp;")
m.pageaction = false
-- 状态卡片
m:section(SimpleSection).template = "easytier/easytier_status"
-- 连接信息卡片
m:section(SimpleSection).template = "easytier/easytier_cli"
return m
@@ -1,5 +0,0 @@
f = SimpleForm("easytier")
f.reset = false
f.submit = false
f:append(Template("easytier/easytierweb_log"))
return f
File diff suppressed because it is too large. Load diff
@@ -1,408 +0,0 @@
<%
local uci = require 'luci.model.uci'.cursor()
%>
<script type="text/javascript">//<![CDATA[
// 中文翻译映射
var translations = {
// 表头翻译
"Virtual IP": "虚拟IP",
"Hostname": "主机名",
"Proxy CIDRs": "代理CIDR",
"Peer ID": "节点ID",
"Public IPv4": "公网IPv4",
"UDP Stun Type": "UDP穿透类型",
"Interface IPv4": "IPv4地址",
"Interface IPv6": "IPv6地址",
"ipv4": "IPv4地址",
"hostname": "主机名",
"cost": "路由",
"lat(ms)": "延迟(ms)",
"loss": "丢包率",
"rx": "下载",
"tx": "上传",
"tunnel": "协议",
"NAT": "NAT类型",
"version": "版本",
"proxy_cidrs": "代理网段",
"next_hop_ipv4": "下一跳IPv4",
"next_hop_hostname": "下一跳主机名",
"next_hop_lat": "下一跳延迟",
"path_len": "路径长度",
"path_latency": "路径延迟",
"node_id": "节点ID",
"direct_peers": "直连节点",
"src": "源地址",
"dst": "目标地址",
"start_time": "启动时间",
"state": "状态",
"transport_type": "传输类型",
"Metric Name": "指标名称",
"Value": "值",
"Labels": "标签",
// 值翻译
"Local": "本机",
"PortRestricted": "端口受限",
"NoPat": "无端口映射",
"DIRECT": "直连"
};
function translate(text) {
return translations[text] || text;
}
function parseCliTable(content, isKeyValue) {
if (!content || content.trim() === '') {
return '<div class="conninfo-empty">暂无数据</div>';
}
if (content.indexOf('错误:') === 0) {
return '<div class="conninfo-error">' + escapeHtml(content) + '</div>';
}
var lines = content.split(/\r?\n/).filter(function(l) { return l.trim() !== ''; });
// 检查是否是表格格式
if (lines.length === 0 || lines[0].indexOf('|') === -1) {
return '<pre class="conninfo-pre">' + escapeHtml(content) + '</pre>';
}
var html = '<div class="conninfo-table-wrap"><table class="conninfo-table">';
var rowIndex = 0;
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
// 跳过分隔行
if (/^\|[\s-]+\|/.test(line)) continue;
var cells = [];
var matches = line.match(/\|([^|]+)/g);
if (matches) {
for (var j = 0; j < matches.length; j++) {
var cell = matches[j].replace(/^\|/, '').trim();
cells.push(cell);
}
}
if (cells.length === 0) continue;
html += '<tr>';
for (var k = 0; k < cells.length; k++) {
var tag = rowIndex === 0 ? 'th' : 'td';
var cellContent = rowIndex === 0 ? translate(cells[k]) : translate(cells[k]);
html += '<' + tag + '>' + escapeHtml(cellContent) + '</' + tag + '>';
}
html += '</tr>';
rowIndex++;
}
html += '</table></div>';
return html;
}
function escapeHtml(text) {
var div = document.createElement('div');
div.appendChild(document.createTextNode(text));
return div.innerHTML;
}
function toggleSection(id) {
var content = document.getElementById('conninfo_content_' + id);
var icon = document.getElementById('conninfo_icon_' + id);
if (content.style.display === 'none') {
content.style.display = 'block';
icon.innerHTML = '▼';
} else {
content.style.display = 'none';
icon.innerHTML = '▶';
}
}
function refreshConnInfo() {
var btn = document.getElementById('conninfo_refresh_btn');
btn.disabled = true;
btn.value = '刷新中...';
XHR.get('<%=url([[admin]], [[vpn]], [[easytier]], [[conninfo]])%>', null,
function(x, data) {
updateConnInfo(data);
btn.disabled = false;
btn.value = '刷新数据';
}
);
}
function updateConnInfo(data) {
if (!data) return;
var sections = [
{id: 'node', title: '节点信息', key: 'node'},
{id: 'peer', title: '节点列表', key: 'peer'},
{id: 'connector', title: '连接器信息', key: 'connector'},
{id: 'stun', title: 'STUN信息', key: 'stun'},
{id: 'route', title: '路由信息', key: 'route'},
{id: 'peercenter', title: '节点中心', key: 'peer_center'},
{id: 'vpnportal', title: 'VPN门户', key: 'vpn_portal'},
{id: 'proxy', title: 'TCP/KCP代理', key: 'proxy'},
{id: 'acl', title: 'ACL规则', key: 'acl'},
{id: 'mappedlistener', title: '映射监听器', key: 'mapped_listener'},
{id: 'stats', title: '统计信息', key: 'stats'},
{id: 'cmdline', title: '启动参数', key: 'cmdline'}
];
for (var i = 0; i < sections.length; i++) {
var sec = sections[i];
var el = document.getElementById('conninfo_content_' + sec.id);
if (el && data[sec.key] !== undefined) {
el.innerHTML = parseCliTable(data[sec.key], sec.id === 'node');
}
}
// 更新时间戳
var ts = document.getElementById('conninfo_timestamp');
if (ts) {
ts.innerHTML = '最后更新: ' + new Date().toLocaleTimeString();
}
}
// 初始加载
document.addEventListener('DOMContentLoaded', function() {
refreshConnInfo();
});
// 自动刷新 (每5秒)
XHR.poll(5, '<%=url([[admin]], [[vpn]], [[easytier]], [[conninfo]])%>', null,
function(x, data) {
updateConnInfo(data);
var ts = document.getElementById('conninfo_timestamp');
if (ts) {
ts.innerHTML = '最后更新: ' + new Date().toLocaleTimeString();
}
}
);
//]]></script>
<style>
.conninfo-container {
padding: 10px 0;
}
.conninfo-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border-color-medium, #ddd);
}
.conninfo-timestamp {
color: var(--text-color-secondary, #666);
font-size: 12px;
}
.conninfo-section {
margin-bottom: 15px;
border: 1px solid var(--border-color-medium, #ddd);
border-radius: 4px;
overflow: hidden;
}
.conninfo-section-header {
background: var(--background-color-high, rgba(128,128,128,0.1));
padding: 10px 15px;
cursor: pointer;
display: flex;
align-items: center;
user-select: none;
}
.conninfo-section-header:hover {
background: var(--background-color-medium, rgba(128,128,128,0.15));
}
.conninfo-section-icon {
margin-right: 10px;
font-size: 12px;
color: var(--text-color-secondary, #666);
}
.conninfo-section-title {
font-weight: bold;
font-size: 14px;
}
.conninfo-section-content {
padding: 15px;
overflow-x: auto;
}
.conninfo-table-wrap {
overflow-x: auto;
}
.conninfo-table {
border-collapse: collapse;
width: 100%;
font-family: monospace;
font-size: 13px;
}
.conninfo-table th {
background: var(--background-color-high, rgba(128,128,128,0.1));
padding: 8px 12px;
text-align: left;
white-space: nowrap;
border: 1px solid var(--border-color-medium, #ddd);
font-weight: bold;
}
.conninfo-table td {
padding: 6px 12px;
text-align: left;
white-space: nowrap;
border: 1px solid var(--border-color-medium, #ddd);
}
.conninfo-table tr:nth-child(even) td {
background: var(--background-color-low, rgba(128,128,128,0.05));
}
.conninfo-table tr:hover td {
background: var(--background-color-medium, rgba(128,128,128,0.15));
}
.conninfo-pre {
background: var(--background-color-low, rgba(128,128,128,0.05));
border: 1px solid var(--border-color-medium, #ccc);
padding: 10px;
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
font-family: monospace;
font-size: 13px;
}
.conninfo-empty {
color: var(--text-color-secondary, #999);
font-style: italic;
padding: 10px;
}
.conninfo-error {
color: #c00;
padding: 10px;
background: rgba(255,0,0,0.1);
border: 1px solid rgba(255,0,0,0.3);
border-radius: 4px;
}
</style>
<div class="conninfo-container">
<div class="conninfo-header">
<input type="button" class="cbi-button cbi-button-action" id="conninfo_refresh_btn" value="刷新数据" onclick="refreshConnInfo()" />
<span id="conninfo_timestamp" class="conninfo-timestamp">加载中...</span>
</div>
<div class="conninfo-section">
<div class="conninfo-section-header" onclick="toggleSection('node')">
<span id="conninfo_icon_node" class="conninfo-section-icon">▼</span>
<span class="conninfo-section-title">节点信息</span>
</div>
<div id="conninfo_content_node" class="conninfo-section-content">
<em><%:Collecting data...%></em>
</div>
</div>
<div class="conninfo-section">
<div class="conninfo-section-header" onclick="toggleSection('peer')">
<span id="conninfo_icon_peer" class="conninfo-section-icon">▼</span>
<span class="conninfo-section-title">节点列表</span>
</div>
<div id="conninfo_content_peer" class="conninfo-section-content">
<em><%:Collecting data...%></em>
</div>
</div>
<div class="conninfo-section">
<div class="conninfo-section-header" onclick="toggleSection('connector')">
<span id="conninfo_icon_connector" class="conninfo-section-icon">▼</span>
<span class="conninfo-section-title">连接器信息</span>
</div>
<div id="conninfo_content_connector" class="conninfo-section-content">
<em><%:Collecting data...%></em>
</div>
</div>
<div class="conninfo-section">
<div class="conninfo-section-header" onclick="toggleSection('stun')">
<span id="conninfo_icon_stun" class="conninfo-section-icon">▼</span>
<span class="conninfo-section-title">STUN信息</span>
</div>
<div id="conninfo_content_stun" class="conninfo-section-content">
<em><%:Collecting data...%></em>
</div>
</div>
<div class="conninfo-section">
<div class="conninfo-section-header" onclick="toggleSection('route')">
<span id="conninfo_icon_route" class="conninfo-section-icon">▼</span>
<span class="conninfo-section-title">路由信息</span>
</div>
<div id="conninfo_content_route" class="conninfo-section-content">
<em><%:Collecting data...%></em>
</div>
</div>
<div class="conninfo-section">
<div class="conninfo-section-header" onclick="toggleSection('peercenter')">
<span id="conninfo_icon_peercenter" class="conninfo-section-icon">▼</span>
<span class="conninfo-section-title">节点中心</span>
</div>
<div id="conninfo_content_peercenter" class="conninfo-section-content">
<em><%:Collecting data...%></em>
</div>
</div>
<div class="conninfo-section">
<div class="conninfo-section-header" onclick="toggleSection('vpnportal')">
<span id="conninfo_icon_vpnportal" class="conninfo-section-icon">▼</span>
<span class="conninfo-section-title">VPN门户</span>
</div>
<div id="conninfo_content_vpnportal" class="conninfo-section-content">
<em><%:Collecting data...%></em>
</div>
</div>
<div class="conninfo-section">
<div class="conninfo-section-header" onclick="toggleSection('proxy')">
<span id="conninfo_icon_proxy" class="conninfo-section-icon">▼</span>
<span class="conninfo-section-title">TCP/KCP代理</span>
</div>
<div id="conninfo_content_proxy" class="conninfo-section-content">
<em><%:Collecting data...%></em>
</div>
</div>
<div class="conninfo-section">
<div class="conninfo-section-header" onclick="toggleSection('acl')">
<span id="conninfo_icon_acl" class="conninfo-section-icon">▼</span>
<span class="conninfo-section-title">ACL规则</span>
</div>
<div id="conninfo_content_acl" class="conninfo-section-content">
<em><%:Collecting data...%></em>
</div>
</div>
<div class="conninfo-section">
<div class="conninfo-section-header" onclick="toggleSection('mappedlistener')">
<span id="conninfo_icon_mappedlistener" class="conninfo-section-icon">▼</span>
<span class="conninfo-section-title">映射监听器</span>
</div>
<div id="conninfo_content_mappedlistener" class="conninfo-section-content">
<em><%:Collecting data...%></em>
</div>
</div>
<div class="conninfo-section">
<div class="conninfo-section-header" onclick="toggleSection('stats')">
<span id="conninfo_icon_stats" class="conninfo-section-icon">▼</span>
<span class="conninfo-section-title">统计信息</span>
</div>
<div id="conninfo_content_stats" class="conninfo-section-content">
<em><%:Collecting data...%></em>
</div>
</div>
<div class="conninfo-section">
<div class="conninfo-section-header" onclick="toggleSection('cmdline')">
<span id="conninfo_icon_cmdline" class="conninfo-section-icon">▼</span>
<span class="conninfo-section-title">启动参数</span>
</div>
<div id="conninfo_content_cmdline" class="conninfo-section-content">
<em><%:Collecting data...%></em>
</div>
</div>
</div>
@@ -1,49 +1,542 @@
<%
local dsp = require "luci.dispatcher"
local util = require "luci.util"
local i18n = require "luci.i18n"
local translate = i18n.translate
-%>
<%+header%>
<style>
:root {
--log-bg: #ffffff;
--log-text: #333333;
--log-border: #e0e0e0;
--log-header-bg: #f5f5f5;
--log-info: #2196F3;
--log-warn: #FF9800;
--log-error: #F44336;
--log-hover: #f0f0f0;
--tab-active: #2196F3;
--shadow: rgba(0,0,0,0.1);
}
@media (prefers-color-scheme: dark) {
:root {
--log-bg: #1e1e1e;
--log-text: #d4d4d4;
--log-border: #3e3e3e;
--log-header-bg: #252525;
--log-info: #4FC3F7;
--log-warn: #FFB74D;
--log-error: #EF5350;
--log-hover: #2a2a2a;
--tab-active: #42A5F5;
--shadow: rgba(0,0,0,0.3);
}
}
.log-container {
background: var(--log-bg);
border-radius: 12px;
overflow: hidden;
margin: 16px 0;
box-shadow: 0 4px 12px var(--shadow);
border: 1px solid var(--log-border);
transition: box-shadow 0.3s ease;
}
.log-container:hover {
box-shadow: 0 6px 16px var(--shadow);
}
.log-header {
background: var(--log-header-bg);
padding: 16px 20px;
border-bottom: 1px solid var(--log-border);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
}
.log-tabs {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.log-tab {
padding: 10px 20px;
border: none;
background: transparent;
color: #757575;
cursor: pointer;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
.log-tab:hover {
background: var(--log-hover);
transform: translateY(-2px);
}
.log-tab.active {
color: var(--tab-active);
background: var(--log-hover);
}
.log-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 10%;
right: 10%;
height: 3px;
background: var(--tab-active);
border-radius: 3px 3px 0 0;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
transform: scaleX(0);
opacity: 0;
}
to {
transform: scaleX(1);
opacity: 1;
}
}
.log-controls {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.log-select {
padding: 8px 14px;
border: 1px solid var(--log-border);
background: var(--log-bg);
color: var(--log-text);
border-radius: 8px;
cursor: pointer;
font-size: 13px;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.log-select:hover, .log-select:focus {
outline: none;
border-color: var(--tab-active);
box-shadow: 0 2px 8px rgba(33,150,243,0.2);
transform: translateY(-1px);
}
.log-btn {
padding: 8px 16px;
border: 1px solid var(--log-border);
background: var(--log-bg);
color: var(--log-text);
border-radius: 8px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.log-btn:hover {
background: var(--log-hover);
border-color: var(--tab-active);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.log-btn:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.log-btn-primary {
background: var(--tab-active);
color: white;
border-color: var(--tab-active);
}
.log-btn-primary:hover {
opacity: 0.9;
box-shadow: 0 4px 12px rgba(33,150,243,0.3);
}
.log-content {
background: var(--log-bg);
color: var(--log-text);
padding: 20px;
height: 500px;
overflow-y: auto;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 13px;
line-height: 1.8;
white-space: pre-wrap;
word-wrap: break-word;
}
.log-line {
margin: 2px 0;
padding: 4px 10px;
border-radius: 0;
transition: background 0.15s ease;
}
.log-line:hover {
background: var(--log-hover);
}
.log-level-INFO { color: var(--log-info); }
.log-level-WARN { color: var(--log-warn); }
.log-level-ERROR { color: var(--log-error); }
.log-level-DEBUG { color: #9E9E9E; }
.log-level-TRACE { color: #BDBDBD; }
.log-timestamp {
color: #9E9E9E;
margin-right: 12px;
font-weight: 600;
}
.log-empty {
text-align: center;
color: #9E9E9E;
padding: 60px 20px;
font-style: italic;
font-size: 14px;
}
.log-footer {
background: var(--log-header-bg);
padding: 8px 20px;
border-top: 1px solid var(--log-border);
font-size: 12px;
color: #757575;
text-align: right;
}
.log-size-1 { color: #4CAF50; }
.log-size-2 { color: #FF9800; }
.log-size-3 { color: #FF5722; }
.log-size-4 { color: #F44336; }
.log-size-5 { color: #D32F2F; font-weight: bold; }
.log-content::-webkit-scrollbar {
width: 10px;
height: 10px;
}
.log-content::-webkit-scrollbar-track {
background: var(--log-header-bg);
border-radius: 5px;
}
.log-content::-webkit-scrollbar-thumb {
background: var(--log-border);
border-radius: 5px;
transition: background 0.3s ease;
}
.log-content::-webkit-scrollbar-thumb:hover {
background: #757575;
}
@media (max-width: 768px) {
.log-header {
flex-direction: column;
align-items: stretch;
padding: 12px 16px;
}
.log-tabs {
width: 100%;
justify-content: space-around;
}
.log-controls {
width: 100%;
justify-content: space-between;
}
.log-content {
height: 400px;
font-size: 12px;
padding: 16px;
}
.log-btn {
flex: 1;
text-align: center;
}
}
</style>
<div class="log-container">
<div class="log-header">
<div class="log-tabs">
<button class="log-tab active" data-log="core" onclick="switchLog('core')">
<%=translate("core log")%>
</button>
<button class="log-tab" data-log="web" onclick="switchLog('web')">
<%=translate("web log")%>
</button>
</div>
<div class="log-controls">
<select id="log_level_filter" class="log-select" onchange="filterLogLevel()">
<option value="all"><%=translate("All Levels")%></option>
<option value="INFO">INFO</option>
<option value="WARN">WARN</option>
<option value="ERROR">ERROR</option>
<option value="DEBUG">DEBUG</option>
<option value="TRACE">TRACE</option>
</select>
<button class="log-btn log-btn-primary" id="auto_refresh_btn" onclick="toggleAutoRefresh()">
<%=translate("Stop Auto Refresh")%>
</button>
<button class="log-btn" onclick="clearCurrentLog()"><%=translate("Clear Logs")%></button>
<button class="log-btn" onclick="downloadCurrentLog()"><%=translate("Download Log")%></button>
</div>
</div>
<div class="log-content" id="log_content">
<div class="log-empty"><%=translate("Loading logs...")%></div>
</div>
<div class="log-footer" id="log_footer">
<span id="log_size_info"></span>
</div>
</div>
<script type="text/javascript">
//<![CDATA[
function clearlog(btn) {
XHR.get('<%=dsp.build_url("admin/vpn/easytier/clear_log")%>', null,
function(x, data) {
if(x && x.status == 200) {
var log_textarea = document.getElementById('log_textarea');
log_textarea.innerHTML = "";
log_textarea.scrollTop = log_textarea.scrollHeight;
//<![CDATA[
var currentLog = 'core';
var pollTimer = null;
var allLogLines = [];
var autoRefreshEnabled = true;
function filterLogLevel() {
var filter = document.getElementById('log_level_filter').value;
var content = document.getElementById('log_content');
var lines = content.querySelectorAll('.log-line');
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (filter === 'all') {
line.style.display = '';
} else {
var hasLevel = false;
for (var j = 0; j < line.classList.length; j++) {
if (line.classList[j] === 'log-level-' + filter) {
hasLevel = true;
break;
}
}
);
line.style.display = hasLevel ? '' : 'none';
}
}
function downloadLog() {
var log_textarea = document.getElementById('log_textarea');
var logContent = log_textarea.value || log_textarea.innerText || log_textarea.innerHTML;
var blob = new Blob([logContent], {type: 'text/plain;charset=utf-8'});
var url = window.URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'easytier-core_log.txt';
document.body.appendChild(a);
a.click();
setTimeout(function() {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 0);
}
function switchLog(type) {
currentLog = type;
var tabs = document.querySelectorAll('.log-tab');
for (var i = 0; i < tabs.length; i++) {
if (tabs[i].getAttribute('data-log') === type) {
tabs[i].classList.add('active');
} else {
tabs[i].classList.remove('active');
}
}
XHR.poll(2, '<%=dsp.build_url("admin/vpn/easytier/get_log")%>', null,
function(x, data) {
if(x && x.status == 200 && document.getElementById("checkbox1").checked == true) {
var log_textarea = document.getElementById('log_textarea');
log_textarea.innerHTML = x.responseText;
log_textarea.scrollTop = log_textarea.scrollHeight;
document.getElementById('log_size_info').innerHTML = '';
refreshLog();
}
function formatLogLine(line) {
if (!line || line.trim() === '') return '';
var timestamp = '';
var content = line.trim();
var level = 'INFO';
// 匹配 ISO 8601 格式: 2026-04-14T20:30:54.744803154+08:00
var isoTimeMatch = content.match(/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})\.\d+[+-]\d{2}:\d{2}\s*/);
if (isoTimeMatch) {
timestamp = isoTimeMatch[1] + ' ' + isoTimeMatch[2];
content = content.substring(isoTimeMatch[0].length);
} else {
// 匹配简单格式: 2026-04-14 20:19:33
var simpleTimeMatch = content.match(/^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})[:\s]*/);
if (simpleTimeMatch) {
timestamp = simpleTimeMatch[1];
content = content.substring(simpleTimeMatch[0].length);
}
}
// 匹配日志级别
var levelMatch = content.match(/^\s*(INFO|WARN|ERROR|DEBUG|TRACE)\s+/i);
if (levelMatch) {
level = levelMatch[1].toUpperCase();
content = content.substring(levelMatch[0].length);
}
var html = '<div class="log-line log-level-' + level + '">';
if (timestamp) {
html += '<span class="log-timestamp">' + escapeHtml(timestamp) + '</span>';
}
html += '<span class="log-level-' + level + '">' + escapeHtml(content) + '</span>';
html += '</div>';
return html;
}
function escapeHtml(text) {
var div = document.createElement('div');
div.appendChild(document.createTextNode(text));
return div.innerHTML;
}
function refreshLog() {
var url = currentLog === 'core'
? '<%=url("admin/vpn/easytier/get_log")%>'
: '<%=url("admin/vpn/easytier/get_wlog")%>';
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
var content = document.getElementById('log_content');
var rawLog = xhr.responseText || '';
if (rawLog.trim() === '') {
content.innerHTML = '<div class="log-empty"><%=translate("No logs available")%></div>';
return;
}
var lines = rawLog.split('\n');
var html = '';
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (!line.trim()) continue;
html += formatLogLine(line);
}
content.innerHTML = html;
filterLogLevel();
if (autoRefreshEnabled) {
content.scrollTop = content.scrollHeight;
updateLogSize();
}
}
);
//]]>
};
xhr.send();
}
function updateLogSize() {
var url = currentLog === 'core'
? '<%=url("admin/vpn/easytier/get_log_size")%>'
: '<%=url("admin/vpn/easytier/get_wlog_size")%>';
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
var data = JSON.parse(xhr.responseText);
var sizeMB = (data.size / 1024 / 1024).toFixed(2);
var logPath = currentLog === 'core' ? '/tmp/easytier.log' : '/tmp/easytierweb.log';
var sizeClass = '';
if (sizeMB < 1) sizeClass = 'log-size-1';
else if (sizeMB < 2) sizeClass = 'log-size-2';
else if (sizeMB < 3) sizeClass = 'log-size-3';
else if (sizeMB < 4) sizeClass = 'log-size-4';
else sizeClass = 'log-size-5';
document.getElementById('log_size_info').innerHTML =
logPath + ': <span class="' + sizeClass + '">' + sizeMB + ' MB</span>';
}
};
xhr.send();
}
function clearCurrentLog() {
var url = currentLog === 'core'
? '<%=url("admin/vpn/easytier/clear_log")%>'
: '<%=url("admin/vpn/easytier/clear_wlog")%>';
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
document.getElementById('log_content').innerHTML = '<div class="log-empty"><%=translate("Logs cleared")%></div>';
}
};
xhr.send();
}
function downloadCurrentLog() {
var content = document.getElementById('log_content');
var logText = '';
var lines = content.querySelectorAll('.log-line');
for (var i = 0; i < lines.length; i++) {
logText += lines[i].innerText + '\n';
}
if (logText.trim() === '') {
logText = content.innerText;
}
var blob = new Blob([logText], {type: 'text/plain;charset=utf-8'});
var url = window.URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'easytier-' + currentLog + '-log.txt';
document.body.appendChild(a);
a.click();
setTimeout(function() {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 0);
}
function startPolling() {
if (pollTimer) clearInterval(pollTimer);
pollTimer = setInterval(function() {
if (autoRefreshEnabled) {
refreshLog();
}
}, 2000);
}
function toggleAutoRefresh() {
autoRefreshEnabled = !autoRefreshEnabled;
var btn = document.getElementById('auto_refresh_btn');
if (autoRefreshEnabled) {
btn.textContent = '<%=translate("Stop Auto Refresh")%>';
btn.classList.add('log-btn-primary');
} else {
btn.textContent = '<%=translate("Start Auto Refresh")%>';
btn.classList.remove('log-btn-primary');
}
}
refreshLog();
startPolling();
window.addEventListener('beforeunload', function() {
if (pollTimer) clearInterval(pollTimer);
});
//]]>
</script>
<fieldset class="cbi-section" id="_log_fieldset">
<input type="checkbox" id="checkbox1" style="vertical-align:middle;height: auto;"checked><%:Auto Refresh%></input>
<input class="cbi-button cbi-input-remove" type="button" onclick="clearlog()" value="<%:Clear Logs%>" />
<input class="cbi-button cbi-input-apply" type="button" onclick="downloadLog()" value="<%:Download Log%>" />
<textarea id="log_textarea" class="cbi-input-textarea" style="width: 100%;margin-top: 10px;" data-update="change" rows="30" wrap="off" readonly="readonly"></textarea>
</fieldset>
<%+footer%>
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,847 @@
<%
local util = require "luci.util"
local i18n = require "luci.i18n"
local translate = i18n.translate
-%>
<%+header%>
<style>
:root {
--upload-bg: #ffffff;
--upload-border: #e0e0e0;
--upload-hover: #f5f5f5;
--text-primary: #333333;
--text-secondary: #666666;
--success: #4CAF50;
--error: #F44336;
--info: #2196F3;
}
@media (prefers-color-scheme: dark) {
:root {
--upload-bg: #1e1e1e;
--upload-border: #3e3e3e;
--upload-hover: #2a2a2a;
--text-primary: #e0e0e0;
--text-secondary: #b0b0b0;
--success: #66BB6A;
--error: #EF5350;
--info: #42A5F5;
}
}
.upload-container {
max-width: 800px;
margin: 20px auto;
}
.config-input, .config-textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--upload-border);
border-radius: 6px;
background: var(--upload-bg);
color: var(--text-primary);
font-size: 13px;
font-family: 'Consolas', 'Monaco', monospace;
transition: border-color 0.2s;
}
.config-input:focus, .config-textarea:focus {
outline: none;
border-color: var(--info);
}
.config-textarea {
resize: vertical;
min-height: 60px;
}
.upload-card {
background: var(--upload-bg);
border: 2px dashed var(--upload-border);
border-radius: 12px;
padding: 40px;
text-align: center;
transition: all 0.3s;
cursor: pointer;
margin-top: 20px;
}
.upload-card:hover, .upload-card.drag-over {
border-color: var(--info);
background: var(--upload-hover);
}
.upload-icon {
font-size: 48px;
color: var(--info);
margin-bottom: 16px;
}
.upload-title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 8px;
}
.upload-desc {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 20px;
}
.upload-btn {
display: inline-block;
padding: 10px 24px;
background: var(--info);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
}
.upload-btn:hover {
opacity: 0.9;
}
.upload-info {
background: var(--upload-bg);
border: 1px solid var(--upload-border);
border-radius: 8px;
padding: 16px;
margin-top: 20px;
text-align: left;
}
.upload-info-title {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 12px;
}
.upload-info-list {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.8;
}
.upload-info-list li {
margin-bottom: 4px;
}
.proxy-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
background: var(--upload-hover);
border: 1px solid var(--upload-border);
border-radius: 6px;
margin-bottom: 6px;
font-size: 13px;
color: var(--text-primary);
}
.proxy-item span {
flex: 1;
word-break: break-all;
}
.proxy-item button {
padding: 4px 12px;
background: var(--error);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: opacity 0.2s;
}
.proxy-item button:hover {
opacity: 0.8;
}
.spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid var(--upload-border);
border-top-color: var(--info);
border-radius: 50%;
animation: spin 0.8s linear infinite;
vertical-align: middle;
margin-right: 8px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.upload-progress {
display: none;
margin-top: 20px;
}
.progress-bar {
width: 100%;
height: 8px;
background: var(--upload-border);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--info);
transition: width 0.3s;
width: 0%;
}
.progress-text {
text-align: center;
margin-top: 8px;
font-size: 13px;
color: var(--text-secondary);
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 9999;
}
.modal.show {
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: var(--upload-bg);
border-radius: 12px;
padding: 24px;
max-width: 400px;
width: 90%;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.modal-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 12px;
}
.modal-message {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 20px;
line-height: 1.6;
}
.modal-buttons {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.modal-btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.2s;
}
.modal-btn-primary {
background: var(--info);
color: white;
}
.modal-btn-secondary {
background: var(--upload-border);
color: var(--text-primary);
}
.modal-btn:hover {
opacity: 0.9;
}
.alert {
padding: 14px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
position: fixed;
top: 80px;
left: 50%;
transform: translateX(-50%);
z-index: 10000;
min-width: 320px;
max-width: 500px;
text-align: center;
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
opacity: 0;
transition: opacity 0.3s ease-in-out;
pointer-events: none;
}
.alert.show {
opacity: 1;
pointer-events: auto;
}
.alert-success {
background: var(--success);
color: white;
border: none;
}
.alert-error {
background: var(--error);
color: white;
border: none;
}
.alert-info {
background: var(--info);
color: white;
border: none;
}
@media (max-width: 768px) {
.upload-container {
margin: 10px;
}
.upload-card {
padding: 24px;
}
}
</style>
<div class="upload-container">
<div class="upload-info">
<div class="upload-info-title"><%=translate("Configuration")%></div>
<div style="margin-bottom:16px;">
<label style="display:block;font-size:13px;color:var(--text-secondary);margin-bottom:4px;">
<%=translate("easytier-core Binary Path")%>
</label>
<input type="text" id="easytierbin" class="config-input" placeholder="/usr/bin/easytier-core" onblur="checkDiskSpace('easytierbin')">
<div id="easytierbin_space" style="font-size:11px;color:var(--text-secondary);margin-top:4px;"></div>
<div id="easytierbin_size" style="font-size:11px;color:var(--text-secondary);margin-top:2px;"></div>
</div>
<div style="margin-bottom:16px;">
<label style="display:block;font-size:13px;color:var(--text-secondary);margin-bottom:4px;">
<%=translate("easytier-web Binary Path")%>
</label>
<input type="text" id="webbin" class="config-input" placeholder="/usr/bin/easytier-web" onblur="checkDiskSpace('webbin')">
<div id="webbin_space" style="font-size:11px;color:var(--text-secondary);margin-top:4px;"></div>
<div id="webbin_size" style="font-size:11px;color:var(--text-secondary);margin-top:2px;"></div>
</div>
<div style="margin-bottom:16px;">
<label style="display:block;font-size:13px;font-weight:500;color:var(--text-primary);margin-bottom:8px;">
<%=translate("GitHub Proxy URLs")%>
</label>
<div id="proxy_list" style="margin-bottom:8px;"></div>
<div style="display:flex;gap:8px;margin-bottom:8px;">
<input type="text" id="new_proxy" class="config-input" placeholder="https://ghproxy.net/" style="flex:1;">
<button class="upload-btn" onclick="addProxy()" style="width:auto;padding:8px 16px;">
<%=translate("Add")%>
</button>
</div>
<div style="font-size:11px;color:#999;opacity:0.8;">
<%=translate("Used to accelerate GitHub downloads when fetching binaries online")%>
</div>
</div>
<div style="margin-bottom:16px;">
<label style="display:block;font-size:13px;font-weight:500;color:var(--text-primary);margin-bottom:4px;">
<%=translate("Fallback Version")%>
</label>
<input type="text" id="fallback_version" class="config-input" placeholder="v2.5.0">
<div style="font-size:11px;color:#999;opacity:0.8;margin-top:4px;">
<%=translate("Used when unable to fetch the latest version during online download")%>
</div>
</div>
<button class="upload-btn" onclick="saveConfig()" style="width:100%;">
<%=translate("Save Configuration")%>
</button>
</div>
<div class="upload-card" id="upload_area">
<div class="upload-icon">📦</div>
<div class="upload-title"><%=translate("Upload EasyTier Binary")%></div>
<div class="upload-desc"><%=translate("Drag *.tar.gz / *.zip / easytier binaries here or click to select file")%></div>
<input type="file" id="file_input" style="display:none" accept="*">
<button class="upload-btn" onclick="event.stopPropagation(); document.getElementById('file_input').click();">
<%=translate("Select File")%>
</button>
</div>
<div class="upload-progress" id="upload_progress">
<div class="progress-bar">
<div class="progress-fill" id="progress_fill"></div>
</div>
<div class="progress-text" id="progress_text"><%=translate("Uploading...")%> 0%</div>
</div>
<div class="upload-progress" id="processing_progress" style="display:none;">
<div class="progress-text" style="margin-top:0;">
<span class="spinner"></span>
<%=translate("Processing uploaded file, please wait...")%>
</div>
</div>
<div class="alert" id="alert_box"></div>
<div class="upload-info">
<div class="upload-info-title"><%=translate("Supported Files")%></div>
<ul class="upload-info-list">
<li><strong>easytier-core</strong> - <%=translate("Core service binary")%></li>
<li><strong>easytier-cli</strong> - <%=translate("Command line tool")%></li>
<li><strong>easytier-web-embed</strong> - <%=translate("Embedded web console")%> <span style="color:var(--success);">(<%=translate("Recommended")%>)</span></li>
<li><strong>easytier-web</strong> - <%=translate("Web console binary")%> <span style="color:var(--error);">(<%=translate("Will be ignored, use embed version instead")%>)</span></li>
<li><strong>*.zip / *.tar.gz</strong> - <%=translate("Compressed archive containing binaries")%></li>
</ul>
<div style="margin-top:12px;padding:12px;background:rgba(33,150,243,0.1);border-left:3px solid var(--info);border-radius:4px;">
<div style="font-size:12px;color:var(--text-primary);margin-bottom:4px;">
<strong><%=translate("Note")%>:</strong>
</div>
<div style="font-size:12px;color:var(--text-secondary);line-height:1.6;">
<%=translate("Files will be uploaded to the paths specified above")%>. <%=translate("EasyTier console uses easytier-web-embed. The easytier-web binary will be rejected during upload validation.")%>
</div>
</div>
<div style="margin-top:12px;padding:12px;background:rgba(244,67,54,0.1);border-left:3px solid var(--error);border-radius:4px;">
<div style="font-size:12px;color:var(--text-primary);margin-bottom:4px;">
<strong><%=translate("Warning")%>:</strong>
</div>
<div style="font-size:12px;color:var(--text-secondary);line-height:1.6;">
<%=translate("If the device has insufficient storage space, the upload may fail and existing programs may be deleted. Please ensure adequate free space before uploading.")%>
</div>
</div>
<div style="margin-top:12px;font-size:12px;color:var(--text-secondary);">
<%=translate("Download from")%>: <a href="https://github.com/EasyTier/EasyTier/releases" target="_blank">https://github.com/EasyTier/EasyTier</a>
</div>
</div>
</div>
<div class="modal" id="restart_modal">
<div class="modal-content">
<div class="modal-title"><%=translate("Upload Successful")%></div>
<div class="modal-message" id="modal_message">
<%=translate("Binary uploaded successfully. Restart the service to apply changes?")%>
</div>
<div class="modal-buttons">
<button class="modal-btn modal-btn-secondary" onclick="closeModal()"><%=translate("Later")%></button>
<button class="modal-btn modal-btn-primary" onclick="restartService()"><%=translate("Restart Now")%></button>
</div>
</div>
</div>
<script type="text/javascript">
//<![CDATA[
var originalConfig = {};
// 加载配置
function loadConfig() {
var xhr = new XMLHttpRequest();
xhr.open('GET', '<%=url("admin", "vpn", "easytier", "get_upload_config")%>', true);
xhr.onload = function() {
if (xhr.status === 200) {
try {
var config = JSON.parse(xhr.responseText);
document.getElementById('easytierbin').value = config.easytierbin || '/usr/bin/easytier-core';
document.getElementById('webbin').value = config.webbin || '/usr/bin/easytier-web';
// 确保是数组
var proxys = config.github_proxys || [];
proxyList = Array.isArray(proxys) ? proxys : [];
renderProxyList();
document.getElementById('fallback_version').value = config.fallback_version || 'v2.6.0';
// 保存原始配置
originalConfig = {
easytierbin: config.easytierbin || '/usr/bin/easytier-core',
webbin: config.webbin || '/usr/bin/easytier-web',
github_proxys: JSON.stringify(proxyList),
fallback_version: config.fallback_version || 'v2.6.0'
};
checkDiskSpace('easytierbin');
checkDiskSpace('webbin');
} catch (e) {
proxyList = [];
renderProxyList();
}
}
};
xhr.send();
}
function checkDiskSpace(inputId) {
var input = document.getElementById(inputId);
var spaceDiv = document.getElementById(inputId + '_space');
var sizeDiv = document.getElementById(inputId + '_size');
var path = input.value || input.placeholder;
if (!path) {
spaceDiv.innerHTML = '';
sizeDiv.innerHTML = '';
return;
}
var dir = path.match(/^(.*)\/[^\/]+$/);
dir = dir ? dir[1] : '/tmp';
var xhr = new XMLHttpRequest();
xhr.open('POST', '<%=url("admin", "vpn", "easytier", "get_disk_space")%>', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
if (xhr.status === 200) {
try {
var response = JSON.parse(xhr.responseText);
if (response.success) {
var color = response.available_mb < 50 ? 'var(--error)' : 'var(--success)';
spaceDiv.innerHTML = '<span style="color:' + color + ';"><%=translate("Available space")%>: ' + response.available + ' (' + response.available_mb + ' MB)</span>';
var sizeHtml = '';
if (inputId === 'easytierbin') {
if (response.core_size) {
sizeHtml += 'easytier-core ' + response.core_size;
}
if (response.cli_size) {
sizeHtml += (sizeHtml ? ' | ' : '') + ' easytier-cli ' + response.cli_size;
}
} else if (inputId === 'webbin') {
if (response.web_size) {
sizeHtml = 'easytier-web ' + response.web_size;
}
}
sizeDiv.innerHTML = sizeHtml ? '<span style="color:#999;opacity:0.8;"><%=translate("Program size")%>: ' + sizeHtml + '</span>' : '';
} else {
spaceDiv.innerHTML = '<span style="color:var(--error);"><%=translate("Unable to query disk space")%></span>';
sizeDiv.innerHTML = '';
}
} catch (e) {
spaceDiv.innerHTML = '';
sizeDiv.innerHTML = '';
}
}
};
xhr.send(JSON.stringify({path: dir, full_path: path, type: inputId}));
}
function getCurrentConfig() {
return {
easytierbin: document.getElementById('easytierbin').value || '/usr/bin/easytier-core',
webbin: document.getElementById('webbin').value || '/usr/bin/easytier-web',
github_proxys: JSON.stringify(proxyList),
fallback_version: document.getElementById('fallback_version').value || 'v2.6.0'
};
}
function isConfigChanged() {
var current = getCurrentConfig();
for (var key in originalConfig) {
if (originalConfig[key] !== current[key]) {
return true;
}
}
return false;
}
// 保存配置
function saveConfig() {
if (!isConfigChanged()) {
showAlert('info', '<%=translate("No changes to save")%>');
return;
}
var data = {
easytierbin: document.getElementById('easytierbin').value || '/usr/bin/easytier-core',
webbin: document.getElementById('webbin').value || '/usr/bin/easytier-web',
github_proxys: proxyList,
fallback_version: document.getElementById('fallback_version').value || 'v2.6.0'
};
var xhr = new XMLHttpRequest();
xhr.open('POST', '<%=url("admin", "vpn", "easytier", "save_upload_config")%>', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
if (xhr.status === 200) {
try {
var response = JSON.parse(xhr.responseText);
if (response.success) {
showAlert('success', '<%=translate("Configuration saved successfully")%>');
originalConfig = getCurrentConfig();
} else {
showAlert('error', response.message || '<%=translate("Save failed")%>');
}
} catch (e) {
showAlert('error', '<%=translate("Save failed")%>');
}
} else {
showAlert('error', '<%=translate("Save failed")%>');
}
};
xhr.onerror = function() {
showAlert('error', '<%=translate("Save failed")%>');
};
xhr.send(JSON.stringify(data));
}
var proxyList = [];
var uploadArea, fileInput, progressDiv, progressFill, progressText, alertBox, restartModal;
function renderProxyList() {
var container = document.getElementById('proxy_list');
if (proxyList.length === 0) {
container.innerHTML = '<div style="font-size:12px;color:#FF9800;padding:8px;background:rgba(255,152,0,0.1);border-radius:4px;"><%=translate("No proxy configured, using default proxy")%></div>';
return;
}
var html = '';
for (var i = 0; i < proxyList.length; i++) {
html += '<div class="proxy-item">';
html += '<span>' + escapeHtml(proxyList[i]) + '</span>';
html += '<button onclick="removeProxy(' + i + ')"><%=translate("Delete")%></button>';
html += '</div>';
}
container.innerHTML = html;
}
function addProxy() {
var input = document.getElementById('new_proxy');
var url = input.value.trim();
if (!url) {
showAlert('error', '<%=translate("Please enter proxy URL")%>');
return;
}
if (!/^https?:\/\/.+/.test(url)) {
showAlert('error', '<%=translate("Invalid URL format")%>');
return;
}
if (proxyList.indexOf(url) !== -1) {
showAlert('error', '<%=translate("Proxy already exists")%>');
return;
}
proxyList.push(url);
input.value = '';
renderProxyList();
}
function removeProxy(index) {
proxyList.splice(index, 1);
renderProxyList();
}
function escapeHtml(text) {
var div = document.createElement('div');
div.appendChild(document.createTextNode(text));
return div.innerHTML;
}
window.onload = function() {
uploadArea = document.getElementById('upload_area');
fileInput = document.getElementById('file_input');
progressDiv = document.getElementById('upload_progress');
progressFill = document.getElementById('progress_fill');
progressText = document.getElementById('progress_text');
alertBox = document.getElementById('alert_box');
restartModal = document.getElementById('restart_modal');
loadConfig();
var validNames = ['easytier-core', 'easytier-cli', 'easytier-web', 'easytier-web-embed'];
// 拖放事件
uploadArea.addEventListener('dragover', function(e) {
e.preventDefault();
uploadArea.classList.add('drag-over');
});
uploadArea.addEventListener('dragleave', function(e) {
e.preventDefault();
uploadArea.classList.remove('drag-over');
});
uploadArea.addEventListener('drop', function(e) {
e.preventDefault();
uploadArea.classList.remove('drag-over');
var files = e.dataTransfer.files;
if (files.length > 0) {
handleFile(files[0]);
}
});
// 文件选择
fileInput.addEventListener('change', function(e) {
if (e.target.files.length > 0) {
handleFile(e.target.files[0]);
}
});
// 点击上传区域
uploadArea.addEventListener('click', function() {
fileInput.click();
});
};
function handleFile(file) {
var validNames = ['easytier-core', 'easytier-cli', 'easytier-web', 'easytier-web-embed'];
var filename = file.name;
// 检查是否是压缩文件
var isArchive = filename.match(/\.(zip|tar\.gz|tgz|tar)$/i);
// 验证文件名(压缩文件直接通过,二进制文件需要验证)
if (!isArchive) {
var isValid = false;
for (var i = 0; i < validNames.length; i++) {
if (filename === validNames[i]) {
isValid = true;
break;
}
}
if (!isValid) {
showAlert('error', '<%=translate("Invalid file name")%>. <%=translate("Only easytier binaries or compressed archives (.zip, .tar.gz, .tar) are allowed")%>');
return;
}
}
uploadFile(file);
}
function uploadFile(file) {
hideAlert();
progressDiv.style.display = 'block';
progressFill.style.width = '0%';
progressText.textContent = '<%=translate("Uploading...")%> 0%';
var formData = new FormData();
formData.append('filename', file.name);
formData.append('file', file);
var xhr = new XMLHttpRequest();
xhr.open('POST', '<%=url("admin", "vpn", "easytier", "upload_binary")%>', true);
xhr.upload.onprogress = function(event) {
if (event.lengthComputable) {
var percent = Math.round((event.loaded / event.total) * 100);
progressFill.style.width = percent + '%';
progressText.textContent = '<%=translate("Uploading...")%> ' + percent + '%';
}
};
xhr.onload = function() {
var uploadComplete = progressFill.style.width === '100%';
if (uploadComplete) {
progressDiv.style.display = 'none';
document.getElementById('processing_progress').style.display = 'block';
}
if (xhr.status === 200) {
try {
var response = JSON.parse(xhr.responseText);
if (response.success) {
setTimeout(function() {
progressDiv.style.display = 'none';
document.getElementById('processing_progress').style.display = 'none';
showAlert('success', response.message);
document.getElementById('modal_message').textContent = response.message + ' <%=translate("Restart the service to apply changes?")%>';
restartModal.classList.add('show');
}, 100);
} else {
progressDiv.style.display = 'none';
document.getElementById('processing_progress').style.display = 'none';
showAlert('error', response.message);
}
} catch (e) {
progressDiv.style.display = 'none';
document.getElementById('processing_progress').style.display = 'none';
showAlert('error', '<%=translate("Upload failed")%>: ' + e.message);
}
} else {
progressDiv.style.display = 'none';
document.getElementById('processing_progress').style.display = 'none';
showAlert('error', '<%=translate("Upload failed")%>: HTTP ' + xhr.status);
}
};
xhr.onerror = function() {
progressDiv.style.display = 'none';
document.getElementById('processing_progress').style.display = 'none';
showAlert('error', '<%=translate("Network error")%>');
};
xhr.send(formData);
}
function showAlert(type, message) {
var alertBox = document.getElementById('alert_box');
if (!alertBox) return;
alertBox.className = 'alert show alert-' + type;
alertBox.textContent = message;
// 3秒后自动淡出
setTimeout(function() {
hideAlert();
}, 3000);
}
function hideAlert() {
var alertBox = document.getElementById('alert_box');
if (!alertBox) return;
alertBox.className = 'alert';
}
function closeModal() {
restartModal.classList.remove('show');
}
function restartService() {
closeModal();
showAlert('info', '<%=translate("Restarting service...")%>');
var xhr = new XMLHttpRequest();
xhr.open('GET', '<%=url("admin", "vpn", "easytier", "restart_service")%>', true);
xhr.onload = function() {
if (xhr.status === 200) {
setTimeout(function() {
showAlert('success', '<%=translate("Service restarted successfully")%>');
}, 2000);
} else {
showAlert('error', '<%=translate("Restart failed")%>');
}
};
xhr.onerror = function() {
showAlert('error', '<%=translate("Restart failed")%>');
};
xhr.send();
}
//]]>
</script>
<%+footer%>
@@ -0,0 +1,645 @@
<%
local util = require "luci.util"
local uci = require "luci.model.uci".cursor()
-%>
<%+header%>
<style>
:root {
--card-bg: #ffffff;
--border-color: #e0e0e0;
--text-primary: #333333;
--text-secondary: #666666;
--success: #4CAF50;
--error: #F44336;
--info: #2196F3;
--warning: #FF9800;
--hover-bg: #f5f5f5;
}
@media (prefers-color-scheme: dark) {
:root {
--card-bg: #1e1e1e;
--border-color: #3e3e3e;
--text-primary: #e0e0e0;
--text-secondary: #b0b0b0;
--success: #66BB6A;
--error: #EF5350;
--info: #42A5F5;
--warning: #FFA726;
--hover-bg: #2a2a2a;
}
}
.web-console-container {
max-width: 1200px;
margin: 20px auto;
padding: 0 20px;
}
.web-card {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 28px;
margin-bottom: 24px;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
transition: box-shadow 0.3s ease;
}
.web-card:hover {
box-shadow: 0 6px 16px rgba(0,0,0,0.12);
}
.web-card h3 {
margin: 0 0 24px 0;
color: var(--text-primary);
font-size: 20px;
font-weight: 600;
border-bottom: 3px solid var(--info);
padding-bottom: 14px;
display: flex;
align-items: center;
gap: 12px;
}
.web-card h3::before {
content: "⚙️";
font-size: 24px;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 24px;
}
.form-group label {
display: block;
margin-bottom: 10px;
color: var(--text-primary);
font-weight: 600;
font-size: 14px;
}
.form-group .description {
display: block;
margin-top: 6px;
color: var(--text-secondary);
font-size: 12px;
line-height: 1.6;
padding-left: 0;
opacity: 0.8;
}
.form-group .description a {
color: var(--info);
text-decoration: none;
font-weight: 500;
}
.form-group .description a:hover {
text-decoration: underline;
}
.form-control {
width: 100%;
padding: 12px 16px;
border: 2px solid var(--border-color);
border-radius: 8px;
background: var(--card-bg);
color: var(--text-primary);
font-size: 14px;
transition: all 0.2s ease;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.form-control:focus {
outline: none;
border-color: var(--info);
box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.1);
}
.form-control:disabled {
opacity: 0.5;
cursor: not-allowed;
background: var(--hover-bg);
}
.checkbox-wrapper {
display: block;
padding: 12px 16px;
background: var(--hover-bg);
border-radius: 8px;
transition: background 0.2s;
position: relative;
min-height: 26px;
}
.checkbox-wrapper:hover {
background: var(--border-color);
}
.checkbox-wrapper label {
margin: 0 !important;
cursor: pointer;
user-select: none;
display: block;
line-height: 26px;
padding-right: 60px;
}
.checkbox-wrapper label::before {
display: none;
}
/* iOS风格开关 */
.switch {
position: absolute;
width: 48px;
height: 26px;
right: 16px;
top: 12px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
position: absolute;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
width: 48px;
height: 26px;
background-color: #ccc;
-webkit-transition: 0.3s;
transition: 0.3s;
border-radius: 26px;
}
.slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 3px;
top: 3px;
background-color: white;
-webkit-transition: 0.3s;
transition: 0.3s;
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
input:checked + .slider {
background-color: var(--info);
}
input:checked + .slider:before {
-webkit-transform: translateX(22px);
-ms-transform: translateX(22px);
transform: translateX(22px);
}
input:disabled + .slider {
opacity: 0.5;
cursor: not-allowed;
}
.btn {
padding: 12px 28px;
border: none;
border-radius: 8px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 8px;
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.btn:active {
transform: translateY(0);
}
.btn-primary {
background: var(--info);
color: white;
}
.btn-primary::before {
content: "💾";
}
.btn-success {
background: var(--success);
color: white;
}
.btn-success::before {
content: "🔄";
}
.btn-group {
display: flex;
gap: 16px;
margin-top: 32px;
padding-top: 24px;
border-top: 2px dashed var(--border-color);
}
.iframe-container {
width: 100%;
height: 800px;
border: 2px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
background: var(--card-bg);
box-shadow: inset 0 2px 8px rgba(0,0,0,0.05);
}
.iframe-container iframe {
width: 100%;
height: 100%;
border: none;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 16px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
margin-left: auto;
}
.status-badge::before {
content: "";
width: 8px;
height: 8px;
border-radius: 50%;
animation: pulse 2s infinite;
}
.status-running {
background: rgba(76, 175, 80, 0.15);
color: var(--success);
}
.status-running::before {
background: var(--success);
}
.status-stopped {
background: rgba(244, 67, 54, 0.15);
color: var(--error);
}
.status-stopped::before {
background: var(--error);
animation: none;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.loading-spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
.alert {
padding: 14px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
position: fixed;
top: 80px;
left: 50%;
-webkit-transform: translateX(-50%);
-ms-transform: translateX(-50%);
transform: translateX(-50%);
z-index: 10000;
min-width: 320px;
max-width: 500px;
text-align: center;
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
opacity: 0;
-webkit-transition: opacity 0.3s ease-in-out;
transition: opacity 0.3s ease-in-out;
pointer-events: none;
}
.alert.show {
opacity: 1;
pointer-events: auto;
}
.alert-success {
background: var(--success);
color: white;
border: none;
}
.alert-error {
background: var(--error);
color: white;
border: none;
}
.alert-info {
background: var(--info);
color: white;
border: none;
}
</style>
<div class="web-console-container">
<div class="web-card">
<h3><%:Self-hosted Web Console%></h3>
<form id="webConsoleForm">
<div class="form-group">
<div class="checkbox-wrapper">
<label for="web_enabled"><%:Enable Self-hosted Web Console%></label>
<label class="switch">
<input type="checkbox" id="web_enabled" name="web_enabled">
<span class="slider"></span>
</label>
</div>
</div>
<div class="form-group">
<label for="web_db_path"><%:Database File Path%></label>
<input type="text" class="form-control" id="web_db_path" name="web_db_path" placeholder="/etc/easytier/et.db">
<span class="description"><%:Path to the sqlite3 database file used to store all data. (-d parameter)%></span>
</div>
<div class="form-group">
<label for="web_protocol"><%:Listening Protocol%></label>
<select class="form-control" id="web_protocol" name="web_protocol">
<option value="udp">UDP</option>
<option value="tcp">TCP</option>
<option value="ws">WS</option>
<option value="wss">WSS</option>
</select>
<span class="description"><%:Configure the server's listening protocol for easytier-core to connect. (-p parameter)%></span>
</div>
<div class="form-group">
<label for="web_port"><%:Server Port%></label>
<input type="number" class="form-control" id="web_port" name="web_port" placeholder="22020" min="1" max="65535">
<span class="description"><%:Configure the server's listening port for easytier-core to connect. (-c parameter)%></span>
</div>
<div class="form-group">
<div class="checkbox-wrapper">
<label for="web_fw_web"><%:WAN access to WEB%></label>
<label class="switch">
<input type="checkbox" id="web_fw_web" name="web_fw_web">
<span class="slider"></span>
</label>
</div>
<span class="description"><%:Automatically add firewall rules to allow WAN access to this WEB console%></span>
</div>
<div class="form-group">
<label for="web_api_port"><%:API Port%></label>
<input type="number" class="form-control" id="web_api_port" name="web_api_port" placeholder="11211" min="1" max="65535">
<span class="description"><%:Listening port of the RESTful server, used as ApiHost by the web frontend. (-a parameter)%></span>
</div>
<div class="form-group">
<label for="web_html_port"><%:Web Interface Port%></label>
<input type="number" class="form-control" id="web_html_port" name="web_html_port" placeholder="11211" min="1" max="65535">
<span class="description"><%:Frontend listening port for the web dashboard server. Leave empty to disable. (-l parameter)%></span>
</div>
<div class="form-group">
<div class="checkbox-wrapper">
<label for="web_fw_api"><%:WAN access to API%></label>
<label class="switch">
<input type="checkbox" id="web_fw_api" name="web_fw_api">
<span class="slider"></span>
</label>
</div>
<span class="description"><%:Automatically add firewall rules to allow WAN access to the API control page%></span>
</div>
<div class="form-group">
<label for="web_api_host"><%:Default API Server URL%></label>
<input type="text" class="form-control" id="web_api_host" name="web_api_host" placeholder="http://192.168.1.1:11211">
<span class="description"><%:The URL of the API server, used for connecting the web frontend%>. (--api-host <%:parameter%>)<br>
<%:Example%>: http://[<%:current device IP or domain%>]:[<%:API port%>]</span>
</div>
<div class="form-group">
<label for="web_geoip_db"><%:GEOIP Database Path%></label>
<input type="text" class="form-control" id="web_geoip_db" name="web_geoip_db" placeholder="/etc/easytier/GeoLite.mmdb">
<span class="description"><%:GeoIP2 database file path used to locate the client%>. <%:Defaults to an embedded file (country-level information only)%>.<br>
<%:Recommended%>: <a href="https://github.com/P3TERX/GeoLite.mmdb" target="_blank">https://github.com/P3TERX/GeoLite.mmdb</a> (--geoip-db <%:parameter%>)</span>
</div>
<div class="form-group">
<label for="web_weblog"><%:Log Level%></label>
<select class="form-control" id="web_weblog" name="web_weblog">
<option value="off"><%:Off%></option>
<option value="error"><%:Error%></option>
<option value="warn"><%:Warning%></option>
<option value="info"><%:Info%></option>
<option value="debug"><%:Debug%></option>
<option value="trace"><%:Trace%></option>
</select>
<span class="description"><%:Runtime log located at%> /tmp/easytierweb.log, <%:viewable in the log section%>.<br>
<%:Log levels%>: Error < Warning < Info < Debug < Trace</span>
</div>
<div class="btn-group">
<button type="button" class="btn btn-primary" onclick="saveConfig()"><%:Save Configuration%></button>
</div>
</form>
</div>
<div class="web-card" id="consoleCard" style="display:none;">
<div class="iframe-container">
<iframe id="webConsoleFrame"></iframe>
</div>
</div>
</div>
<!-- 提示框 -->
<div id="alert_box" class="alert"></div>
<script type="text/javascript">//<![CDATA[
var originalConfig = {};
function showAlert(type, message) {
var alertBox = document.getElementById('alert_box');
if (!alertBox) return;
alertBox.className = 'alert alert-' + type + ' show';
alertBox.textContent = message;
setTimeout(function() {
hideAlert();
}, 3000);
}
function hideAlert() {
var alertBox = document.getElementById('alert_box');
if (!alertBox) return;
alertBox.className = 'alert';
}
function loadConfig() {
var xhr = new XMLHttpRequest();
xhr.open('GET', '<%=url("admin", "vpn", "easytier", "get_web_config")%>', true);
xhr.onload = function() {
if (xhr.status === 200) {
var data = JSON.parse(xhr.responseText);
document.getElementById('web_enabled').checked = data.web_enabled === '1';
document.getElementById('web_db_path').value = data.web_db_path || '/etc/easytier/et.db';
document.getElementById('web_protocol').value = data.web_protocol || 'udp';
document.getElementById('web_port').value = data.web_port || '22020';
document.getElementById('web_fw_web').checked = data.web_fw_web === '1';
document.getElementById('web_api_port').value = data.web_api_port || '11211';
document.getElementById('web_html_port').value = data.web_html_port || '11211';
document.getElementById('web_fw_api').checked = data.web_fw_api === '1';
document.getElementById('web_api_host').value = data.web_api_host || '';
document.getElementById('web_geoip_db').value = data.web_geoip_db || '';
document.getElementById('web_weblog').value = data.web_weblog || 'off';
// 保存原始配置
originalConfig = JSON.parse(JSON.stringify(data));
checkWebStatus();
}
};
xhr.send();
}
function getCurrentConfig() {
return {
web_enabled: document.getElementById('web_enabled').checked ? '1' : '0',
web_db_path: document.getElementById('web_db_path').value,
web_protocol: document.getElementById('web_protocol').value,
web_port: document.getElementById('web_port').value,
web_fw_web: document.getElementById('web_fw_web').checked ? '1' : '0',
web_api_port: document.getElementById('web_api_port').value,
web_html_port: document.getElementById('web_html_port').value,
web_fw_api: document.getElementById('web_fw_api').checked ? '1' : '0',
web_api_host: document.getElementById('web_api_host').value,
web_geoip_db: document.getElementById('web_geoip_db').value,
web_weblog: document.getElementById('web_weblog').value
};
}
function isConfigChanged() {
var current = getCurrentConfig();
for (var key in originalConfig) {
if (originalConfig[key] !== current[key]) {
return true;
}
}
return false;
}
function saveConfig() {
if (!isConfigChanged()) {
showAlert('info', '<%:No changes to save%>');
return;
}
var formData = new FormData();
var config = getCurrentConfig();
for (var key in config) {
formData.append(key, config[key]);
}
var xhr = new XMLHttpRequest();
xhr.open('POST', '<%=url("admin", "vpn", "easytier", "save_web_config")%>', true);
xhr.onload = function() {
if (xhr.status === 200) {
showAlert('success', '<%:Configuration saved successfully%>');
originalConfig = getCurrentConfig();
} else {
showAlert('error', '<%:Save failed%>');
}
};
xhr.onerror = function() {
showAlert('error', '<%:Save failed%>');
};
xhr.send(formData);
}
function checkWebStatus() {
var xhr = new XMLHttpRequest();
xhr.open('GET', '<%=url("admin", "vpn", "easytier", "check_web_status")%>', true);
xhr.onload = function() {
if (xhr.status === 200) {
var data = JSON.parse(xhr.responseText);
var consoleCard = document.getElementById('consoleCard');
var iframe = document.getElementById('webConsoleFrame');
if (data.running && data.port) {
var newSrc = 'http://' + window.location.hostname + ':' + data.port;
// 只在URL变化或iframe未加载时才更新,避免闪烁
if (!iframe.src || iframe.src.indexOf(':' + data.port) === -1) {
iframe.src = newSrc;
}
consoleCard.style.display = 'block';
} else {
consoleCard.style.display = 'none';
if (iframe.src) {
iframe.src = '';
}
}
}
};
xhr.send();
}
window.addEventListener('load', function() {
loadConfig();
checkWebStatus();
setInterval(checkWebStatus, 10000);
});
//]]></script>
<%+footer%>
@@ -1,49 +0,0 @@
<%
local dsp = require "luci.dispatcher"
-%>
<script type="text/javascript">
//<![CDATA[
function clearlog(btn) {
XHR.get('<%=dsp.build_url("admin/vpn/easytier/clear_wlog")%>', null,
function(x, data) {
if(x && x.status == 200) {
var log_textarea = document.getElementById('log_textarea');
log_textarea.innerHTML = "";
log_textarea.scrollTop = log_textarea.scrollHeight;
}
}
);
}
function downloadLog() {
var log_textarea = document.getElementById('log_textarea');
var logContent = log_textarea.value || log_textarea.innerText || log_textarea.innerHTML;
var blob = new Blob([logContent], {type: 'text/plain;charset=utf-8'});
var url = window.URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'easytier-web_log.txt';
document.body.appendChild(a);
a.click();
setTimeout(function() {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 0);
}
XHR.poll(2, '<%=dsp.build_url("admin/vpn/easytier/get_wlog")%>', null,
function(x, data) {
if(x && x.status == 200 && document.getElementById("checkbox1").checked == true) {
var log_textarea = document.getElementById('log_textarea');
log_textarea.innerHTML = x.responseText;
log_textarea.scrollTop = log_textarea.scrollHeight;
}
}
);
//]]>
</script>
<fieldset class="cbi-section" id="_log_fieldset">
<input type="checkbox" id="checkbox1" style="vertical-align:middle;height: auto;"checked><%:Auto Refresh%></input>
<input class="cbi-button cbi-input-remove" type="button" onclick="clearlog()" value="<%:Clear Logs%>" />
<input class="cbi-button cbi-input-apply" type="button" onclick="downloadLog()" value="<%:Download Log%>" />
<textarea id="log_textarea" class="cbi-input-textarea" style="width: 100%;margin-top: 10px;" data-update="change" rows="30" wrap="off" readonly="readonly"></textarea>
</fieldset>
@@ -1,8 +0,0 @@
<%+cbi/valueheader%>
<span style="color: red">
<%
local val = self:cfgvalue(section) or self.default or ""
write(pcdata(val))
%>
</span>
<%+cbi/valuefooter%>
@@ -1,5 +0,0 @@
<%+cbi/valueheader%>
<label class="cbi-value" style="display:inline-block; width: 130px" for="ulfile"><%:Upload Program:%></label>
<input class="cbi-input-file" style="width: 400px" type="file" id="ulfile" name="ulfile" />
<input type="submit" class="cbi-button cbi-input-apply" name="upload" value="<%:Upload%>" />
<%+cbi/valuefooter%>
+313
View File
@@ -72,6 +72,14 @@ msgstr ""
msgid "Allow traffic from WAN to EasyTier virtual network"
msgstr ""
#: applications/luci-app-easytier/luasrc/view/easytier/easytier_unified_log.htm:210
msgid "Stop Auto Refresh"
msgstr ""
#: applications/luci-app-easytier/luasrc/view/easytier/easytier_unified_log.htm:210
msgid "Start Auto Refresh"
msgstr ""
#: applications/luci-app-easytier/luasrc/view/easytier/easytier_log.htm:30
#: applications/luci-app-easytier/luasrc/view/easytier/easytierweb_log.htm:30
msgid "Auto Refresh"
@@ -1090,3 +1098,308 @@ msgstr ""
msgid "If true, allow relay kcp packets from foreign network. default is false (not forward foreign network kcp packets). (--enable-relay-foreign-network-kcp parameter)"
msgstr ""
msgid "Auto Configure Interface"
msgstr ""
msgid "Automatically create and configure the EasyTier network interface"
msgstr ""
msgid "Interface Netmask"
msgstr ""
msgid "Subnet mask for the EasyTier interface (default: 255.0.0.0)"
msgstr ""
msgid "Auto Configure Firewall"
msgstr ""
msgid "Automatically add and manage firewall rules"
msgstr ""
msgid "GitHub Proxy URLs"
msgstr ""
msgid "Used to accelerate GitHub downloads when fetching binaries online"
msgstr ""
msgid "Add"
msgstr ""
msgid "Delete"
msgstr ""
msgid "No proxy configured"
msgstr ""
msgid "No proxy configured, using default proxy"
msgstr ""
msgid "Please enter proxy URL"
msgstr ""
msgid "Invalid URL format"
msgstr ""
msgid "Proxy already exists"
msgstr ""
msgid "Proxy added successfully"
msgstr ""
msgid "Space-separated list of GitHub proxy URLs for downloading binaries. Leave empty to use default proxies."
msgstr ""
msgid "Fallback Version"
msgstr ""
msgid "Fallback version to use when unable to fetch the latest version from GitHub."
msgstr ""
msgid "Logs"
msgstr ""
msgid "EasyTier Core"
msgstr ""
msgid "EasyTier Web"
msgstr ""
msgid "All Levels"
msgstr ""
msgid "Auto Refresh"
msgstr ""
msgid "Stop Auto Refresh"
msgstr ""
msgid "Start Auto Refresh"
msgstr ""
msgid "Clear Logs"
msgstr ""
msgid "Download Log"
msgstr ""
msgid "Loading logs..."
msgstr ""
msgid "No logs available"
msgstr ""
msgid "Logs cleared"
msgstr ""
msgid "Settings"
msgstr ""
msgid "Running"
msgstr ""
msgid "Stopped"
msgstr ""
msgid "Memory"
msgstr ""
msgid "Uptime"
msgstr ""
msgid "Version"
msgstr ""
msgid "Refresh Version"
msgstr ""
msgid "Clear cached version and fetch latest version from GitHub?"
msgstr ""
msgid "Cancel"
msgstr ""
msgid "Confirm"
msgstr ""
msgid "Web Console"
msgstr ""
msgid "Service not running"
msgstr ""
msgid "Collecting data..."
msgstr ""
msgid "New"
msgstr ""
msgid "Unknown"
msgstr ""
msgid "Quick Actions"
msgstr ""
msgid "Restart Service"
msgstr ""
msgid "Restart EasyTier service?"
msgstr ""
msgid "Please wait, operation in progress..."
msgstr ""
msgid "Restarting service..."
msgstr ""
msgid "Upload Program"
msgstr ""
msgid "Upload EasyTier Binary"
msgstr ""
msgid "Drag and drop file here or click to browse"
msgstr ""
msgid "Select File"
msgstr ""
msgid "Uploading..."
msgstr ""
msgid "Supported Files"
msgstr ""
msgid "Core service binary"
msgstr ""
msgid "Command line tool"
msgstr ""
msgid "Web console binary"
msgstr ""
msgid "Embedded web console"
msgstr ""
msgid "Download from"
msgstr ""
msgid "Upload Successful"
msgstr ""
msgid "Binary uploaded successfully. Restart the service to apply changes?"
msgstr ""
msgid "Restart the service to apply changes?"
msgstr ""
msgid "Later"
msgstr ""
msgid "Restart Now"
msgstr ""
msgid "Invalid file name"
msgstr ""
msgid "Only easytier binaries or compressed archives (.zip, .tar.gz, .tar) are allowed"
msgstr ""
msgid "Upload failed"
msgstr ""
msgid "Network error"
msgstr ""
msgid "Service restarted successfully"
msgstr ""
msgid "Restart failed"
msgstr ""
msgid "No changes to save"
msgstr ""
msgid "Configuration"
msgstr ""
msgid "Save Configuration"
msgstr ""
msgid "Configuration saved successfully"
msgstr ""
msgid "Save failed"
msgstr ""
msgid "Recommended"
msgstr ""
msgid "Will be ignored, use embed version instead"
msgstr ""
msgid "Note"
msgstr ""
msgid "Files will be uploaded to the paths specified above"
msgstr ""
msgid "EasyTier console uses easytier-web-embed. The easytier-web binary will be rejected during upload validation."
msgstr ""
msgid "Compressed archive containing binaries"
msgstr ""
msgid "Drag *.tar.gz / *.zip / easytier binaries here or click to select file"
msgstr ""
msgid "Used when unable to fetch the latest version during online download"
msgstr ""
msgid "Error: Program not running! Please start the program and refresh"
msgstr ""
msgid "Status"
msgstr ""
msgid "Failed to connect to RPC server. Please check if the RPC port is set to 15888 in the configuration."
msgstr ""
msgid "Self-hosted Web Console"
msgstr ""
msgid "Enable Self-hosted Web Console"
msgstr ""
msgid "Log Level"
msgstr ""
msgid "Runtime log located at"
msgstr ""
msgid "viewable in the log section"
msgstr ""
msgid "Log levels"
msgstr ""
msgid "parameter"
msgstr ""
msgid "Example"
msgstr ""
msgid "current device IP or domain"
msgstr ""
msgid "API port"
msgstr ""
msgid "GeoIP2 database file path used to locate the client"
msgstr ""
msgid "Defaults to an embedded file (country-level information only)"
msgstr ""
msgid "GEOIP Database Path"
msgstr ""
File diff suppressed because it is too large. Load diff
+54 -35
View File
@@ -72,15 +72,27 @@ get_tz() {
fi
check_bin() {
# 优先使用 UCI 配置的代理列表(list 类型),否则使用默认值
local proxys=""
local proxy_list=$(uci -q get easytier.@easytier[0].github_proxys)
# 判断是否有多个代理
if [ ! -z "$proxy_list" ]; then
if [[ "$(grep "list github_proxys" /etc/config/easytier | awk '{print $3}' | wc -l)" -eq 1 ]]; then
proxys="$proxy_list"
else
proxys=$(cat /etc/config/easytier | grep 'list github_proxys' | awk -F 'list github_proxys' '{print $2}' | sed "s/'/\n/g" | tr -d " ' ")
fi
fi
# 如果没有配置代理,使用默认值
if [ -z "$proxys" ]; then
proxys="https://ghproxy.net/ https://gh-proxy.com/ https://cdn.gh-proxy.com/ https://ghfast.top/"
fi
curltest=`which curl`
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
#github加速地址,一行一个 你可以自建改为你自己的
proxys="
https://ghproxy.net/
https://gh-proxy.com/
https://cdn.gh-proxy.com/
https://ghfast.top/
"
if [ -z "$curltest" ] || [ ! -s "`which curl`" ] ; then
tag="$( wget --no-check-certificate -T 5 -t 3 --user-agent "$user_agent" --max-redirect=0 --output-document=- https://api.github.com/repos/EasyTier/EasyTier/releases/latest 2>&1 | grep 'tag_name' | cut -d\" -f4 )"
[ -z "$tag" ] && tag="$( wget --no-check-certificate -T 5 -t 3 --user-agent "$user_agent" --quiet --output-document=- https://api.github.com/repos/EasyTier/EasyTier/releases/latest 2>&1 | grep 'tag_name' | cut -d\" -f4 )"
@@ -396,25 +408,7 @@ start_et() {
etcmd="$(uci -q get easytier.@easytier[0].etcmd)"
echo "$(date '+%Y-%m-%d %H:%M:%S') easytier : easytier-core_${cpucore}开始启动" >>/tmp/easytier.log
path=$(dirname "$easytierbin")
if [[ -f /tmp/easytier-core || -f /tmp/easytier-cli ]] && [[ "${path:0:4}" != "/tmp" ]]; then
chmod +x /tmp/easytier-core 2>/dev/null
chmod +x /tmp/easytier-cli 2>/dev/null
mkdir -p "$path"
echo "$(date '+%Y-%m-%d %H:%M:%S') easytier : 找到上传的程序/tmp/easytier-core,替换为$easytierbin " >>/tmp/easytier.log
upsize="$(du -k /tmp/easytier-core | cut -f1)"
result=$(expr $size - $upsize)
[ -z "$size"] && echo "$(date '+%Y-%m-%d %H:%M:%S') easytier : 获取可用空间失败,请使用ssh手动上传至 ${easytierbin} ${path}/easytier-cli" >>/tmp/easytier.log
if [ "$(($(/tmp/easytier-core -h 2>&1 | wc -l)))" -gt 3 ] && [ "$result" -gt 1000 ] ; then
mv -f /tmp/easytier-core "$easytierbin" 2>/dev/null
mv -f /tmp/easytier-cli "${path}/easytier-cli" 2>/dev/null
else
if [ -z "$size" ] && [ "$(($(/tmp/easytier-core -h 2>&1 | wc -l)))" -gt 3 ] ; then
mv -f /tmp/easytier-core "$easytierbin" 2>/dev/null
mv -f /tmp/easytier-cli "${path}/easytier-cli" 2>/dev/null
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') easytier : 无法替换,上传的程序不完整或自定义路径的可用空间不足,当前空间剩余${size}kb" >>/tmp/easytier.log
fi
fi
if [ ! -f "$easytierbin" ] && [ "$size" -lt 5000 ] && [ ! -z "$size" ] ; then
echo "$(date '+%Y-%m-%d %H:%M:%S') easytier : 自定义程序路径的可用空间不足,当前可用空间剩余${size}kb,自动切换为内存/tmp/easytier-core" >>/tmp/easytier.log
sed -i "/easytierbin/c option easytierbin '/tmp/easytier-core' " /etc/config/easytier
@@ -761,17 +755,28 @@ start_service() {
size="$(df -k | awk '/\/overlay$/ {sub(/K$/, "", $4); print $4}')"
[ -z "$size" ] && size="$(df -kP /usr/bin | awk 'NR==2 {print $(NF-2) }')"
if [ -z "$(uci -q get easytier.@easytier[0].web_enabled)" ] ; then
# Migrate old config if needed or just init
# For now just set default 0 if not present
uci -q set easytier.@easytier[0].web_enabled='0'
uci commit easytier
fi
[ "$(uci -q get easytier.@easytier[0].web_enabled)" = "1" ] && start_web
[ "$(uci -q get easytier.@easytier[0].enabled)" = "1" ] && start_et easytier
echo "$(date '+%Y-%m-%d %H:%M:%S') easytier : 重载防火墙和网络配置...会出现短暂断网现象... " >>/tmp/easytier.log
echo "$(date '+%Y-%m-%d %H:%M:%S') easytier : 重载防火墙和网络配置...会出现短暂断网现象... " >>/tmp/easytierweb.log
[ -n "$(uci changes network)" ] && uci commit network && /etc/init.d/network reload >/dev/null 2>&1
[ -n "$(uci changes firewall)" ] && uci commit firewall && /etc/init.d/firewall reload >/dev/null 2>&1
# 延迟重载网络配置,避免与拨号等网络事件冲突
if [ -n "$(uci changes network)" ] || [ -n "$(uci changes firewall)" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') easytier : 检测到配置变更,将在5秒后后台重载... " >>/tmp/easytier.log
(
sleep 5
# 使用锁文件防止重复重载
LOCK_FILE="/var/lock/easytier_reload.lock"
if [ ! -f "$LOCK_FILE" ]; then
touch "$LOCK_FILE"
[ -n "$(uci changes network)" ] && uci commit network && /etc/init.d/network reload >/dev/null 2>&1
[ -n "$(uci changes firewall)" ] && uci commit firewall && /etc/init.d/firewall reload >/dev/null 2>&1
echo "$(date '+%Y-%m-%d %H:%M:%S') easytier : 网络配置重载完成 " >>/tmp/easytier.log
rm -f "$LOCK_FILE"
fi
) &
fi
}
@@ -796,8 +801,22 @@ stop_service() {
uci -q delete firewall.easytier_quic >/dev/null 2>&1
uci -q delete firewall.easytier_wireguard >/dev/null 2>&1
uci -q delete firewall.easytier_socks5 >/dev/null 2>&1
[ -n "$(uci changes network)" ] && uci commit network && /etc/init.d/network reload >/dev/null 2>&1
[ -n "$(uci changes firewall)" ] && uci commit firewall && /etc/init.d/firewall reload >/dev/null 2>&1
# 延迟重载,避免与网络事件冲突
if [ -n "$(uci changes network)" ] || [ -n "$(uci changes firewall)" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') easytier : 停止服务,将在5秒后后台重载配置... " >>/tmp/easytier.log
(
sleep 5
LOCK_FILE="/var/lock/easytier_reload.lock"
if [ ! -f "$LOCK_FILE" ]; then
touch "$LOCK_FILE"
[ -n "$(uci changes network)" ] && uci commit network && /etc/init.d/network reload >/dev/null 2>&1
[ -n "$(uci changes firewall)" ] && uci commit firewall && /etc/init.d/firewall reload >/dev/null 2>&1
rm -f "$LOCK_FILE"
fi
) &
fi
rm -rf /tmp/easytier-cli_peer /tmp/easytier-cli_connector /tmp/easytier-cli_stun /tmp/easytier-cli_route /tmp/easytier-cli_acl >/dev/null 2>&1
rm -rf /tmp/easytier-cli_peer-center /tmp/easytier-cli_vpn-portal /tmp/easytier_cmd /tmp/easytier-cli_node /tmp/easytier-cli_mapped_listener >/dev/null 2>&1
rm -rf /tmp/easytier-*.* /tmp/easytier.log >/dev/null 2>&1
@@ -820,6 +839,6 @@ reload_service() {
service_triggers() {
procd_add_reload_trigger "easytier"
#如果WAN网络接口上线则触发重新启动easytier
procd_add_interface_trigger "interface.*.up" wan /etc/init.d/easytier reload
# procd_add_interface_trigger "interface.*.up" wan /etc/init.d/easytier reload
}
@@ -187,8 +187,18 @@ setup_web_firewall() {
fi
}
# 应用防火墙和网络配置更改
# 应用防火墙和网络配置更改(延迟后台执行)
apply_network_changes() {
[ -n "$(uci changes network)" ] && uci commit network && /etc/init.d/network reload >/dev/null 2>&1
[ -n "$(uci changes firewall)" ] && uci commit firewall && /etc/init.d/firewall reload >/dev/null 2>&1
if [ -n "$(uci changes network)" ] || [ -n "$(uci changes firewall)" ]; then
(
sleep 5
LOCK_FILE="/var/lock/easytier_reload.lock"
if [ ! -f "$LOCK_FILE" ]; then
touch "$LOCK_FILE"
[ -n "$(uci changes network)" ] && uci commit network && /etc/init.d/network reload >/dev/null 2>&1
[ -n "$(uci changes firewall)" ] && uci commit firewall && /etc/init.d/firewall reload >/dev/null 2>&1
rm -f "$LOCK_FILE"
fi
) &
fi
}
@@ -2,7 +2,8 @@
"luci-app-easytier": {
"description": "Grant UCI access for luci-app-easytier",
"read": {
"uci": [ "easytier" ]
"uci": [ "easytier" ],
"cgi-io": [ "upload" ]
},
"write": {
"uci": [ "easytier" ]
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.4.15
PKG_RELEASE:=73
PKG_RELEASE:=74
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
+1 -1
View File
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.4.10
PKG_RELEASE:=29
PKG_RELEASE:=30
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \