mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-26 22:12:51 +08:00
344 lines
9.9 KiB
Bash
Executable File
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
|
|
}
|