Files
op-packages/luci-app-caddy/root/etc/init.d/caddy
T

344 lines
9.9 KiB
Bash
Executable File

#!/bin/sh /etc/rc.common
START=99
USE_PROCD=1
# ---- 全局值 (来自 @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=.*//'
}
# 按 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() {
local enabled
enabled="$(uci -q get caddy.@caddy[0].enabled)"
[ -z "$enabled" ] && enabled=0
[ "$enabled" != "1" ] && return 1
mkdir -p "$(dirname "$G_LOG")"
check_prog
generate_caddyfile
"$G_BIN" fmt --overwrite "$G_FILE" 2>/dev/null || true
procd_open_instance caddy
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
date +%s > /tmp/caddy_time
}
service_triggers() {
procd_add_reload_trigger "caddy"
}
stop_service() {
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
}
# 覆盖 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
start
}
# reload: procd 模式 rc.common 调 reload_service, 委托给热重载
reload_service() {
restart
}