mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-29 07:35:13 +08:00
🍓 Sync 2026-04-16 20:36:17
This commit is contained in:
1 parent
ea416c7ab6
commit
aa40804205
24 files changed
+6145
-969
No files matched your search
+9
-5
@@ -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
|
||||
|
||||
@@ -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)?
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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> "
|
||||
.. "<a href=\"http://easytier.cn\" target=\"_blank\">Official Documentation</a> "
|
||||
.. "<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> ")
|
||||
|
||||
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> "
|
||||
.. "<a href=\"http://easytier.cn\" target=\"_blank\">Official Documentation</a> "
|
||||
.. "<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> ")
|
||||
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%>
|
||||
@@ -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
@@ -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" ]
|
||||
|
||||
@@ -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:= \
|
||||
|
||||
@@ -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:= \
|
||||
|
||||
Reference in new issue
Block a user