🌴 Sync 2026-03-05 23:51:42

This commit is contained in:
github-actions[bot] committed 2026-03-05 23:51:42 +08:00
1 parent 97c049ed0a
commit 0acff03804
14253 files changed
+3893239

No files matched your search

@@ -0,0 +1,91 @@
module("luci.controller.caddy", package.seeall)
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
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)
e.running=luci.sys.call("pidof caddy >/dev/null")==0
local tagfile = io.open("/tmp/caddy_time", "r")
if tagfile then
local tagcontent = tagfile:read("*all")
tagfile:close()
if tagcontent and tagcontent ~= "" then
os.execute("start_time=$(cat /tmp/caddy_time) && time=$(($(date +%s)-start_time)) && day=$((time/86400)) && [ $day -eq 0 ] && day='' || day=${day}天 && time=$(date -u -d @${time} +'%H小时%M分%S秒') && echo $day $time > /tmp/command_caddy 2>&1")
local command_output_file = io.open("/tmp/command_caddy", "r")
if command_output_file then
e.caddysta = command_output_file:read("*all")
command_output_file:close()
if e.caddysta == "" then
e.caddysta = "unknown"
end
end
end
end
local command2 = io.popen('test ! -z "`pidof caddy`" && (top -b -n1 | grep -E "$(pidof caddy)" 2>/dev/null | grep -v grep | awk \'{for (i=1;i<=NF;i++) {if ($i ~ /caddy/) break; else cpu=i}} END {print $cpu}\')')
e.caddycpu = command2:read("*all")
command2:close()
if e.caddycpu == "" then
e.caddycpu = "unknown"
end
local command3 = io.popen("test ! -z `pidof caddy` && (cat /proc/$(pidof caddy | awk '{print $NF}')/status | grep -w VmRSS | awk '{printf \"%.2f MB\", $2/1024}')")
e.caddyram = command3:read("*all")
command3:close()
if e.caddyram == "" then
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)")
e.caddytag = command4:read("*all")
command4:close()
if e.caddytag == "" then
e.caddytag = "unknown"
end
local command5 = io.popen("([ -s /tmp/caddynew.tag ] && cat /tmp/caddynew.tag ) || ( curl -L -k -s --connect-timeout 3 --user-agent 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36' https://api.github.com/repos/caddyserver/caddy/releases/latest | grep tag_name | sed 's/[^0-9.]*//g' >/tmp/caddynew.tag && cat /tmp/caddynew.tag )")
e.caddynewtag = command5:read("*all")
command5:close()
if e.caddynewtag == "" then
e.caddynewtag = "unknown"
end
luci.http.prepare_content("application/json")
luci.http.write_json(e)
end
function get_log()
luci.http.write(luci.sys.exec("[ -s $(uci -q get caddy.@caddy[0].log_dir) ] && cat $(uci -q get caddy.@caddy[0].log_dir)"))
end
function clear_log()
luci.sys.call("cat /dev/null > $(uci -q get caddy.@caddy[0].log_dir)")
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")
luci.http.prepare_content("application/json")
luci.http.write_json({ validate = validate })
end
@@ -0,0 +1,127 @@
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>&nbsp; &nbsp;&nbsp;" .. " caddy文档:" .. [[<a href="https://caddyserver.com/docs/" target="_blank">]] .. translate("caddyserver.com/docs/") .. [[</a>]])
m:section(SimpleSection).template = "caddy/caddy_status"
s = m:section(TypedSection, "caddy")
s.addremove = false
s.anonymous = true
o = s:option(Flag, "enabled", translate("Enabled"))
o.rmempty = false
o.default = 0
o = s:option(Button, "btnrm", translate("重启"))
o.inputtitle = translate("重启")
o.description = translate("在没有修改参数的情况下快速重新启动一次")
o.inputstyle = "apply"
o:depends("enabled", "1")
o.write = function()
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 &nbsp;&nbsp;&nbsp; 例如:/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.placeholder = "/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后缀&nbsp;&nbsp;如: 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.rawhtml = true
o.template = "caddy/admin_info"
return m
@@ -0,0 +1,5 @@
m = Map("caddy")
m:append(Template("caddy/caddy_log"))
return m
@@ -0,0 +1,25 @@
<%+cbi/valueheader%>
<script type="text/javascript">//<![CDATA[
function admin_info(btn)
{
btn.disabled = true;
btn.value = '<%:Collecting data...%>';
XHR.get('<%=luci.dispatcher.build_url("admin", "nas", "caddy", "admin_info")%>',
null,
function(x,rv)
{
var tb = document.getElementById('<%=self.option%>-status');
if (tb) {
rv.validate = rv.validate.replace(/{"level":/g, '<br>{"level":').replace(/Error/g, "<br>Error").replace(/Valid configuration/g, "<br><strong><font color='#008000'>当前配置文件正确!</font></strong>").replace(/warn/g, "<font color='#800080'>警告</font>").replace(/level/g, "<font color='#1E90FF'>等级</font>").replace(/info/g, "<font color='#00BFFF'>普通</font>").replace(/msg/g, "<font color='#FFA500'>信息</font>").replace(/error/g, "<font color='#DB7093'>错误</font>").replace(/Error:/g, "<font color='red'><strong>当前配置文件错误!</strong><br>错误位置信息:</font>");
tb.innerHTML = "<br><font color='#FF00FF'><%:状态:%></font>" + "<font color='#2F4F4F'>" + rv.validate + "</font>";
}
btn.disabled = false;
btn.value = '<%:检测%>';
}
);
return false;
}
//]]></script>
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:检测%>" onclick="return admin_info(this)" />
<span id="<%=self.option%>-status"><em><%=self.value%></em></span>
<%+cbi/valuefooter%>
@@ -0,0 +1,124 @@
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></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; }
}
</style>
<div id="app">
<div class="toolbar">
<div>
<button type="button" class="toggle-btn" @click="toggle">{{ paused ? '继续刷新' : '暂停刷新' }}</button>
<button type="button" class="clear-log" @click="clear">清空日志</button>
</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>
</div>
<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>
</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>
@@ -0,0 +1,35 @@
<%
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,
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 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 ? "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;" + "&nbsp;内存占用:" + "<span style='color:#0000CD;'>" + st.caddyram + "</span>" : "";
var caddytagContent = st.running ? "<br>" + "当前版本:" + "<span style='color:#CD853F;'>" + st.caddytag + "</span>" : "";
var caddynewtagContent = st.running ? "&nbsp;&nbsp;&nbsp;" + "最新版本:" + "<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 + "/')\"/>" : "";
tb.innerHTML = "<b><em>Caddy " + caddyStatus + caddystaContent + caddycpuContent + caddyramContent + caddytagContent + caddynewtagContent + caddywebContent + "</b></em>";
}
}
);
//]]></script>
<style>.mar-10 {margin-left: 50px; margin-right: 10px;}</style>
<fieldset class="cbi-section">
<p id="caddy_status">
<em><b><%:Collecting data...%></b></em>
</p>
</fieldset>