mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-01 15:03:56 +08:00
🤞 Sync 2026-09-27 22:16:29
This commit is contained in:
1 parent
8ede4a8320
commit
618f51e9dc
459 files changed
+44817
-2191
No files matched your search
@@ -0,0 +1,10 @@
|
||||
# 保证脚本 / lua / shell 配置以 LF 入库, 避免 Windows 编辑后 CRLF 上传杀死 busybox ash
|
||||
* text=eol=lf
|
||||
|
||||
# 二进制例外
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.ico binary
|
||||
*.bin binary
|
||||
*.zip binary
|
||||
*.ipk binary
|
||||
@@ -0,0 +1,2 @@
|
||||
/docs/
|
||||
*.md
|
||||
@@ -1,4 +1,5 @@
|
||||
# Copyright (C) 2016 Openwrt.org
|
||||
# luci-app-caddy - LuCI 管理 Caddy (反向代理 / ACME DNS-01 / Aliyun DDNS)
|
||||
# Copyright (C) 2026 andangel - https://github.com/andangel/luci-app-caddy
|
||||
#
|
||||
# This is free software, licensed under the Apache License, Version 2.0 .
|
||||
#
|
||||
@@ -9,17 +10,18 @@ PKG_NAME:=luci-app-caddy
|
||||
|
||||
LUCI_TITLE:=LuCI support for caddy
|
||||
LUCI_DEPENDS:=+caddy
|
||||
PKG_VERSION:=1.0
|
||||
PKG_RELEASE:=2
|
||||
PKG_VERSION:=2.0
|
||||
PKG_RELEASE:=3
|
||||
|
||||
define Package/$(PKG_NAME)/conffiles
|
||||
/etc/caddy
|
||||
/etc/config/caddy
|
||||
/etc/caddy/Caddyfile
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postrm
|
||||
#!/bin/sh
|
||||
[ -f /etc/caddy/Caddyfile ] && rm -rf /etc/caddy/Caddyfile >/dev/null 2>&1
|
||||
return 0
|
||||
return 0
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postinst
|
||||
|
||||
@@ -1,30 +1,38 @@
|
||||
module("luci.controller.caddy", package.seeall)
|
||||
|
||||
local DL_DEFAULT = "https://github.com/andangel/luci-app-caddy/releases/latest/download/caddy_linux_arm64"
|
||||
local DL_API = "https://api.github.com/repos/andangel/luci-app-caddy/releases/latest"
|
||||
|
||||
function index()
|
||||
entry({"admin", "nas"}, firstchild(), _("NAS") , 45).dependent = false
|
||||
if not nixio.fs.access("/etc/config/caddy") then
|
||||
return
|
||||
end
|
||||
|
||||
local page = entry({"admin", "nas", "caddy"}, alias("admin", "nas", "caddy", "basic"), _("Caddy"), 20)
|
||||
page.dependent = true
|
||||
page.acl_depends = { "luci-app-caddy" }
|
||||
|
||||
entry({"admin", "nas"}, firstchild(), "NAS", 44).dependent = false
|
||||
entry({"admin", "nas", "caddy", "basic"}, cbi("caddy/caddy"), _("基本设置"), 1).leaf = true
|
||||
entry({"admin", "nas", "caddy", "log"}, cbi("caddy/caddy_log"), _("日志"), 2).leaf = true
|
||||
entry({"admin", "nas", "caddy", "caddy_status"}, call("caddy_status")).leaf = true
|
||||
entry({"admin", "nas", "caddy", "get_log"}, call("get_log")).leaf = true
|
||||
entry({"admin", "nas", "caddy", "clear_log"}, call("clear_log")).leaf = true
|
||||
entry({"admin", "nas", "caddy", "admin_info"}, call("admin_info")).leaf = true
|
||||
entry({"admin", "network", "caddy"}, alias("admin", "network", "caddy", "basic"), _("Caddy"), 30)
|
||||
entry({"admin", "network", "caddy", "basic"}, cbi("caddy/caddy"), _("状态"), 1).leaf = true
|
||||
entry({"admin", "network", "caddy", "domain"}, cbi("caddy/domain"), _("域名"), 2).leaf = true
|
||||
entry({"admin", "network", "caddy", "host"}, cbi("caddy/host"), _("主机"), 3).leaf = true
|
||||
entry({"admin", "network", "caddy", "caddyfile"}, cbi("caddy/caddyfile"), _("配置"), 4).leaf = true
|
||||
entry({"admin", "network", "caddy", "log"}, cbi("caddy/caddy_log"), _("日志"), 5).leaf = true
|
||||
entry({"admin", "network", "caddy", "caddy_status"}, call("caddy_status")).leaf = true
|
||||
entry({"admin", "network", "caddy", "get_log"}, call("get_log")).leaf = true
|
||||
entry({"admin", "network", "caddy", "clear_log"}, call("clear_log")).leaf = true
|
||||
entry({"admin", "network", "caddy", "admin_info"}, call("admin_info")).leaf = true
|
||||
entry({"admin", "network", "caddy", "download"}, call("download")).leaf = true
|
||||
entry({"admin", "network", "caddy", "download_status"}, call("download_status")).leaf = true
|
||||
entry({"admin", "network", "caddy", "download_ctl"}, call("download_ctl")).leaf = true
|
||||
end
|
||||
|
||||
function caddy_status()
|
||||
local e={}
|
||||
local sys = require "luci.sys"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local port = tonumber(uci:get_first("caddy", "caddy", "port"))
|
||||
e.port = (port or 12311)
|
||||
-- Web 界面按钮地址: 取第一个 proxy host 的 域名:端口, 没有则留空
|
||||
local weburl = ""
|
||||
local pname = uci:get_first("caddy", "proxy")
|
||||
if pname then
|
||||
local pdom = uci:get("caddy", pname, "domain")
|
||||
local pprt = uci:get("caddy", pname, "port")
|
||||
if pdom then
|
||||
weburl = "http://" .. pdom .. (pprt and (":" .. pprt) or "")
|
||||
end
|
||||
end
|
||||
e.weburl = weburl
|
||||
e.running=luci.sys.call("pidof caddy >/dev/null")==0
|
||||
local tagfile = io.open("/tmp/caddy_time", "r")
|
||||
if tagfile then
|
||||
@@ -57,7 +65,8 @@ function caddy_status()
|
||||
e.caddyram = "unknown"
|
||||
end
|
||||
|
||||
local command4 = io.popen("([ -s /tmp/caddy.tag ] && cat /tmp/caddy.tag ) || (echo `$(uci -q get caddy.@caddy[0].bin_dir) -v |awk '{print $1}' | sed 's/[^0-9.]*//g'` > /tmp/caddy.tag && cat /tmp/caddy.tag)")
|
||||
-- 官方 CLI: caddy version (输出形如 v2.11.4 h1:xxx, 取版本号段)
|
||||
local command4 = io.popen("$(uci -q get caddy.@caddy[0].bin_dir) version 2>/dev/null | awk '{print $1}'")
|
||||
e.caddytag = command4:read("*all")
|
||||
command4:close()
|
||||
if e.caddytag == "" then
|
||||
@@ -83,9 +92,165 @@ function clear_log()
|
||||
end
|
||||
|
||||
function admin_info()
|
||||
local validate = luci.sys.exec("$(uci -q get caddy.@caddy[0].bin_dir) validate --config /etc/caddy/Caddyfile --adapter caddyfile 2>&1")
|
||||
|
||||
-- HOME=/root 兜底: LuCI CGI (uhttpd) 环境没 $HOME/$XDG_CONFIG_HOME,
|
||||
-- caddy validate 会 warn "unable to determine directory for user configuration; falling back to current directory".
|
||||
-- 显式给个家目录就没这条 warn; --config 已显式传, 结果不受 HOME 影响.
|
||||
local validate = luci.sys.exec("env HOME=/root $(uci -q get caddy.@caddy[0].bin_dir) validate --config /etc/caddy/Caddyfile --adapter caddyfile 2>&1")
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({ validate = validate })
|
||||
end
|
||||
|
||||
function download_status()
|
||||
local st = { state = "idle", size = 0, total = 0, speed = 0, msg = "" }
|
||||
local f = io.open("/tmp/caddy_dl.status", "r")
|
||||
if f then
|
||||
for line in f:lines() do
|
||||
local k, v = line:match("^([%a_]+)=(.*)$")
|
||||
if k then st[k] = v end
|
||||
end
|
||||
f:close()
|
||||
end
|
||||
st.size = tonumber(st.size) or 0
|
||||
st.total = tonumber(st.total) or 0
|
||||
st.speed = tonumber(st.speed) or 0
|
||||
-- 看门狗: 活动状态下 status 由下载进程每秒刷新一次并带 ts 时间戳 (该 lua 桥无 os.stat, 不靠文件 mtime),
|
||||
-- 若 >60s 没刷新说明进程已丢失 (被 OOM/清理 SIGKILL 不执行 trap), 状态文件残留会导致 UI 永远下载中
|
||||
local actives = { downloading = 1, paused = 1, verifying = 1 }
|
||||
local ts = tonumber(st.ts) or 0
|
||||
if actives[st.state] and ts > 0 and os.time() - ts > 60 then
|
||||
st.state = "lost"
|
||||
st.speed = 0
|
||||
st.msg = (st.size > 0) and "下载进程已丢失 (保留进度, 点[下载并重启]续传)" or "状态异常 (点[下载并重启]重新开始)"
|
||||
end
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(st)
|
||||
end
|
||||
|
||||
-- 暂停/继续/停止: 写一个控制命令到 /tmp/caddy_dl.ctl, 下载进程每秒轮询处理.
|
||||
-- 暂停=SIGSTOP 冻结 curl (连接仍挂着, 不额外撞 GitHub); 继续=SIGCONT; 停止=杀 curl 留已下部分.
|
||||
function download_ctl()
|
||||
-- 本 runtime (Luci openwrt-25.12 / ucode 桥) 的 luci.http 不提供 readrequest, 用 formvalue 读 POST 字段
|
||||
local act = tostring(luci.http.formvalue("act") or "")
|
||||
local ok, msg = false, ""
|
||||
if act == "pause" or act == "resume" or act == "stop" then
|
||||
local f = io.open("/tmp/caddy_dl.ctl", "w")
|
||||
if f then f:write(act .. "\n"); f:close(); ok = true; msg = "ok" end
|
||||
else
|
||||
msg = "unknown act: " .. act
|
||||
end
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({ ok = ok, msg = msg })
|
||||
end
|
||||
|
||||
function download()
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local bin = uci:get("caddy", "caddy", "bin_dir") or "/usr/sbin/caddy"
|
||||
local u = uci:get("caddy", "caddy", "download_url")
|
||||
local url = (u and u ~= "") and u or DL_DEFAULT
|
||||
local use_sha = (url == DL_DEFAULT) and "1" or "0"
|
||||
|
||||
local sq = function(s) return s:gsub("'", "'\\''") end
|
||||
local L = {
|
||||
"#!/bin/sh",
|
||||
"URL='" .. sq(url) .. "'",
|
||||
"TMP='/tmp/caddy.dl'",
|
||||
"STATUS='/tmp/caddy_dl.status'",
|
||||
"CTL='/tmp/caddy_dl.ctl'",
|
||||
"BIN='" .. sq(bin) .. "'",
|
||||
"USE_SHA=" .. use_sha,
|
||||
"API='" .. DL_API .. "'",
|
||||
"RECEIPT='/tmp/caddy_dl.done'",
|
||||
"CURLPID=0; PAUSED=0; STOPTED=0",
|
||||
"rm -f \"$CTL\"",
|
||||
"",
|
||||
"# 写临时文件再原子 mv, 防状态端点读到半截文件而把 UI 误渲染成 idle",
|
||||
"ws(){ printf 'state=%s\\nsize=%s\\ntotal=%s\\nspeed=%s\\nmsg=%s\\nts=%s\\n' \"$1\" \"$2\" \"$3\" \"$4\" \"$5\" $(date +%s) > \"$STATUS.tmp\"; mv -f \"$STATUS.tmp\" \"$STATUS\"; }",
|
||||
"",
|
||||
"PIDFILE='/tmp/caddy_dl.pid'",
|
||||
"# 去重锁: 已有存活实例则直接退出 (防清理漏杀导致多实例竞争写同一文件 / 互相覆盖 status)",
|
||||
"if [ -f \"$PIDFILE\" ]; then OLD=$(cat \"$PIDFILE\" 2>/dev/null); if [ -n \"$OLD\" ] && kill -0 \"$OLD\" 2>/dev/null; then exit 0; fi; fi",
|
||||
"echo $$ > \"$PIDFILE\"",
|
||||
"trap 'rm -f \"$PIDFILE\"' EXIT",
|
||||
"",
|
||||
"# 取总大小: 发 Range bytes=0-0 只下 1 字节, 从 Content-Range: bytes 0-0/TOTAL 抽分母 (比完整 HEAD 稳); GitHub 抖则重试 3 次",
|
||||
"TOT=0",
|
||||
"i=0",
|
||||
'while [ "$i" -lt 3 ]; do',
|
||||
"TOT=$(curl -s -L -r 0-0 --connect-timeout 10 -D - -o /dev/null \"$URL\" 2>/dev/null | tr -d '\\r' | grep -i 'Content-Range:' | grep -oE '[0-9]+/[0-9]+' | awk -F/ '{print $2}' | head -1)",
|
||||
"if [ -n \"$TOT\" ] && [ \"$TOT\" -gt 0 ] 2>/dev/null; then break; fi",
|
||||
"i=$((i+1)); sleep 2",
|
||||
"done",
|
||||
"ws downloading $(wc -c < \"$TMP\" 2>/dev/null || echo 0) \"$TOT\" 0 \"\"",
|
||||
"",
|
||||
"# 已下载收据: 上次下载且 SHA 通过时, 记下当时的期望 sha (写入在成功替换后). 收据在 /tmp, 重启即清空 → 重启后再点会重新下载(拉到新版).",
|
||||
"# 本次点击若 API 最新 sha == 收据 == 当前二进制 sha, 说明盘上的就是刚下载的版本 → 不重复下载, 直接重启",
|
||||
"SKIP=0",
|
||||
"BINSIZE=$(wc -c < \"$BIN\" 2>/dev/null || echo 0)",
|
||||
"if [ \"$USE_SHA\" -eq 1 ] && [ -f \"$RECEIPT\" ]; then",
|
||||
"ws verifying \"$BINSIZE\" \"$BINSIZE\" 0 '核对已下载版本...'",
|
||||
"API_NOW=$(curl -L -sS --connect-timeout 10 --max-time 20 \"$API\" | grep -oE 'sha256:[0-9a-f]{64}' | head -1 | sed 's/sha256://')",
|
||||
"BIN_SHA=$(sha256sum \"$BIN\" 2>/dev/null | awk '{print $1}')",
|
||||
"if [ \"${#API_NOW}\" -ge 64 ] && [ \"${#BIN_SHA}\" -ge 64 ] && [ \"$(cat \"$RECEIPT\" 2>/dev/null)\" = \"$API_NOW\" ] && [ \"$BIN_SHA\" = \"$API_NOW\" ]; then SKIP=1; fi",
|
||||
"fi",
|
||||
"if [ $SKIP -eq 1 ]; then /etc/init.d/caddy restart >/dev/null 2>&1; ws done \"$BINSIZE\" \"$BINSIZE\" 0 '已是上次下载的最新版, 无需重复下载, 已直接重启 Caddy'; exit 0; fi",
|
||||
"",
|
||||
"# 直接后台运行 curl (不经函数包一层), 使 $! 即真实 curl pid, 暂停/停止(信号)才能直接作用于 curl",
|
||||
"# GitHub 直连不稳: 除网络类错误外, HTTP/2 reset / -C 续传冲突等瞬时错误也按 --retry 自动再来",
|
||||
"# speed-limit/speed-time: 低于 ~100B/s 持续 45s 视为假死, 触发重试 (比干等 5 分钟超时快)",
|
||||
"curl -L -C - -sS --retry 4 --retry-delay 8 --retry-all-errors --connect-timeout 20 --speed-limit 100 --speed-time 45 -o \"$TMP\" \"$URL\" >> /tmp/caddy_dl.log 2>&1 &",
|
||||
"CURLPID=$!; RC=",
|
||||
"LAST=$(wc -c < \"$TMP\" 2>/dev/null || echo 0); LTIME=$(date +%s)",
|
||||
"# 主循环: 每秒刷新进度 + 处理暂停/继续/停止控制命令 (与下载解耦, 控制端点只写 $CTL 文件)",
|
||||
'while kill -0 "$CURLPID" 2>/dev/null; do',
|
||||
'if [ -f "$CTL" ]; then',
|
||||
' CMD=$(cat "$CTL" 2>/dev/null); rm -f "$CTL"',
|
||||
' if [ "$CMD" = "pause" ] && [ $PAUSED -eq 0 ]; then kill -STOP $CURLPID 2>/dev/null; PAUSED=1',
|
||||
' elif [ "$CMD" = "resume" ] && [ $PAUSED -eq 1 ]; then kill -CONT $CURLPID 2>/dev/null; PAUSED=0',
|
||||
' elif [ "$CMD" = "stop" ]; then kill $CURLPID 2>/dev/null; STOPTED=1',
|
||||
' fi',
|
||||
'fi',
|
||||
'if [ $PAUSED -eq 1 ]; then ws paused $(wc -c < "$TMP" 2>/dev/null || echo 0) "$TOT" 0 ""; sleep 1; continue; fi',
|
||||
'SZ=$(wc -c < "$TMP" 2>/dev/null || echo 0)',
|
||||
'NOW=$(date +%s); DT=$((NOW - LTIME)); SP=0',
|
||||
'if [ "$DT" -gt 0 ]; then SP=$(( (SZ - LAST) / DT )); fi',
|
||||
'LAST=$SZ; LTIME=$NOW',
|
||||
'ws downloading "$SZ" "$TOT" "$SP" ""',
|
||||
"# 僵尸检测: curl 死掉但未 wait 收尸时 kill -0 仍成功会卡死主循环, /proc 状态 Z 即已死; 已下满按成功, 否则按失败",
|
||||
'CS=$(awk "{print $3}" /proc/$CURLPID/stat 2>/dev/null)',
|
||||
'if [ "$CS" = "Z" ]; then if [ "$TOT" -gt 0 ] 2>/dev/null && [ "$SZ" -ge "$TOT" ] 2>/dev/null; then RC=0; else RC=128; fi; break; fi',
|
||||
"sleep 1",
|
||||
"done",
|
||||
"# 主循环已判定 (z 检测设了 RC) 则不覆盖; 否则 wait 收尸取真实退出码",
|
||||
'if [ -z "$RC" ]; then wait $CURLPID 2>/dev/null; RC=$?; fi',
|
||||
'SZ=$(wc -c < "$TMP" 2>/dev/null || echo 0)',
|
||||
"if [ $STOPTED -eq 1 ]; then ws stopped \"$SZ\" \"$TOT\" 0 '已停止 (保留已下载部分, 点[下载并重启]可续传)'; exit 0; fi",
|
||||
"if [ \"$RC\" -ne 0 ]; then ws error \"$SZ\" \"$TOT\" 0 '下载失败 (GitHub 波动/网络), 点[下载并重启]可自动续传重试'; exit 0; fi",
|
||||
"if [ \"$USE_SHA\" -eq 1 ]; then",
|
||||
"API_SHA=$(curl -L -sS --connect-timeout 10 --max-time 20 \"$API\" | grep -oE 'sha256:[0-9a-f]{64}' | head -1 | sed 's/sha256://')",
|
||||
"LOC_SHA=$(sha256sum \"$TMP\" 2>/dev/null | awk '{print $1}')",
|
||||
'if [ "${#API_SHA}" -ge 64 ] && [ "${#LOC_SHA}" -ge 64 ] && [ "$API_SHA" != "$LOC_SHA" ]; then',
|
||||
'rm -f "$TMP"; ws error "$SZ" "$TOT" 0 "SHA256 不匹配, 文件可能损坏, 已丢弃, 请重试"; exit 0',
|
||||
"fi",
|
||||
"fi",
|
||||
"# curl -o 落盘不带可执行位, -h 前先 chmod, 否则任何架构 (包括正确 arm64) 都会 Permission denied",
|
||||
"chmod +x \"$TMP\" 2>/dev/null",
|
||||
"if ! \"$TMP\" -h >/dev/null 2>&1; then rm -f \"$TMP\"; ws error \"$SZ\" \"$TOT\" 0 '文件无法执行, 可能架构不匹配 (应为 linux/arm64 静态二进制)'; exit 0; fi",
|
||||
"if ! mv -f \"$TMP\" \"$BIN\"; then ws error \"$SZ\" \"$TOT\" 0 '替换 $BIN 失败'; exit 0; fi",
|
||||
"# 写收据: 本版本已在盘上, 下次点击命中 SKIP 不再重复下载 (仅默认 URL 有 API_SHA)",
|
||||
"[ \"$USE_SHA\" -eq 1 ] && echo \"$API_SHA\" > \"$RECEIPT\" 2>/dev/null",
|
||||
"if /etc/init.d/caddy restart >/dev/null 2>&1; then ws done \"$SZ\" \"$TOT\" 0 '下载完成, 已替换并重启 Caddy'; else ws done \"$SZ\" \"$TOT\" 0 '下载完成并已替换, 但重启 Caddy 失败, 请手动 /etc/init.d/caddy restart'; fi",
|
||||
}
|
||||
local f = io.open("/tmp/caddy_dl.sh", "w")
|
||||
f:write(table.concat(L, "\n") .. "\n")
|
||||
f:close()
|
||||
os.execute("chmod +x /tmp/caddy_dl.sh")
|
||||
-- 先清掉旧的下载进程 (脚本本体 + 它遗留的写 caddy.dl 的 curl 子进程, 否则多实例互相覆盖 status)
|
||||
-- ps 会截断命令行导致漏杀 curl, 故直接扫 /proc/*/cmdline; 用 [.] 自转义 glob 避免匹配到本命令自身
|
||||
os.execute("for p in /proc/[0-9]*; do c=$(tr '\\0' ' ' < $p/cmdline 2>/dev/null); case \"$c\" in *caddy_dl[.]sh*|*caddy[.]dl*) kill ${p##*/} 2>/dev/null;; esac; done; sleep 1; rm -f /tmp/caddy_dl.status")
|
||||
-- nohup 脱离 CGI, 页面刷新也不中断
|
||||
os.execute("nohup sh /tmp/caddy_dl.sh >/dev/null 2>&1 &")
|
||||
luci.http.prepare_content("application/json")
|
||||
-- 注意: call 端点内 _ 未注入 (只在 index 阶段有), 这里用字面量; 该消息仅作启动占位, 随即被进度轮询覆盖
|
||||
luci.http.write_json({ ok = true, msg = "已开始后台下载 (支持断点续传), 自动刷新进度..." })
|
||||
end
|
||||
|
||||
@@ -1,127 +1,56 @@
|
||||
local m, s
|
||||
m = Map("caddy", translate("Caddy"), translate("Caddy 是一个可扩展的服务器平台,具有自动 HTTPS 功能的快速且可扩展的多平台 HTTP/1-2-3 Web 服务器") .. "<br/>" .. "项目地址:" .. [[<a href="https://github.com/caddyserver/caddy" target="_blank">]] .. translate("github.com/caddyserver/caddy") .. "</a> " .. " caddy文档:" .. [[<a href="https://caddyserver.com/docs/" target="_blank">]] .. translate("caddyserver.com/docs/") .. [[</a>]])
|
||||
local m, s, o
|
||||
|
||||
m:section(SimpleSection).template = "caddy/caddy_status"
|
||||
m = Map("caddy", translate("Caddy 状态"),
|
||||
translate("全局配置: 启用 Caddy / 公共开关 / 程序与日志路径. 反向代理 Host 在 [Host] 标签页增删改查") .. "<br/>" ..
|
||||
[[<a href="https://github.com/andangel/luci-app-caddy" target="_blank">]] ..
|
||||
translate("github.com/andangel/luci-app-caddy") .. "</a> " ..
|
||||
[[<a href="https://caddyserver.com/docs/" target="_blank">]] ..
|
||||
translate("caddyserver.com/docs/ (Caddy 官方文档)") .. [[</a>]])
|
||||
|
||||
m:section(SimpleSection).template = "caddy/caddy_status"
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- 全局设置 (单例) - JSON 里的 global 对象, 只有一条, 只有"改", 没有增删
|
||||
----------------------------------------------------------------------
|
||||
s = m:section(TypedSection, "caddy")
|
||||
s.addremove = false
|
||||
s.anonymous = true
|
||||
|
||||
o = s:option(Flag, "enabled", translate("Enabled"))
|
||||
o = s:option(Flag, "enabled", translate("启用 Caddy"))
|
||||
o.rmempty = false
|
||||
o.default = 0
|
||||
o.default = 1
|
||||
|
||||
o = s:option(Button, "btnrm", translate("重启"))
|
||||
o.inputtitle = translate("重启")
|
||||
o.description = translate("在没有修改参数的情况下快速重新启动一次")
|
||||
o = s:option(Button, "restart", translate("重启 Caddy"))
|
||||
o.inputtitle = translate("重启 Caddy")
|
||||
o.description = translate("按全局 + Host 配置重新生成 Caddyfile 并重启 Caddy")
|
||||
o.inputstyle = "apply"
|
||||
o:depends("enabled", "1")
|
||||
o.write = function()
|
||||
os.execute("/etc/init.d/caddy restart")
|
||||
os.execute("/etc/init.d/caddy restart")
|
||||
end
|
||||
|
||||
e=s:option(ListValue,"cmd",translate("启动方式"),
|
||||
translate("自定义配置文件启动,若不懂参数请勿选择自定义"))
|
||||
e:value("默认")
|
||||
e:value("自定义")
|
||||
|
||||
o = s:option(TextValue, "caddyfile", translate("Caddyfile配置文件"),
|
||||
translate("这是caddy的启动配置文件Caddyfile,路径在/etc/caddy/Caddyfile<br>使用的命令是:caddy run --config /etc/caddy/Caddyfile --adapter caddyfile<br>如需设置密码,这里就不能使用明文密码,需要转换<br>转换命令:$(uci -q get caddy.@caddy[0].bin_dir) hash-password --plaintext 新密码"))
|
||||
o.rows = 3
|
||||
o.wrap = "off"
|
||||
o:depends("cmd", "自定义")
|
||||
|
||||
o = s:option(Value, "port", translate("端口"))
|
||||
o.datatype = "and(port,min(1))"
|
||||
o.default = "12311"
|
||||
o:depends("cmd", "默认")
|
||||
|
||||
o = s:option(Flag,"file_pass", translate("启用 认证"))
|
||||
o.datatype = "string"
|
||||
o.default = "0"
|
||||
o:depends("cmd", "默认")
|
||||
|
||||
o = s:option(Value,"file_username", translate("用户名"))
|
||||
o.datatype = "string"
|
||||
o.default = "admin"
|
||||
o:depends("file_pass", "1")
|
||||
|
||||
o = s:option(Value,"file_password", translate("密码"))
|
||||
o.datatype = "string"
|
||||
o.password = true
|
||||
o.default = "123456"
|
||||
o:depends("file_pass", "1")
|
||||
|
||||
o = s:option(Flag, "filezip", translate("启用 压缩"))
|
||||
o.default = 0
|
||||
o:depends("cmd", "默认")
|
||||
|
||||
o = s:option(Flag, "log", translate("启用日志"))
|
||||
o.default = 1
|
||||
o:depends("cmd", "默认")
|
||||
|
||||
o = s:option(Value, "log_dir", translate("日志路径"),
|
||||
translate("日志的存放路径,填写完整的路径及日志文件名<br>建议/tmp里,例如:/tmp/caddy/requests.log"))
|
||||
o.datatype = "string"
|
||||
o.default = "/tmp/caddy/requests.log"
|
||||
o:depends("log", "1")
|
||||
|
||||
o = s:option(Value, "bin_dir", translate("程序路径"),
|
||||
translate("caddy二进制的存放路径,填写完整的路径及caddy名称<br>例如:/usr/bin/caddy 例如:/tmp/caddy<br>可自行编译对应架构的二进制程序:<a href='https://www.right.com.cn/forum/forum.php?mod=viewthread&tid=6006472&highlight=caddy' target='_blank'>编译教程</a>"))
|
||||
o.datatype = "string"
|
||||
o.default = "/usr/bin/caddy"
|
||||
|
||||
o = s:option(Value, "data_dir", translate("指向路径"),
|
||||
translate("指向一个路径,可在web界面访问你的文件,默认为 /mnt"))
|
||||
o.datatype = "string"
|
||||
o.default = "/mnt"
|
||||
o:depends("cmd", "默认")
|
||||
|
||||
o = s:option(Flag, "webdav", translate("启用 webdav"))
|
||||
o.default = "0"
|
||||
o:depends("cmd", "默认")
|
||||
|
||||
o = s:option(Flag,"webdav_pass", translate("启用 认证"))
|
||||
o.datatype = "string"
|
||||
o:depends("webdav", "1")
|
||||
o.default = "0"
|
||||
|
||||
o = s:option(Value,"webdav_username", translate("用户名"))
|
||||
o.datatype = "string"
|
||||
o.default = "admin"
|
||||
o:depends("webdav_pass", "1")
|
||||
|
||||
o = s:option(Value,"webdav_password", translate("密码"))
|
||||
o.datatype = "string"
|
||||
o.password = true
|
||||
o.default = "123456"
|
||||
o:depends("webdav_pass", "1")
|
||||
|
||||
o = s:option(Value,"webdav_port", translate("webdav端口"))
|
||||
o.datatype = "and(port,min(1))"
|
||||
o.default = "12322"
|
||||
o:depends("webdav", "1")
|
||||
|
||||
o = s:option(Flag, "webzip", translate("启用 压缩"))
|
||||
o.default = 0
|
||||
o:depends("webdav", "1")
|
||||
|
||||
o = s:option(Value, "webdav_data_dir", translate("webdav指向路径"),
|
||||
translate("指向一个路径,使用webdav访问你的文件,默认为 /mnt<br>连接地址须加/dav后缀 如: 192.168.1.1:12311/dav"))
|
||||
o.datatype = "string"
|
||||
o:depends("webdav", "1")
|
||||
o.default = "/mnt"
|
||||
|
||||
o = s:option(Flag, "allow_wan", translate("允许从外网访问"))
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Flag, "api", translate("启用 API接口"))
|
||||
o:depends("cmd", "默认")
|
||||
|
||||
o = s:option(Button, "admin_info", translate("检测配置文件"),
|
||||
translate("验证Caddyfile配置文件是否正确,它会模拟启动caddy<br>但是并不会真的启动,会列出详细信息,以便修正配置文件"))
|
||||
|
||||
o = s:option(Button, "validate", translate("检测配置文件"))
|
||||
o.rawhtml = true
|
||||
o.template = "caddy/admin_info"
|
||||
|
||||
--- 主程序 ---
|
||||
o = s:option(Value, "bin_dir", translate("主程序"),
|
||||
translate("caddy 二进制完整路径"))
|
||||
o.default = "/usr/sbin/caddy"
|
||||
o.placeholder = "/usr/sbin/caddy"
|
||||
|
||||
o = s:option(Value, "download_url", translate("下载地址(可选)"),
|
||||
translate("留空用默认 GitHub latest Release. 填了则用该 URL (可换镜像/固定版本). 下载带重试+SHA256 校验"))
|
||||
o.placeholder = "https://github.com/andangel/luci-app-caddy/releases/latest/download/caddy_linux_arm64"
|
||||
|
||||
o = s:option(Button, "download", translate("下载并重启"))
|
||||
o.rawhtml = true
|
||||
o.template = "caddy/download"
|
||||
|
||||
--- 日志 ---
|
||||
o = s:option(Value, "log_dir", translate("日志"),
|
||||
translate("各 Host 的访问日志写入此文件, 建议放 /tmp 以免占用闪存"))
|
||||
o.default = "/tmp/caddy/requests.log"
|
||||
o.placeholder = "/tmp/caddy/requests.log"
|
||||
|
||||
return m
|
||||
@@ -0,0 +1,45 @@
|
||||
local m, s, o
|
||||
|
||||
-- 手动 Caddyfile 编辑: 路径从 UCI caddy_file 取 (默认 /etc/caddy/Caddyfile),
|
||||
-- 与 init.d 里 G_FILE 一致, 避免用户在 [状态] 页改 bin_dir/log_dir 后此处仍写死路径.
|
||||
local uci = require("luci.model.uci").cursor()
|
||||
-- get_first 返回的是 section name 而非字段值, 必须用 get(cfg, section, key) 取字段
|
||||
local cf_file = uci:get("caddy", "caddy", "caddy_file") or "/etc/caddy/Caddyfile"
|
||||
|
||||
local cur_caddyfile
|
||||
local cfh = io.open(cf_file, "r")
|
||||
if cfh then
|
||||
cur_caddyfile = cfh:read("*a")
|
||||
cfh:close()
|
||||
end
|
||||
|
||||
m = Map("caddy", translate("Caddy 配置 (手动)"),
|
||||
translate("直接编辑 当前 Caddyfile (" .. cf_file .. "). 默认载入当前文件. 注意: 在 [主机] 页重启 Caddy 时会按表单重新生成并覆盖此页内容"))
|
||||
|
||||
s = m:section(TypedSection, "caddy")
|
||||
s.addremove = false
|
||||
s.anonymous = true
|
||||
|
||||
o = s:option(TextValue, "caddyfile", translate("Caddyfile"),
|
||||
translate("完整 Caddyfile. 要 Caddy 的任何功能 (forward_proxy / encode / basicauth / reverse_proxy ...) 都写在这里<br/>"
|
||||
.. "如需设置密码, 用命令生成 hash: caddy hash-password --plaintext 新密码"))
|
||||
o.default = cur_caddyfile
|
||||
o.rows = 25
|
||||
o.wrap = "off"
|
||||
|
||||
o = s:option(Button, "apply_caddyfile", translate("写入并重启 Caddy"))
|
||||
o.inputtitle = translate("写入并重启 Caddy")
|
||||
o.description = translate("把上方内容写入当前 Caddyfile 并重启 Caddy (下次 [主机] 页重启 Caddy 会覆盖)")
|
||||
o.inputstyle = "apply"
|
||||
o.write = function(_, value)
|
||||
-- 经 /tmp 中转, 避免写一半坏掉 Caddyfile; 写完 mv 原子替换
|
||||
local tmp = "/tmp/caddyfile_manual.tmp"
|
||||
local f = io.open(tmp, "w")
|
||||
if f then
|
||||
f:write(value or "")
|
||||
f:close()
|
||||
os.execute(string.format("cp '%s' '%s' && rm -f '%s' && /etc/init.d/caddy restart", tmp, cf_file, tmp))
|
||||
end
|
||||
end
|
||||
|
||||
return m
|
||||
@@ -0,0 +1,65 @@
|
||||
local m, s, o
|
||||
local uci = require("luci.model.uci").cursor()
|
||||
|
||||
m = Map("caddy", translate("Caddy Domain"),
|
||||
translate("域名列表 (增删改查). 每条域名对应一个阿里云账号 (AK), 该域名的 TLS 证书和 DDNS 都用这份 AK"))
|
||||
|
||||
--- Domain (多条) - 增删改查, 每条 = 一个域名 + 对应的阿里云 AK + DDNS 参数 ---
|
||||
--- anonymous: 用户点"添加"即可, 无需填 UCI 标识符 (域名含 . 不合 UCI 命名规范) ---
|
||||
s = m:section(TypedSection, "domain")
|
||||
s.addremove = true
|
||||
s.anonymous = true
|
||||
s.nodeps = true
|
||||
s.cfgtitle = "domain_name"
|
||||
|
||||
o = s:option(Value, "domain_name", translate("域名"),
|
||||
translate("例如 example.com. Host 页的监听域名下拉从这里取"))
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "key_id", translate("阿里云 Access Key ID"),
|
||||
translate("该域名的 AliDNS 写权限 AK. 需要 AliyunDNS 的 Read/Write 权限"))
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "key_secret", translate("阿里云 Access Key Secret"))
|
||||
o.password = true
|
||||
o.rmempty = false
|
||||
|
||||
--- DDNS 子块: 勾选 ddns_on 才显示以下字段 ---
|
||||
o = s:option(Flag, "ddns_on", translate("启用 DDNS (更新 A 记录)"),
|
||||
translate("勾选 = 生成的 Caddyfile 包含该域名的 aliyun_ddns 块, 定期把公网 IP 写入 A 记录"))
|
||||
o.default = 0
|
||||
|
||||
o = s:option(TextValue, "ddns_records", translate("额外记录名 (每行一条)"),
|
||||
translate("每行一个子域名前缀, 用换行分隔, 不要带主域名. 例如填 mail 会更新 mail.example.com, 填 www 会更新 www.example.com. 留空 = 只更新 @ 主域 A 记录"))
|
||||
o.rows = 3
|
||||
o.wrap = "off"
|
||||
o:depends("ddns_on", "1")
|
||||
|
||||
o = s:option(ListValue, "ddns_wan_iface", translate("IP 获取方式"),
|
||||
translate("public = 公网查询 (api.ipify.org 等取出口 IP); 选接口 = 直读该网卡 IPv4. 一般填 pppoe-wan, 下拉自动列出路由器实际接口"))
|
||||
o:value("public")
|
||||
local netf = io.popen("ls /sys/class/net 2>/dev/null")
|
||||
if netf then
|
||||
for line in netf:lines() do
|
||||
local name = line:match("%S+")
|
||||
-- 只保留 WAN 口 (名字含 wan, 如 pppoe-wan / eth0-wan), 过滤掉 LAN/WiFi/桥等无关设备
|
||||
if name and name:lower():find("wan", 1, true) then
|
||||
o:value(name)
|
||||
end
|
||||
end
|
||||
netf:close()
|
||||
end
|
||||
o.default = "pppoe-wan"
|
||||
o:depends("ddns_on", "1")
|
||||
|
||||
o = s:option(Value, "ddns_check_interval", translate("检查间隔"),
|
||||
translate("多久检查一次 A 记录是否需要更新"))
|
||||
o.default = "5m"
|
||||
o:depends("ddns_on", "1")
|
||||
|
||||
o = s:option(Value, "ddns_ttl", translate("TTL"),
|
||||
translate("A 记录 TTL, 影响公网 DNS 生效速度"))
|
||||
o.default = "60s"
|
||||
o:depends("ddns_on", "1")
|
||||
|
||||
return m
|
||||
@@ -0,0 +1,80 @@
|
||||
local m, s, gs, o
|
||||
|
||||
m = Map("caddy", translate("Caddy Host"),
|
||||
translate("反向代理 Host 列表 (增删改查). 每条 Host 对应 Caddyfile 里一段 域名:端口 { ... }"))
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- 通用开关 (全局, 单例 @caddy[0]). 放在 Host 列表上方: 跨所有 Host 生效,
|
||||
-- 不是某一条 Host 的属性. 后续全局项 (如日志开关) 也加在这个 section 里
|
||||
----------------------------------------------------------------------
|
||||
gs = m:section(TypedSection, "caddy")
|
||||
gs.addremove = false
|
||||
gs.anonymous = true
|
||||
gs.nodeps = true
|
||||
|
||||
o = gs:option(Flag, "disable_redirect", translate("保留 HTTP (禁用自动跳转 HTTPS)"),
|
||||
translate("勾选 = Caddyfile 写 auto_https disable_redirects; 不勾 = 不写, Caddy 默认 HTTP 自动 301 到 HTTPS"))
|
||||
o.default = 1
|
||||
|
||||
o = gs:option(Flag, "log_on", translate("请求日志"),
|
||||
translate("勾选 = 各 Host 写访问日志到 [状态] 页的 日志 路径; 不勾 = Caddyfile 不生成 log 块, 不写日志, 省资源"))
|
||||
o.default = 1
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Host (多条) - 增删改查, NPM 的 Proxy Hosts
|
||||
----------------------------------------------------------------------
|
||||
s = m:section(TypedSection, "proxy")
|
||||
s.addremove = true
|
||||
s.anonymous = true
|
||||
s.nodeps = true
|
||||
s.cfgtitle = "name"
|
||||
|
||||
o = s:option(Value, "name", translate("名称"),
|
||||
translate("给这条 Host 起个名, 便于区分"))
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Flag, "enabled", translate("启用"))
|
||||
o.default = 1
|
||||
|
||||
o = s:option(ListValue, "domain", translate("监听域名"),
|
||||
translate("从 [Domain] 页的域名列表中选择. 每个域名列两个选项: 裸域 (签 example.com 一张证书, 覆盖裸域) 和 *. 通配符 (签 裸域+通配符 各一张, 覆盖裸域+全部子域, 等价 acme.sh 两条 -d). 只要 Domain 页填了 AK 就一律走 DNS-01, 不依赖 80/443 入站. 需先在 Domain 页创建域名"))
|
||||
o.rmempty = false
|
||||
-- 从 UCI 文件读 domain_name: 直接读 /etc/config/caddy, 匹配有引号和没引号两种风格.
|
||||
-- 比 grep + sed 更稳 (不靠 shell pipeline 转义), 比 uci cursor 更简单 (不依赖 cursor API 版本差异).
|
||||
local f = io.open("/etc/config/caddy", "r")
|
||||
if f then
|
||||
for line in f:lines() do
|
||||
local dn = line:match("option%s+domain_name%s+'([^']+)'")
|
||||
or line:match("option%s+domain_name%s+([%w%.%-]+)")
|
||||
if dn and #dn > 0 then
|
||||
o:value(dn)
|
||||
o:value("*." .. dn)
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
end
|
||||
|
||||
o = s:option(Value, "port", translate("监听端口"),
|
||||
translate("非标端口可防指纹扫描, 例如 443 / 8443"))
|
||||
o.datatype = "and(port,min(1))"
|
||||
o.default = "443"
|
||||
|
||||
o = s:option(Value, "target", translate("转发目标"),
|
||||
translate("内网后端地址, 形如 192.168.1.x:8080"))
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(ListValue, "tls_cert", translate("TLS 证书"),
|
||||
translate("auto = Caddy 自动签发+自动续签 (Domain 页填了 AK 走 DNS-01, 不依赖 80/443 入站); off = 纯 HTTP 不加密 (内网调试用)"))
|
||||
o.default = "auto"
|
||||
o:value("auto")
|
||||
o:value("off")
|
||||
|
||||
o = s:option(Value, "api_key", translate("API Key (可选)"),
|
||||
translate("留空 = Caddy 不做 key 校验 (交给后端); 填值 = Caddy 层校验 Authorization: Bearer <key>"))
|
||||
|
||||
o = s:option(TextValue, "extra", translate("自定义指令 (可选)"),
|
||||
translate("Caddyfile 语法, 原样插入此 Host 块末尾. 用于 path 路由 / rate_limit / encode / basicauth / handle 等表单没覆盖的指令"))
|
||||
o.rows = 6
|
||||
o.wrap = "off"
|
||||
|
||||
return m
|
||||
@@ -4,7 +4,7 @@
|
||||
{
|
||||
btn.disabled = true;
|
||||
btn.value = '<%:Collecting data...%>';
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "nas", "caddy", "admin_info")%>',
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "network", "caddy", "admin_info")%>',
|
||||
null,
|
||||
function(x,rv)
|
||||
{
|
||||
|
||||
@@ -1,124 +1,167 @@
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
/* caddy_log: 纯 LuCI 风格, XHR + DOM 拼接, 无外部框架 (与 caddy_status.htm 一致) */
|
||||
var LOG_URL = '<%=url("admin/network/caddy/get_log")%>';
|
||||
var CLEAR_URL = '<%=url("admin/network/caddy/clear_log")%>';
|
||||
var logPaused = false;
|
||||
var logFilter = 'all';
|
||||
|
||||
function logStatusClass(c) {
|
||||
if (c >= 500) return 'err';
|
||||
if (c >= 400) return 'warn';
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
function logEscape(s) {
|
||||
var d = document.createElement('div');
|
||||
d.appendChild(document.createTextNode(String(s)));
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function logRender() {
|
||||
var tb = document.getElementById('caddy_log_list');
|
||||
var cnt = document.getElementById('caddy_log_count');
|
||||
if (!tb) return;
|
||||
|
||||
XHR.get(LOG_URL, null, function(x) {
|
||||
if (!x || x.status !== 200) return;
|
||||
var logs = [];
|
||||
x.responseText.split('\n').filter(Boolean).forEach(function(l, i) {
|
||||
try {
|
||||
var o = JSON.parse(l);
|
||||
o.__id = i;
|
||||
logs.push(o);
|
||||
} catch (e) { }
|
||||
});
|
||||
logs.reverse();
|
||||
|
||||
if (logFilter !== 'all')
|
||||
logs = logs.filter(function(l) { return logStatusClass(l.status) === logFilter; });
|
||||
|
||||
if (cnt) cnt.innerHTML = logs.length + ' <em><%:条日志%></em>';
|
||||
|
||||
if (logs.length === 0) {
|
||||
tb.innerHTML = '<em><%:暂无日志%></em>';
|
||||
return;
|
||||
}
|
||||
|
||||
tb.innerHTML = logs.map(function(l) {
|
||||
var h = (l.request && l.request.headers) || {};
|
||||
var rows = [];
|
||||
if (l.ts)
|
||||
rows.push(['时间', new Date(l.ts * 1000).toLocaleString()]);
|
||||
if (l.request)
|
||||
rows.push(['访问', l.request.host + l.request.uri]);
|
||||
if (l.request && l.request.method)
|
||||
rows.push(['方法', l.request.method]);
|
||||
if (l.status)
|
||||
rows.push(['状态', l.status]);
|
||||
var ip = (h['Cf-Connecting-Ip'] && h['Cf-Connecting-Ip'][0]) || (l.request && l.request.client_ip);
|
||||
if (ip)
|
||||
rows.push(['IP', ip]);
|
||||
var ua = h['User-Agent'] ? h['User-Agent'][0] : null;
|
||||
if (ua)
|
||||
rows.push(['标识', ua]);
|
||||
if (l.duration)
|
||||
rows.push(['耗时', (l.duration * 1000).toFixed(2) + ' ms']);
|
||||
|
||||
return '<div class="log-card ' + logStatusClass(l.status) + '">' +
|
||||
rows.map(function(r) {
|
||||
return '<div class="log-row"><span class="log-label">' + logEscape(r[0]) + ':</span>' + logEscape(r[1]) + '</div>';
|
||||
}).join('') +
|
||||
'<div class="log-row"><input type="button" class="btn cbi-button view-json" data-idx="' + l.__id + '" value="<%:查看原始 JSON%>" /></div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
var list = document.getElementById('caddy_log_list');
|
||||
list.querySelectorAll('.view-json').forEach(function(btn) {
|
||||
btn.onclick = function() {
|
||||
logShowJson(logs[+btn.getAttribute('data-idx')]);
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function logShowJson(obj) {
|
||||
if (!obj) return;
|
||||
var copy = {};
|
||||
for (var k in obj) { if (k !== '__id') copy[k] = obj[k]; }
|
||||
document.getElementById('log_modal_body').textContent = JSON.stringify(copy, null, 2);
|
||||
document.getElementById('log_modal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function logCloseModal() {
|
||||
document.getElementById('log_modal').style.display = 'none';
|
||||
}
|
||||
|
||||
setInterval(function() {
|
||||
if (!logPaused) logRender();
|
||||
}, 3000);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
logRender();
|
||||
document.getElementById('log_toggle').onclick = function() {
|
||||
logPaused = !logPaused;
|
||||
this.value = logPaused ? '<%:继续刷新%>' : '<%:暂停刷新%>';
|
||||
};
|
||||
document.getElementById('log_clear').onclick = function() {
|
||||
XHR.get(CLEAR_URL, null, function() {
|
||||
var tb = document.getElementById('caddy_log_list');
|
||||
var cnt = document.getElementById('caddy_log_count');
|
||||
if (tb) tb.innerHTML = '<em><%:暂无日志%></em>';
|
||||
if (cnt) cnt.innerHTML = '0 <em><%:条日志%></em>';
|
||||
});
|
||||
};
|
||||
document.getElementById('log_filter').onchange = function() {
|
||||
logFilter = this.value;
|
||||
logRender();
|
||||
};
|
||||
});
|
||||
//]]></script>
|
||||
|
||||
<style>
|
||||
:root { --bg:#ffffff; --card:#f6f8fa; --text:#222; --border:#ddd; --muted:#666; }
|
||||
@media (prefers-color-scheme: dark) { :root { --bg:#0f1115; --card:#1b1e24; --text:#e5e7eb; --border:#333; --muted:#9ca3af; } }
|
||||
body { background:var(--bg); color:var(--text); margin:0; }
|
||||
|
||||
#app { all: unset; display:block; width:100%; padding:12px; font-family:system-ui,-apple-system; text-align:left; }
|
||||
.toolbar { display:flex; justify-content:space-between; align-items:center; margin-bottom:12px; }
|
||||
.toolbar select { margin-left:4px; }
|
||||
|
||||
button { border-radius:6px; padding:6px 10px; border:1px solid var(--border); background:transparent; color:var(--text); cursor:pointer; transition: transform 0.1s ease, box-shadow 0.1s ease, background 0.1s ease; }
|
||||
button:active { transform: scale(0.95); box-shadow: inset 0 2px 4px rgba(0,0,0,0.2); background: rgba(0,0,0,0.05); }
|
||||
button.toggle-btn { background:#d0a026; color:#fff; border:none; }
|
||||
button.clear-log { background:#e74c3c; color:#fff; border:none; }
|
||||
button.view-json { background:#60a5fa; color:#fff; border:none; }
|
||||
|
||||
.log-card { all: unset; display:block; width:100%; padding:12px; margin-bottom:12px; background:var(--card); border-radius:10px; text-align:left; box-sizing:border-box; overflow:hidden; border-width:2px; border-style:solid; border-left-color:#888; }
|
||||
.log-card.ok { border-color:#2ecc71; }
|
||||
.log-card.warn { border-color:#f39c12; }
|
||||
.log-card.err { border-color:#e74c3c; }
|
||||
|
||||
.log-inner { all: unset; display:block; width:100%; text-align:left; overflow-wrap:break-word; word-break:break-word; }
|
||||
.log-row { all: unset; display:flex; width:100%; font-size:13px; line-height:1.8; word-break:break-all; margin-bottom:2px; text-align:left; align-items:flex-start; }
|
||||
.log-row .label { all: unset; flex-shrink:0; color:var(--muted); margin-right:8px; }
|
||||
.log-row a { color:#2563eb; text-decoration:underline; font-size:13px; }
|
||||
|
||||
.modal { display:none; position:fixed; z-index:9999; left:0; top:0; width:100%; height:100%; background-color: rgba(0,0,0,0.5); justify-content:center; align-items:center; overflow:auto; }
|
||||
.modal.show { display:flex; }
|
||||
.modal-content { background:#fff; border-radius:6px; max-width:700px; width:80%; max-height:70vh; overflow:auto; white-space: pre; position:relative; display:flex; flex-direction:column; }
|
||||
|
||||
.modal-header { display:flex; justify-content:space-between; align-items:center; padding:10px 16px; border-bottom:1px solid #ccc; background:#f3f3f3; border-top-left-radius:6px; border-top-right-radius:6px; flex-shrink:0; }
|
||||
.modal-header h3 { margin:0; font-size:16px; }
|
||||
.modal-close { padding:4px 8px; border:none; border-radius:6px; background:#393fec; color:#fff; cursor:pointer; }
|
||||
|
||||
.modal-body { padding:12px; overflow:auto; flex:1; }
|
||||
|
||||
#toTop { position:fixed; right:18px; bottom:120px; padding:8px 14px; border-radius:20px; background:#8644f1; color:#fff; display:none; z-index:9999; cursor:pointer; transition: transform 0.1s ease, box-shadow 0.1s ease, background 0.1s ease; }
|
||||
#toTop.show { display:block; }
|
||||
#toTop:active { transform: scale(0.95); box-shadow: inset 0 2px 4px rgba(0,0,0,0.2); background: rgba(0,0,0,0.05); }
|
||||
|
||||
@media (max-width:600px){ .log-row{font-size:12px;} }
|
||||
|
||||
@media (prefers-color-scheme: dark){
|
||||
body { background:#0f1115; color:#e5e7eb; }
|
||||
.log-card { background:#1b1e24; border-color:#333; color:#e5e7eb; }
|
||||
.modal-content { background:#1b1e24; color:#e5e7eb; }
|
||||
.modal-header { background:#2b2e36; border-bottom-color:#444; }
|
||||
button.toggle-btn { background:#d0a026; color:#fff; }
|
||||
button.clear-log { background:#b91c1c; color:#fff; }
|
||||
button.view-json { background:#2563eb; color:#fff; }
|
||||
.modal-close { background:#393fec; color:#fff; }
|
||||
.log-card {
|
||||
background:rgba(0,0,0,0.04);
|
||||
border-left:3px solid #2ecc71;
|
||||
border-radius:6px;
|
||||
padding:8px 10px;
|
||||
margin-bottom:10px;
|
||||
}
|
||||
.log-card.warn { border-left-color:#f39c12; }
|
||||
.log-card.err { border-left-color:#e74c3c; }
|
||||
.log-row { font-size:13px; line-height:1.8; word-break:break-all; }
|
||||
.log-label { opacity:0.6; margin-right:6px; }
|
||||
#log_modal { display:none; position:fixed; z-index:9999; left:0; top:0; width:100%; height:100%; background:rgba(0,0,0,0.5); }
|
||||
/* 弹窗固定暗色终端风: LuCI bootstrap-dark 主题走自己的 cascade.css,
|
||||
不触发 prefers-color-scheme, 媒体查询不可靠; 且 JSON 适合暗底亮字 */
|
||||
#log_modal .modal-box { background:#1e222a; color:#d8dee6; max-width:700px; width:85%; max-height:70vh; overflow:auto; border-radius:6px; border:1px solid rgba(255,255,255,0.15); margin:8vh auto 0; }
|
||||
#log_modal .modal-box b { color:#fff; }
|
||||
#log_modal_body { margin:8px 12px; white-space:pre-wrap; word-break:break-all; font-size:12px; color:#d8dee6; }
|
||||
</style>
|
||||
|
||||
<div id="app">
|
||||
<div class="toolbar">
|
||||
<fieldset class="cbi-section">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:10px;">
|
||||
<div>
|
||||
<button type="button" class="toggle-btn" @click="toggle">{{ paused ? '继续刷新' : '暂停刷新' }}</button>
|
||||
<button type="button" class="clear-log" @click="clear">清空日志</button>
|
||||
<input type="button" class="btn cbi-button cbi-button-apply" id="log_toggle" value="<%:暂停刷新%>" />
|
||||
<input type="button" class="btn cbi-button" id="log_clear" value="<%:清空日志%>" />
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<label>
|
||||
<select v-model="filterType">
|
||||
<option value="all">全部</option>
|
||||
<option value="warn">警告</option>
|
||||
<option value="err">错误</option>
|
||||
</select>
|
||||
</label>
|
||||
<span>共 {{ filteredLogs.length }} 条日志</span>
|
||||
<div>
|
||||
<select id="log_filter">
|
||||
<option value="all"><%:全部%></option>
|
||||
<option value="warn"><%:警告%></option>
|
||||
<option value="err"><%:错误%></option>
|
||||
</select>
|
||||
<span id="caddy_log_count">0 <em><%:条日志%></em></span>
|
||||
</div>
|
||||
</div>
|
||||
<p id="caddy_log_list"><em><%:加载中...%></em></p>
|
||||
</fieldset>
|
||||
|
||||
<div v-for="log in filteredLogs" :key="log.id" class="log-card" :class="statusClass(log.status)">
|
||||
<div class="log-inner">
|
||||
<div class="log-row" v-if="log.ts"><span class="label">时间:</span>{{ time(log.ts) }}</div>
|
||||
<div class="log-row" v-if="log.request"><span class="label">访问:</span>{{ fullUrl(log) }}</div>
|
||||
<div class="log-row" v-if="log.request && log.request.method"><span class="label">方法:</span>{{ log.request.method }}</div>
|
||||
<div class="log-row" v-if="log.status"><span class="label">状态:</span>{{ log.status }}</div>
|
||||
<div class="log-row" v-if="realIp(log)"><span class="label">IP:</span>{{ realIp(log) }}</div>
|
||||
<div class="log-row" v-if="userAgent(log)"><span class="label">标识:</span>{{ userAgent(log) }}</div>
|
||||
<div class="log-row" v-if="log.duration"><span class="label">耗时:</span>{{ (log.duration*1000).toFixed(2) }} ms</div>
|
||||
|
||||
<div class="log-row">
|
||||
<button type="button" class="view-json" @click.prevent="showModal(log)">查看原始 JSON 日志</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" :class="{show: modalLog}" @click.self="closeModal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>完整日志</h3>
|
||||
<button class="modal-close" @click.prevent="closeModal">关闭</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre>{{ modalLog ? JSON.stringify(modalLog,null,2) : '' }}</pre>
|
||||
</div>
|
||||
<div id="log_modal" onclick="if(event.target===this)logCloseModal()">
|
||||
<div class="modal-box">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #ccc;">
|
||||
<b><%:完整日志%></b>
|
||||
<input type="button" class="btn cbi-button" value="<%:关闭%>" onclick="logCloseModal()" />
|
||||
</div>
|
||||
<pre id="log_modal_body"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toTop">返回顶部</div>
|
||||
|
||||
<script>
|
||||
const { createApp } = Vue;
|
||||
createApp({
|
||||
data(){ return { logs: [], paused:false, filterType:'all', modalLog:null }; },
|
||||
computed:{ filteredLogs(){ if(this.filterType==='all') return this.logs; return this.logs.filter(l=>this.statusClass(l.status)===this.filterType); }},
|
||||
methods:{
|
||||
fetch(){ if(this.paused) return; XHR.get('<%=url([[admin]], [[nas]], [[caddy]], [[get_log]])%>', null, x=>{ if(x && x.status===200){ this.logs = x.responseText.split('\n').filter(Boolean).map((l,i)=>{ try{ let obj=JSON.parse(l); obj.id=i; return obj; }catch{return null;} }).filter(Boolean).reverse(); } }); },
|
||||
toggle(){ this.paused = !this.paused; },
|
||||
clear(){ XHR.get('<%=url([[admin]], [[nas]], [[caddy]], [[clear_log]])%>'); this.logs=[]; },
|
||||
time(ts){ return new Date(ts*1000).toLocaleString(); },
|
||||
fullUrl(l){ const h=l.request.headers||{}; const p=h['X-Forwarded-Proto']?h['X-Forwarded-Proto'][0]:'http'; return p+'://'+l.request.host+l.request.uri; },
|
||||
realIp(l){ const h=l.request.headers||{}; return h['Cf-Connecting-Ip']?h['Cf-Connecting-Ip'][0]:l.request.client_ip; },
|
||||
userAgent(l){ const h=l.request.headers||{}; return h['User-Agent']?h['User-Agent'][0]:'未知'; },
|
||||
statusClass(c){ if(c>=500) return 'err'; if(c>=400) return 'warn'; return 'ok'; },
|
||||
showModal(log){ this.modalLog = log; document.body.style.overflow = 'hidden'; },
|
||||
closeModal(){ this.modalLog = null; document.body.style.overflow = ''; }
|
||||
},
|
||||
mounted(){ this.fetch(); setInterval(this.fetch,2000); const btn=document.getElementById('toTop'); btn.onclick=()=>window.scrollTo({top:0,behavior:'smooth'}); window.addEventListener('scroll',()=>btn.classList.toggle('show',window.scrollY>400)); }
|
||||
}).mount('#app');
|
||||
</script>
|
||||
@@ -1,26 +1,19 @@
|
||||
<%
|
||||
local uci = require 'luci.model.uci'.cursor()
|
||||
ssl = uci:get_first('caddy', 'caddy', 'ssl')
|
||||
if ssl == '1' then
|
||||
protocol="http://"
|
||||
else
|
||||
protocol="http://"
|
||||
end
|
||||
%>
|
||||
%>
|
||||
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
XHR.poll(5, '<%=url("admin/nas/caddy/caddy_status")%>', null,
|
||||
XHR.poll(5, '<%=url("admin/network/caddy/caddy_status")%>', null,
|
||||
function(x, st) {
|
||||
var tb = document.getElementById('caddy_status');
|
||||
if (st && tb) {
|
||||
|
||||
var caddyStatus = st.running ? "<span style='color:green;'> 运行中</span><img src='https://www.right.com.cn/forum/data/attachment/album/202401/30/073058is17bcmu7z3nmn2b.gif' width='30px' alt=''>" : "<span style='color:red;'> 未运行</span>";
|
||||
var caddyStatus = st.running ? "<span style='color:green;'> 运行中</span>" : "<span style='color:red;'> 未运行</span>";
|
||||
var caddystaContent = st.running ? "已运行:" + "<span style='color:#DA70D6;'>" + st.caddysta + "</span>" : "";
|
||||
var caddycpuContent = st.running ? "<br>" + "CPU占用:" + "<span style='color:#6A5ACD;'>" + st.caddycpu + "</span>" : "";
|
||||
var caddyramContent = st.running ? " " + " 内存占用:" + "<span style='color:#0000CD;'>" + st.caddyram + "</span>" : "";
|
||||
var caddytagContent = st.running ? "<br>" + "当前版本:" + "<span style='color:#CD853F;'>" + st.caddytag + "</span>" : "";
|
||||
var caddynewtagContent = st.running ? " " + "最新版本:" + "<span style='color:#FFA500;'>" + st.caddynewtag + "</span>" : "";
|
||||
var caddywebContent = st.running ? "<input class=\"cbi-button cbi-button-reload mar-10\" type=\"button\" value=\" <%:Web 界面%> \" onclick=\"window.open('<%=protocol%>" + window.location.hostname + ":" + st.port + "/')\"/>" : "";
|
||||
var caddywebContent = (st.running && st.weburl) ? "<input class=\"cbi-button cbi-button-reload mar-10\" type=\"button\" value=\" <%:Web 界面%> \" onclick=\"window.open('" + st.weburl + "/')\"/>" : "";
|
||||
tb.innerHTML = "<b><em>Caddy " + caddyStatus + caddystaContent + caddycpuContent + caddyramContent + caddytagContent + caddynewtagContent + caddywebContent + "</b></em>";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<%+cbi/valueheader%>
|
||||
<span style="color:#808080"><%:后台下载 (断点续传). 点击后进度自动刷新: 已下载 / 总大小 / 速度 / 百分比. 中途刷新页面不会中断, 可暂停/继续/停止, 再点[下载并重启]会从断点继续%></span>
|
||||
<div style="margin:6px 0">
|
||||
<input type="button" id="<%=self.option%>-btn" class="btn cbi-button cbi-button-apply" value="<%:下载并重启%>" style="width:auto" onclick="return caddy_dl_start(this)" />
|
||||
<input type="button" id="<%=self.option%>-btn-pause" class="btn cbi-button cbi-button-edit" value="<%:暂停%>" style="width:auto;display:none" onclick="return cdl_ctl('pause')" />
|
||||
<input type="button" id="<%=self.option%>-btn-resume" class="btn cbi-button cbi-button-edit" value="<%:继续%>" style="width:auto;display:none" onclick="return cdl_ctl('resume')" />
|
||||
<input type="button" id="<%=self.option%>-btn-stop" class="btn cbi-button cbi-button-remove" value="<%:停止%>" style="width:auto;display:none" onclick="return cdl_ctl('stop')" />
|
||||
<span id="<%=self.option%>-pct" style="margin-left:10px;font-family:monospace"></span>
|
||||
</div>
|
||||
<div id="<%=self.option%>-barbox" style="display:none;width:100%;max-width:520px;background:#2b2b2b;border:1px solid #555;border-radius:4px;height:18px;overflow:hidden;margin:4px 0">
|
||||
<div id="<%=self.option%>-bar" style="height:100%;width:0%;background:#008000;transition:width 1s linear"></div>
|
||||
</div>
|
||||
<div id="<%=self.option%>-stat" style="font-family:monospace;color:#bbb;font-size:12px"></div>
|
||||
<div id="<%=self.option%>-msg"></div>
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
var cdl_tid = null;
|
||||
// 点击后的宽限窗口(ms 时间戳): 控制器先删旧 status 再启脚本, 新脚本要过 TOT 探测(几秒~几十秒)才写第一个 status,
|
||||
// 这段空窗轮询回来是 idle, 若按普通 idle 停轮询就再也没人刷新了 → 窗口内的 idle 保持轮询, 终态(done/error/stopped)照常停
|
||||
var cdl_until = 0;
|
||||
function cdl_mb(n){ return (n/1048576).toFixed(2) + ' MB'; }
|
||||
function cdl_esc(s){ return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
function cdl_render(rv)
|
||||
{
|
||||
var bar = document.getElementById('<%=self.option%>-bar');
|
||||
var box = document.getElementById('<%=self.option%>-barbox');
|
||||
var pct = document.getElementById('<%=self.option%>-pct');
|
||||
var stat = document.getElementById('<%=self.option%>-stat');
|
||||
var msg = document.getElementById('<%=self.option%>-msg');
|
||||
var btn = document.getElementById('<%=self.option%>-btn');
|
||||
var bP = document.getElementById('<%=self.option%>-btn-pause');
|
||||
var bR = document.getElementById('<%=self.option%>-btn-resume');
|
||||
var bS = document.getElementById('<%=self.option%>-btn-stop');
|
||||
var size = rv.size || 0;
|
||||
var total = rv.total || 0;
|
||||
var speed = rv.speed || 0;
|
||||
var text = cdl_mb(size) + (total ? ' / ' + cdl_mb(total) : '');
|
||||
if (total > 0) {
|
||||
var p = Math.floor(size * 100 / total);
|
||||
text += ' · ' + p + '%';
|
||||
}
|
||||
if (speed > 0) text += ' · ' + cdl_mb(speed) + '/s';
|
||||
pct.innerHTML = text;
|
||||
var st = rv.state || 'idle';
|
||||
// 定时器由这里统一管理: 下载/暂停/校验中保持轮询, 结束(done/error/stopped)则停
|
||||
// 窗口内的 idle(启动空窗)也算活动, 否则首次轮询落在空窗里就把轮询停死, UI 永远 idle
|
||||
var active = (st === 'downloading' || st === 'paused' || st === 'verifying' || (st === 'idle' && Date.now() < cdl_until));
|
||||
if (active && !cdl_tid) cdl_tid = setInterval(cdl_poll, 1500);
|
||||
else if (!active && cdl_tid) { clearInterval(cdl_tid); cdl_tid = null; }
|
||||
// 进度条
|
||||
if (bar && box) {
|
||||
if (st === 'downloading' || st === 'paused' || st === 'done' || st === 'verifying') box.style.display = 'block';
|
||||
var w = total ? Math.min(100, Math.floor(size*100/total)) : 0;
|
||||
bar.style.width = w + '%';
|
||||
}
|
||||
// 主按钮: 有已下载部分(error/stopped/有进度)时点它就是续传
|
||||
if (btn) {
|
||||
if (st === 'downloading') { btn.disabled = true; btn.value = '<%:下载中...%>'; } else if (st === 'lost') { btn.disabled = false; btn.value = '<%:重新下载 (自动续传)%>'; } else if (st === 'paused') { btn.disabled = true; btn.value = '<%:已暂停 (点[继续])%>'; }
|
||||
else if (st === 'verifying') { btn.disabled = true; btn.value = '<%:校验/替换中...%>'; }
|
||||
else if (st === 'stopped' || st === 'lost') { btn.disabled = false; btn.value = '<%:继续下载 (自动续传)%>'; }
|
||||
else if (st === 'error') { btn.disabled = false; btn.value = '<%:重试 (自动续传)%>'; }
|
||||
else { btn.disabled = false; btn.value = '<%:下载并重启%>'; }
|
||||
}
|
||||
// 暂停/继续/停止 控制按钮的显示与可用性
|
||||
if (bP && bR && bS) {
|
||||
var showCtl = (st === 'downloading' || st === 'paused');
|
||||
bP.style.display = showCtl ? '' : 'none'; bP.disabled = (st !== 'downloading');
|
||||
bR.style.display = showCtl ? '' : 'none'; bR.disabled = (st !== 'paused');
|
||||
bS.style.display = showCtl ? '' : 'none'; bS.disabled = false;
|
||||
}
|
||||
if (stat) {
|
||||
var stmap = { idle:'', downloading:'<%:下载中...%>', paused:'<%:已暂停%>', verifying:'<%:校验中...%>', done:'<%:完成%>', stopped:'<%:已停止%>', lost:'<%:进程丢失%>', error:'<%:出错%>' };
|
||||
stat.innerHTML = (stmap[st] || st) + ' — ' + text;
|
||||
}
|
||||
if (msg) {
|
||||
if (st === 'done') msg.innerHTML = "<font color='#008000'>" + cdl_esc(rv.msg) + "</font>";
|
||||
else if (st === 'error' || st === 'lost') msg.innerHTML = "<font color='#DB7093'>" + cdl_esc(rv.msg || '') + "</font>";
|
||||
else if (st === 'stopped' || st === 'paused') msg.innerHTML = "<font color='#aaaaaa'>" + cdl_esc(rv.msg || '') + "</font>";
|
||||
else msg.innerHTML = "";
|
||||
}
|
||||
}
|
||||
function cdl_poll()
|
||||
{
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin","network","caddy","download_status")%>', null, function(x, rv){ cdl_render(rv); });
|
||||
}
|
||||
function cdl_ctl(act)
|
||||
{
|
||||
XHR.post('<%=luci.dispatcher.build_url("admin","network","caddy","download_ctl")%>', { act: act }, function(){ cdl_poll(); });
|
||||
return false;
|
||||
}
|
||||
function caddy_dl_start(btn)
|
||||
{
|
||||
btn.disabled = true; btn.value = '<%:启动中...%>';
|
||||
cdl_until = Date.now() + 60000; // 60s 宽限: 覆盖 控制器删旧 status → 新脚本 TOT 探测 → 第一个进度 的空窗
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin","network","caddy","download")%>', null, function(x, rv){
|
||||
cdl_poll();
|
||||
});
|
||||
return false;
|
||||
}
|
||||
// 页面加载时若上次在后台下载, 恢复进度显示
|
||||
window.addEventListener('load', function(){ cdl_poll(); });
|
||||
//]]></script>
|
||||
<%+cbi/valuefooter%>
|
||||
@@ -1,49 +1,59 @@
|
||||
# 此配置文件路径:/etc/caddy/Caddyfile
|
||||
|
||||
{ # 全局配置
|
||||
# 启动 cgi 模块
|
||||
order cgi before respond
|
||||
# 启动 webdav 模块
|
||||
order webdav before file_server
|
||||
# 关闭 API 端口
|
||||
admin off
|
||||
}
|
||||
# 此配置文件路径: /etc/caddy/Caddyfile
|
||||
# 下面是 proxy 模式生成 Caddyfile 的样例 (通用值).
|
||||
# 手动模式 (Caddyfile) 下, 本文件内容即你实际使用的配置, 可任意写 Caddy 支持的语法.
|
||||
|
||||
:12311 {
|
||||
#启用压缩
|
||||
encode gzip
|
||||
#认证,账号admin 密码123456
|
||||
basicauth {
|
||||
admin $2a$14$RdbOHzJhf5BaapSdlYTCbe.yWY9cEZjyDpfgwStY28K/qsM1tX8tu
|
||||
}
|
||||
#路径 /mnt
|
||||
root * /mnt
|
||||
#文件服务器 端口12311
|
||||
file_server browse
|
||||
|
||||
log {
|
||||
#日志 路径/tmp/caddy/requests.log
|
||||
output file /tmp/caddy/requests.log {
|
||||
roll_size 1MiB
|
||||
roll_local_time
|
||||
roll_keep 5
|
||||
roll_keep_for 120h
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto_https disable_redirects
|
||||
|
||||
aliyun_ddns {
|
||||
access_key_id <ACCESS_KEY_ID>
|
||||
access_key_secret <ACCESS_KEY_SECRET>
|
||||
region_id cn-hangzhou
|
||||
|
||||
domains {
|
||||
example.com @
|
||||
}
|
||||
|
||||
versions ipv4
|
||||
ip_source interface pppoe-wan
|
||||
check_interval 5m
|
||||
ttl 60s
|
||||
}
|
||||
}
|
||||
|
||||
:12322 {
|
||||
#启用压缩
|
||||
encode gzip
|
||||
#认证,账号admin 密码123456
|
||||
basicauth {
|
||||
admin $2a$14$RdbOHzJhf5BaapSdlYTCbe.yWY9cEZjyDpfgwStY28K/qsM1tX8tu
|
||||
}
|
||||
example.com:8443 {
|
||||
tls {
|
||||
dns alidns {
|
||||
access_key_id <ACCESS_KEY_ID>
|
||||
access_key_secret <ACCESS_KEY_SECRET>
|
||||
}
|
||||
}
|
||||
|
||||
# webdav 路径根目录 端口12322
|
||||
webdav /dav/* {
|
||||
prefix /dav
|
||||
root /
|
||||
}
|
||||
header -Server
|
||||
|
||||
request_body max_size 0
|
||||
|
||||
@health path /health
|
||||
@apikey header Authorization "Bearer <API_KEY>"
|
||||
|
||||
handle @health {
|
||||
reverse_proxy 192.168.1.x:8080 { flush_interval -1 }
|
||||
}
|
||||
|
||||
handle @apikey {
|
||||
reverse_proxy 192.168.1.x:8080 { flush_interval -1 }
|
||||
}
|
||||
|
||||
handle {
|
||||
respond 404 "Not Found" { close }
|
||||
}
|
||||
|
||||
log {
|
||||
output file /tmp/caddy/requests.log {
|
||||
roll_size 1MiB
|
||||
roll_local_time
|
||||
roll_keep 5
|
||||
roll_keep_for 120h
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,28 @@
|
||||
# luci-app-caddy 默认配置
|
||||
#
|
||||
# 数据模型:
|
||||
# config caddy -> 全局 (单条): 启用 / 路径 / 重定向开关
|
||||
# config domain -> 域名 (多条, 增删): 每条 = 一个域名 + AK + DDNS 参数
|
||||
# config proxy -> Host (多条, 增删): 每条 = 一段 Caddyfile 的 域名:端口 { ... }
|
||||
#
|
||||
# domain 示例 (在 LuCI [Domain] 页点 添加 创建):
|
||||
# config domain 'd1'
|
||||
# option domain_name 'example.com'
|
||||
# option key_id '<AK_ID>'
|
||||
# option key_secret '<AK_SECRET>'
|
||||
# option ddns_on '0'
|
||||
#
|
||||
# proxy 示例 (在 LuCI [Host] 页点 添加 创建):
|
||||
# config proxy 'host1'
|
||||
# option name 'my-host'
|
||||
# option enabled '1'
|
||||
# option domain 'example.com'
|
||||
# option port '8443'
|
||||
# option tls '1'
|
||||
# option target '192.168.1.x:8080'
|
||||
|
||||
config caddy
|
||||
option 'enabled' '0'
|
||||
option cmd '默认'
|
||||
option 'port' '12311'
|
||||
option 'webdav' '0'
|
||||
option 'filezip' '0'
|
||||
option 'webzip' '0'
|
||||
option 'cgi' '0'
|
||||
option 'api' '0'
|
||||
option 'data_dir' '/mnt'
|
||||
option 'file_pass' '0'
|
||||
option 'log' '1'
|
||||
option 'log_dir' '/tmp/caddy/requests.log'
|
||||
option allow_wan '0'
|
||||
option enabled '0'
|
||||
option disable_redirect '1'
|
||||
option bin_dir '/usr/sbin/caddy'
|
||||
option log_dir '/tmp/caddy/requests.log'
|
||||
@@ -2,178 +2,315 @@
|
||||
|
||||
START=99
|
||||
USE_PROCD=1
|
||||
PROG="$(uci -q get caddy.@caddy[0].bin_dir)"
|
||||
|
||||
get_config() {
|
||||
config_get_bool enabled $1 enabled 0
|
||||
config_get cmd $1 cmd "默认"
|
||||
config_get_bool webdav $1 webdav 0
|
||||
config_get port $1 port 12311
|
||||
config_get webdav_port $1 webdav_port 12322
|
||||
config_get log $1 log 1
|
||||
config_get data_dir $1 data_dir "/mnt"
|
||||
config_get webdav_data_dir $1 webdav_data_dir "/mnt"
|
||||
config_get log_dir $1 log_dir "/tmp/caddy/requests.log"
|
||||
config_get caddy_file $1 caddy_file "/etc/caddy/Caddyfile"
|
||||
config_get caddyfile $1 caddyfile
|
||||
config_get file_pass $1 file_pass 0
|
||||
config_get file_username $1 file_username "admin"
|
||||
config_get file_password $1 file_password "123456"
|
||||
config_get webdav_pass $1 webdav_pass 0
|
||||
config_get webdav_username $1 webdav_username "admin"
|
||||
config_get webdav_password $1 webdav_password "123456"
|
||||
config_get api $1 api 0
|
||||
config_get filezip $1 filezip 0
|
||||
config_get webzip $1 webzip 0
|
||||
config_get allow_wan $1 allow_wan 0
|
||||
# ---- 全局值 (来自 @caddy[0]) ----
|
||||
G_BIN="$(uci -q get caddy.@caddy[0].bin_dir)"
|
||||
[ -z "$G_BIN" ] && G_BIN="/usr/sbin/caddy"
|
||||
G_FILE="$(uci -q get caddy.@caddy[0].caddy_file)"
|
||||
[ -z "$G_FILE" ] && G_FILE="/etc/caddy/Caddyfile"
|
||||
G_LOG="$(uci -q get caddy.@caddy[0].log_dir)"
|
||||
[ -z "$G_LOG" ] && G_LOG="/tmp/caddy/requests.log"
|
||||
|
||||
TMP="$G_FILE.new"
|
||||
|
||||
# 列出所有 domain section 的 UCI 路径 (支持命名+匿名)
|
||||
# 返回完整路径如 caddy.d1 或 caddy.@domain[0]
|
||||
list_all_domains() {
|
||||
uci show caddy 2>/dev/null | grep '\.domain_name' | sed 's/\.domain_name=.*//'
|
||||
}
|
||||
|
||||
set_firewall() {
|
||||
if [ "$external_access" = "allow" ]; then
|
||||
uci -q delete firewall.caddy
|
||||
uci set firewall.caddy=rule
|
||||
uci set firewall.caddy.name="caddy"
|
||||
uci set firewall.caddy.target="ACCEPT"
|
||||
uci set firewall.caddy.src="wan"
|
||||
uci set firewall.caddy.proto="tcp"
|
||||
uci set firewall.caddy.dest_port="$port"
|
||||
[ ! -z "$webdav_port" ] && uci set firewall.caddy.dest_port="$webdav_port"
|
||||
uci set firewall.caddy.enabled="1"
|
||||
uci commit firewall
|
||||
/etc/init.d/firewall reload >/dev/null 2>&1
|
||||
elif [ "$external_access" = "deny" ]; then
|
||||
uci -q delete firewall.caddy
|
||||
uci commit firewall
|
||||
/etc/init.d/firewall reload >/dev/null 2>&1
|
||||
# 按 domain_name 查找匹配的 section UCI 路径
|
||||
find_domain_by_name() {
|
||||
local target="$1" ref dn
|
||||
for ref in $(list_all_domains); do
|
||||
dn="$(uci -q get "$ref".domain_name)"
|
||||
[ "$dn" = "$target" ] && echo "$ref" && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# 列出所有 proxy section (匿名+命名都认, 按 target 字段识别)
|
||||
# 匿名 section 在 uci show 里形如 caddy.@proxy[N]
|
||||
list_all_proxies() {
|
||||
uci show caddy 2>/dev/null | grep '\.target=' | sed 's/\.target=.*//'
|
||||
}
|
||||
|
||||
# ---- 生成 Caddyfile ----
|
||||
# 逻辑: 全局块 (auto_https + 每个 ddns_on 的 domain 生成 aliyun_ddns) + 每个启用的 host
|
||||
generate_caddyfile() {
|
||||
local cf="$TMP"
|
||||
local disr
|
||||
local p dom prt tgt key tls_cert extra
|
||||
|
||||
disr="$(uci -q get caddy.@caddy[0].disable_redirect)"
|
||||
|
||||
{
|
||||
echo "# /etc/caddy/Caddyfile"
|
||||
echo "# generated by luci-app-caddy, edit via LuCI -> Network -> Caddy"
|
||||
echo
|
||||
echo "{"
|
||||
|
||||
# auto_https: 勾选才写
|
||||
if [ "$disr" = "1" ]; then
|
||||
echo " auto_https disable_redirects"
|
||||
fi
|
||||
|
||||
# CA 固定 Let's Encrypt, 避开 Caddy 默认可能跳 ZeroSSL
|
||||
echo " acme_ca https://acme-v02.api.letsencrypt.org/directory"
|
||||
|
||||
# --- DDNS: 遍历 domain, ddns_on=1 的生成 aliyun_ddns 块 ---
|
||||
for d in $(list_all_domains); do
|
||||
local don dkid dksec dname drec diface dint dttl
|
||||
don="$(uci -q get "$d".ddns_on)"
|
||||
[ "$don" != "1" ] && continue
|
||||
dname="$(uci -q get "$d".domain_name)"
|
||||
dkid="$(uci -q get "$d".key_id)"
|
||||
dksec="$(uci -q get "$d".key_secret)"
|
||||
drec="$(uci -q get "$d".ddns_records)"
|
||||
diface="$(uci -q get "$d".ddns_wan_iface)"
|
||||
dint="$(uci -q get "$d".ddns_check_interval)"
|
||||
dttl="$(uci -q get "$d".ddns_ttl)"
|
||||
[ -z "$dname" ] && continue
|
||||
[ -z "$dkid" ] && continue
|
||||
[ -z "$dksec" ] && continue
|
||||
[ -z "$dint" ] && dint="5m"
|
||||
[ -z "$dttl" ] && dttl="60s"
|
||||
|
||||
echo
|
||||
echo " # DDNS: $dname (@ = 主域 A 记录, * = 泛解析 *.domain 独立记录, 二者互不串用)"
|
||||
echo " aliyun_ddns {"
|
||||
echo " access_key_id $dkid"
|
||||
echo " access_key_secret $dksec"
|
||||
echo
|
||||
echo " domains {"
|
||||
echo " $dname @"
|
||||
printf '%s\n' "$drec" | while IFS= read -r r; do
|
||||
[ -z "$r" ] && continue
|
||||
echo " $dname $r"
|
||||
done
|
||||
echo " }"
|
||||
echo
|
||||
echo " versions ipv4"
|
||||
case "$diface" in
|
||||
public|"")
|
||||
: # 走插件默认 simple_http (api.ipify.org / icanhazip.com / ifconfig.me / ident.me)
|
||||
;;
|
||||
*)
|
||||
echo " ip_source interface $diface"
|
||||
;;
|
||||
esac
|
||||
echo " check_interval $dint"
|
||||
echo " ttl $dttl"
|
||||
echo " }"
|
||||
done
|
||||
|
||||
echo "}"
|
||||
|
||||
# ---- Host: 每个启用的 proxy (匿名/命名都认) ----
|
||||
for p in $(list_all_proxies); do
|
||||
local pen
|
||||
pen="$(uci -q get "$p".enabled)"
|
||||
[ "$pen" = "0" ] && continue
|
||||
|
||||
# 一次性迁移: 下拉现在只有 auto/off, 任何旧值 (tls Flag / 静态证书路径) 归到这两个
|
||||
local tlsval
|
||||
tlsval="$(uci -q get "$p".tls_cert)"
|
||||
case "$tlsval" in
|
||||
auto|off|"")
|
||||
if [ -z "$tlsval" ]; then
|
||||
# 从未设过: 按旧版 tls Flag 定 (1=auto, 其余 off)
|
||||
if [ "$(uci -q get "$p".tls)" = "1" ]; then
|
||||
tlsval=auto
|
||||
else
|
||||
tlsval=off
|
||||
fi
|
||||
uci -q set "$p".tls_cert="$tlsval"
|
||||
uci commit caddy
|
||||
fi
|
||||
;;
|
||||
*) # 旧值 (静态证书路径等) 一律归 auto, 写回 UCI 一次性归一
|
||||
uci -q set "$p".tls_cert=auto
|
||||
uci commit caddy
|
||||
tlsval=auto
|
||||
;;
|
||||
esac
|
||||
|
||||
dom="$(uci -q get "$p".domain)"
|
||||
prt="$(uci -q get "$p".port)"
|
||||
tgt="$(uci -q get "$p".target)"
|
||||
key="$(uci -q get "$p".api_key)"
|
||||
extra="$(uci -q get "$p".extra)"
|
||||
[ -z "$prt" ] && prt="443"
|
||||
[ -z "$dom" ] && continue
|
||||
[ -z "$tgt" ] && continue
|
||||
|
||||
# 取对应 Domain 的 AK (Host 域名剥掉 *. 前缀再查 Domain 页)
|
||||
# 注意: 必须用 \*.*) 转义前导星号, 否则 example.net 这种含点的裸域会误中 *.*
|
||||
local dmatch dkid dksec dlookup
|
||||
dlookup="$dom"
|
||||
case "$dlookup" in
|
||||
\*.*) dlookup="${dlookup#\*.}" ;;
|
||||
esac
|
||||
dmatch="$(find_domain_by_name "$dlookup")"
|
||||
dkid=""
|
||||
dksec=""
|
||||
if [ -n "$dmatch" ]; then
|
||||
dkid="$(uci -q get "$dmatch".key_id)"
|
||||
dksec="$(uci -q get "$dmatch".key_secret)"
|
||||
fi
|
||||
|
||||
# 监听地址: 完全由表单的 domain+port 决定, 代码不替用户加地址
|
||||
# 选了 *.x 时, 同一行带上裸域 x (官方"一行多名字"模式):
|
||||
# certmagic 逐名字签发 → 裸域、通配符各一张证书(各一条 order)
|
||||
# ≡ acme.sh -d x --dns + -d '*.x' --dns, 效果等价: 裸域+全部子域都覆盖
|
||||
local addr="$dom:$prt"
|
||||
case "$dom" in
|
||||
\*.*) addr="$dom:$prt ${dom#\*.}:$prt" ;;
|
||||
esac
|
||||
|
||||
echo
|
||||
echo "$addr {"
|
||||
|
||||
# TLS: 只有 auto / off 两个值 (下拉已砍掉静态证书选项)
|
||||
# auto + Domain 页有 AK: 一律 DNS-01 (裸域/通配符都不依赖 80/443 入站, 适合 NAT 家庭宽带)
|
||||
# auto + 无 AK: 回退官方 HTTP-01 (一行 tls 都不写, Caddy 自动处理)
|
||||
# 用上面迁移后的 $tlsval, 不重读 UCI (迁移刚写回时本轮即生效)
|
||||
case "$tlsval" in
|
||||
auto|"")
|
||||
if [ -n "$dkid" ] && [ -n "$dksec" ]; then
|
||||
echo " tls {"
|
||||
echo " dns alidns {"
|
||||
echo " access_key_id $dkid"
|
||||
echo " access_key_secret $dksec"
|
||||
echo " }"
|
||||
echo " }"
|
||||
echo
|
||||
fi
|
||||
;;
|
||||
off)
|
||||
: # 纯 HTTP, 不写 tls 块
|
||||
;;
|
||||
esac
|
||||
|
||||
echo " header -Server"
|
||||
echo
|
||||
echo " request_body max_size 0"
|
||||
|
||||
if [ -n "$key" ]; then
|
||||
echo
|
||||
echo " # Caddy 层 API key 校验: 除 @apikey 外一律 404 (/health 也需带 key)"
|
||||
echo " @apikey header Authorization \"Bearer $key\""
|
||||
echo
|
||||
echo " handle @apikey {"
|
||||
echo " reverse_proxy $tgt { flush_interval -1 }"
|
||||
echo " }"
|
||||
echo
|
||||
echo " handle {"
|
||||
# 无 key 的兜底: 空 body 的 404 (不回 "Not Found" 文本, 不泄露身份)
|
||||
# 注意: respond 带 body 文本 + 尾部 { close } 子块时 Caddyfile 参数错位
|
||||
# (404→body, 文本→status), 运行时 Atoi 炸 500. 故 body 留空.
|
||||
echo " respond 404 { close }"
|
||||
echo " }"
|
||||
else
|
||||
echo
|
||||
echo " reverse_proxy $tgt {"
|
||||
echo " flush_interval -1"
|
||||
echo " }"
|
||||
fi
|
||||
|
||||
# 自定义指令 (原样插入)
|
||||
if [ -n "$extra" ]; then
|
||||
echo
|
||||
printf '%s\n' "$extra" | while IFS= read -r line; do
|
||||
echo " $line"
|
||||
done
|
||||
fi
|
||||
|
||||
# 请求日志: 勾选 log_on 才生成, 关 = 不写日志, 省资源
|
||||
local lon
|
||||
lon="$(uci -q get caddy.@caddy[0].log_on)"
|
||||
if [ "$lon" != "0" ]; then
|
||||
echo
|
||||
echo " # 请求日志"
|
||||
echo " log {"
|
||||
echo " output file $G_LOG {"
|
||||
echo " roll_size 1MiB"
|
||||
echo " roll_local_time"
|
||||
echo " roll_keep 5"
|
||||
echo " roll_keep_for 120h"
|
||||
echo " }"
|
||||
echo " }"
|
||||
fi
|
||||
|
||||
echo "}"
|
||||
done
|
||||
} > "$cf"
|
||||
mv -f "$cf" "$G_FILE"
|
||||
# DDNS/AK secret 明文在文件里, 收紧权限只让 root 读写 (LuCI CGI 同以 root 运行, 不受影响)
|
||||
chmod 600 "$G_FILE"
|
||||
|
||||
# 一次性迁移: wildcard 旧字段. 通配符选择已挪到 Host 页下拉, Domain 页不再显示该字段,
|
||||
# 而 CBI 保存只写页面已定义字段、不删未定义键 → 旧 wildcard 键会永久残留 UCI. 有就删.
|
||||
local wcwipe
|
||||
wcwipe=0
|
||||
for d in $(list_all_domains); do
|
||||
case "$(uci -q get "$d".wildcard)" in
|
||||
""|auto|off) : ;;
|
||||
*) uci -q delete "$d".wildcard; wcwipe=1 ;;
|
||||
esac
|
||||
done
|
||||
[ "$wcwipe" = "1" ] && uci commit caddy
|
||||
|
||||
# 清 UCI 冻结值: [配置] 页 TextValue 点过「保存」后会把当时内容冻进 UCI,
|
||||
# 之后页面优先显示 UCI 而不再读磁盘 (表现: 这里改了但配置页看到的还是旧的).
|
||||
# 每次重新生成后把键删掉, 页面即回落 o.default(读磁盘真值).
|
||||
# 只在键存在时才 commit (commit 会动 /etc/config/caddy 触发 procd reload,
|
||||
# 避免无谓的额外重载: 键存在→删→commit→本轮 reload 重生成时键已不在, 不会死循环)
|
||||
if uci -q get caddy.@caddy[0].caddyfile >/dev/null 2>&1; then
|
||||
uci -q delete caddy.@caddy[0].caddyfile
|
||||
uci commit caddy
|
||||
fi
|
||||
}
|
||||
|
||||
check_prog() {
|
||||
if [ ! -f "$G_BIN" ]; then
|
||||
uci -q set caddy.@caddy[0].enabled=0
|
||||
uci commit caddy
|
||||
echo "caddy: $G_BIN 未找到" >>"$G_LOG" 2>&1
|
||||
exit 1
|
||||
else
|
||||
chmod +x "$G_BIN"
|
||||
if [ "$($G_BIN -h 2>&1 | wc -l)" -lt 3 ]; then
|
||||
uci -q set caddy.@caddy[0].enabled=0
|
||||
uci commit caddy
|
||||
echo "caddy: 程序不完整或架构不匹配" >>"$G_LOG" 2>&1
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
start_service() {
|
||||
config_load caddy
|
||||
config_foreach get_config caddy
|
||||
[ $enabled -ne 1 ] && return 1
|
||||
log_path="$(dirname $log_dir)"
|
||||
mkdir -p $log_path
|
||||
if [ ! -f "$PROG" ] ;then
|
||||
uci -q set caddy.@caddy[0].enabled=0
|
||||
uci commit caddy
|
||||
echo "$(TZ=UTC-8 date -R +%Y年%m月%d日\ %X) : $PROG 未找到,无法启动!请确认已上传程序" >>$log_dir 2>&1
|
||||
exit 1
|
||||
else
|
||||
chmod +x $PROG
|
||||
if [ $(($($PROG -h | wc -l))) -lt 3 ] ;then
|
||||
uci -q set caddy.@caddy[0].enabled=0
|
||||
uci commit caddy
|
||||
echo "$(TZ=UTC-8 date -R +%Y年%m月%d日\ %X) : $PROG 程序不完整或CPU架构不支持的程序,无法启动!" >>$log_dir 2>&1
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
if [ "$cmd" = "默认" ] ;then
|
||||
mkdir -p $data_dir
|
||||
if [ "$log" -eq 1 ] ;then
|
||||
|
||||
logs="log {
|
||||
output file $log_dir {
|
||||
roll_size 1MiB
|
||||
roll_local_time
|
||||
roll_keep 5
|
||||
roll_keep_for 120h
|
||||
}
|
||||
}"
|
||||
fi
|
||||
if [ "$file_pass" -eq 1 ] ;then
|
||||
filepassword="$($PROG hash-password --plaintext $file_password)"
|
||||
filebasicauth="basicauth {
|
||||
$file_username $filepassword
|
||||
}"
|
||||
fi
|
||||
if [ "$webdav" -eq 1 ] ;then
|
||||
mkdir -p $webdav_data_dir
|
||||
if [ "$webdav_pass" -eq 1 ] ;then
|
||||
password="$($PROG hash-password --plaintext $webdav_password)"
|
||||
basicauth="basicauth {
|
||||
$webdav_username $password
|
||||
}"
|
||||
fi
|
||||
webdavs=":$webdav_port {
|
||||
$basicauth
|
||||
webdav * {
|
||||
prefix /dav
|
||||
root $webdav_data_dir
|
||||
}
|
||||
}"
|
||||
orderweb="order webdav before file_server # 启动 webdav 模块"
|
||||
fi
|
||||
if [ "$filezip" -eq 1 ] ;then
|
||||
filezips="encode gzip"
|
||||
fi
|
||||
if [ "$webzip" -eq 1 ] ;then
|
||||
webzips="encode gzip"
|
||||
fi
|
||||
if [ "$api" != "1" ] ;then
|
||||
apis="admin off # 关闭 API 端口"
|
||||
fi
|
||||
if [ "$allow_wan" -eq "1" ]; then
|
||||
external_access="allow"
|
||||
else
|
||||
external_access="deny"
|
||||
fi
|
||||
if [ "$webdav" -eq 1 ] || [ "$api" != "1" ] ;then
|
||||
order="{ # 全局配置
|
||||
$orderweb
|
||||
$apis
|
||||
} "
|
||||
fi
|
||||
set_firewall
|
||||
|
||||
cat > $caddy_file <<EOF
|
||||
# 此配置文件路径:/etc/caddy/Caddyfile
|
||||
local enabled
|
||||
enabled="$(uci -q get caddy.@caddy[0].enabled)"
|
||||
[ -z "$enabled" ] && enabled=0
|
||||
[ "$enabled" != "1" ] && return 1
|
||||
|
||||
$order
|
||||
mkdir -p "$(dirname "$G_LOG")"
|
||||
check_prog
|
||||
|
||||
:$port {
|
||||
$filezips
|
||||
$filebasicauth
|
||||
root * $data_dir
|
||||
file_server browse
|
||||
|
||||
$logs
|
||||
|
||||
}
|
||||
generate_caddyfile
|
||||
|
||||
$webdavs
|
||||
EOF
|
||||
"$G_BIN" fmt --overwrite "$G_FILE" 2>/dev/null || true
|
||||
|
||||
uci -q set caddy.@caddy[0].caddyfile="$(cat $caddy_file)"
|
||||
uci commit caddy
|
||||
else
|
||||
caddyf="$(uci -q get caddy.@caddy[0].caddyfile)"
|
||||
logpath="$(echo $caddyf | grep "output file" | awk -F 'output file' '{print $2}' | tr -d '{ ')"
|
||||
[ ! -z "$logpath" ] && uci -q set caddy.@caddy[0].log_dir=$logpath && uci commit caddy
|
||||
if [ ! -z "$caddyf" ] ;then
|
||||
echo "$caddyf" >$caddy_file 2>&1
|
||||
else
|
||||
uci -q set caddy.@caddy[0].enabled=0
|
||||
uci commit caddy
|
||||
echo "$(TZ=UTC-8 date -R +%Y年%m月%d日\ %X) : $caddy_file 配置文件Caddyfile为空,无法启动!" >>$log_dir 2>&1
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
$PROG fmt --overwrite $caddy_file
|
||||
procd_open_instance caddy
|
||||
procd_set_param command $PROG
|
||||
procd_append_param command run
|
||||
procd_append_param command --config $caddy_file
|
||||
procd_append_param command --adapter caddyfile
|
||||
|
||||
procd_set_param stdout 0
|
||||
procd_set_param stderr 0
|
||||
procd_set_param command "$G_BIN" "run" "--config" "$G_FILE" "--adapter" caddyfile
|
||||
procd_set_param pidfile /var/run/caddy.pid
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_set_param respawn
|
||||
procd_close_instance caddy
|
||||
|
||||
echo `date +%s` > /tmp/caddy_time
|
||||
date +%s > /tmp/caddy_time
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
@@ -181,14 +318,26 @@ service_triggers() {
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
external_access="deny"
|
||||
set_firewall
|
||||
rm -rf /tmp/caddy.tag /tmp/caddynew.tag
|
||||
rm -rf $(uci -q get caddy.@caddy[0].log_dir)
|
||||
procd_kill caddy 2>/dev/null
|
||||
rm -f /tmp/caddynew.tag
|
||||
rm -f "$G_LOG" 2>/dev/null
|
||||
rm -f /var/run/caddy.pid
|
||||
return 0
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
# 覆盖 rc.common 的 restart(): 官方零停机热重载 (caddy reload, 等价 POST /load)
|
||||
# 有运行实例 -> 热重载, 旧连接不断; 无实例 -> 完整 stop/start
|
||||
restart() {
|
||||
generate_caddyfile
|
||||
"$G_BIN" fmt --overwrite "$G_FILE" 2>/dev/null || true
|
||||
if pidof caddy >/dev/null 2>&1; then
|
||||
"$G_BIN" reload --config "$G_FILE" --adapter caddyfile >/dev/null 2>&1 && return 0
|
||||
fi
|
||||
stop
|
||||
sleep 1
|
||||
start
|
||||
}
|
||||
|
||||
# reload: procd 模式 rc.common 调 reload_service, 委托给热重载
|
||||
reload_service() {
|
||||
restart
|
||||
}
|
||||
Reference in new issue
Block a user