fix(agent,acme,cert,web): 修 frps 安装 404 与 nginx 探测两处,证书颁发机构用 LiteSSL 替换 Buypass(v1.2.14)
CI / verify (push) Canceled after 0s
CI / verify (push) Canceled after 0s
- frps 安装/升级一直 404:frp 官方校验文件名始终是 frp_sha256_checksums.txt(不带版本号),代码却拼 frp_<版本>_sha256sums.txt;现按候选名依次尝试并在失败时列出尝试项。同一条 bug 也存在于本机下载 frpc 的路径,一并修复(SHA-256 校验逻辑未改) - nginx 探测(agent):nginxPath() 不再回落到配置里的裸名字 nginx(否则「未安装」被误报为配置校验失败,且一键安装入口永不出现);vhost 目录探测不再要求被加载文件带 .conf 后缀(Debian 的 sites-enabled/default 不带,导致只剩主配置目录可试,报「均无法放置完整 server 配置」),失败信息补上具体原因 - 证书 CA:LiteSSL(亚数 TrustAsia,90 天,支持单域名/通配符/多域名)替换 Buypass。LiteSSL 目录 meta.externalAccountRequired=true,强制 EAB——在 FreeSSL 创建后填 Kid/HMAC 即可,HMAC 只回掩码;Buypass 保留常量与展示元数据但不再可选 - 前端申请证书弹窗的颁发机构卡片改为前端可控清单(原先后端 /api/certs/options 的结果不参与渲染,后端改了弹窗不变) - 测试:internal/frp 校验文件候选名与 sha256sum 解析;internal/acme LiteSSL 目录/别名/通配符/Buypass 停用提示。gofmt / go build / go vet / go test / vue-tsc / vite build 全绿 - AgentVersion 0.12.4 → 0.12.7,双架构二进制已重编:VPS 上需点「升级 Agent」
This commit is contained in:
@@ -2,6 +2,32 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [v1.2.14] - 2026-09-27
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
|
||||||
|
- **证书颁发机构:LiteSSL 替换 Buypass**。LiteSSL(亚数 TrustAsia 的公益免费 DV 证书,90 天,官方支持单域名 / 通配符 / 多域名)已接入,ACME 目录 `https://acme.litessl.com/acme/v2/directory`。该目录里 `meta.externalAccountRequired = true`,所以**强制 EAB**:在 FreeSSL 平台「证书自动化 → EAB 管理」创建后,把 Kid 与 HMAC 填进设置即可(不需要像 ZeroSSL 那样用 API Key 去换取)。EAB HMAC 只回掩码。
|
||||||
|
Buypass 保留常量与展示元数据(历史证书记录仍能正常显示),但不再可选,直接提交会被拦下并提示替代项。域名若有 CAA 记录,需允许 `trustasia.com`。
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
|
||||||
|
- **frps 安装一直报 HTTP 404**:frp 官方的校验文件名始终是 `frp_sha256_checksums.txt`(不带版本号,v0.52.3 / v0.58.1 / v0.62.1 / v0.71.0 实测均如此),而代码拼的是 `frp_<版本>_sha256sums.txt`——任何版本、任何网络环境下都装不上。现改为按候选名依次尝试(旧名保留兜底),失败信息里列出尝试过的文件名。
|
||||||
|
**同一条 bug 也在本机下载 frpc 的路径上,一并修复**;SHA-256 校验逻辑本身一行未改。
|
||||||
|
- **Nginx 探测两处(均在 agent)**:
|
||||||
|
- `nginxPath()` 探测不到时不再回落到配置里的裸名字 `nginx`:那会让「未安装」被误判为「配置校验未通过(nginx -t 执行失败)」,且「VPS 未安装 Nginx」告警与一键安装按钮**永远不会出现**。
|
||||||
|
- vhost 目录探测不再要求被加载文件以 `.conf` 结尾:Debian/Ubuntu 的 `sites-enabled/default` 不带后缀、而 `nginx.conf` 里是 `include .../sites-enabled/*`,过滤掉它之后只剩主配置目录可试,报「在 1 个被加载目录中均无法放置完整 server 配置」;现在失败信息会带上最后一次的具体原因与常见显式指定目录。
|
||||||
|
- **前端申请证书弹窗的颁发机构卡片**改为前端可控清单:原先硬编码为 Let's Encrypt / ZeroSSL / Buypass,后端 `/api/certs/options` 的结果不参与渲染,导致后端增删 CA 时弹窗不变(本次 LiteSSL 替换就是这幺被发现的)。现在卡片为 Let's Encrypt / ZeroSSL / LiteSSL。
|
||||||
|
|
||||||
|
### 测试
|
||||||
|
|
||||||
|
- `internal/frp`:校验文件候选名顺序、sha256sum 格式解析(含 `*` 前缀与不存在的文件负例)
|
||||||
|
- `internal/acme`:LiteSSL 目录地址 / 展示名 / 别名归一化、通配符与多域名不被拦、Buypass 停用提示里含替代项
|
||||||
|
|
||||||
|
### 变更
|
||||||
|
|
||||||
|
- `AgentVersion` 0.12.4 → **0.12.7**(0.12.5 修 frps 校验文件名;0.12.6 修 nginxPath;0.12.7 修 vhost 目录探测),双架构内嵌二进制已重编;**VPS 上需点一次「升级 Agent」**
|
||||||
|
- 版本号提升到 1.2.14;`web/package.json` 同步对齐
|
||||||
|
|
||||||
## [v1.2.13] - 2026-09-27
|
## [v1.2.13] - 2026-09-27
|
||||||
|
|
||||||
### 新增
|
### 新增
|
||||||
|
|||||||
+13
-2
@@ -12,13 +12,14 @@ const (
|
|||||||
CALetsEncryptStaging = "letsencrypt-staging"
|
CALetsEncryptStaging = "letsencrypt-staging"
|
||||||
CAZeroSSL = "zerossl"
|
CAZeroSSL = "zerossl"
|
||||||
CABuypass = "buypass"
|
CABuypass = "buypass"
|
||||||
|
// CALiteSSL 亚数 TrustAsia 的公益免费证书(90 天,支持单域名/通配符/多域名,强制 EAB)
|
||||||
|
CALiteSSL = "litessl"
|
||||||
)
|
)
|
||||||
|
|
||||||
// caRetired 记录已停止签发的 CA:仍然认识这些值(历史证书记录里可能存着,用于展示与清理),
|
// caRetired 记录已停止签发的 CA:仍然认识这些值(历史证书记录里可能存着,用于展示与清理),
|
||||||
// 但不允许再用来申请——否则用户只会看到一个难懂的 ACME 报错而不是「这家 CA 不干了」。
|
// 但不允许再用来申请——否则用户只会看到一个难懂的 ACME 报错而不是「这家 CA 不干了」。
|
||||||
// 否则用户只会看到一个难懂的 ACME 报错。
|
|
||||||
var caRetired = map[string]string{
|
var caRetired = map[string]string{
|
||||||
CABuypass: "Buypass 已于 2025-10-15 停止签发证书(ACME 服务 2026-04-15 终止),请改用 Let's Encrypt 或 ZeroSSL",
|
CABuypass: "Buypass 已于 2025-10-15 停止签发证书(ACME 服务 2026-04-15 终止),请改用 Let's Encrypt、LiteSSL 或 ZeroSSL",
|
||||||
}
|
}
|
||||||
|
|
||||||
// CAUnavailableReason 返回该 CA 已停用的原因;可用的 CA 返回空字符串。
|
// CAUnavailableReason 返回该 CA 已停用的原因;可用的 CA 返回空字符串。
|
||||||
@@ -29,6 +30,8 @@ func CAUnavailableReason(ca string) string {
|
|||||||
const (
|
const (
|
||||||
zeroSSLDirectoryURL = "https://acme.zerossl.com/v2/DV90"
|
zeroSSLDirectoryURL = "https://acme.zerossl.com/v2/DV90"
|
||||||
buypassDirectoryURL = "https://api.buypass.com/acme/directory"
|
buypassDirectoryURL = "https://api.buypass.com/acme/directory"
|
||||||
|
// liteSSLDirectoryURL 实测 2026-09-27 可访问,目录里 meta.externalAccountRequired = true
|
||||||
|
liteSSLDirectoryURL = "https://acme.litessl.com/acme/v2/directory"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NormalizeCA(ca string) string {
|
func NormalizeCA(ca string) string {
|
||||||
@@ -41,6 +44,8 @@ func NormalizeCA(ca string) string {
|
|||||||
return CAZeroSSL
|
return CAZeroSSL
|
||||||
case CABuypass, "buypass-go":
|
case CABuypass, "buypass-go":
|
||||||
return CABuypass
|
return CABuypass
|
||||||
|
case CALiteSSL, "trustasia":
|
||||||
|
return CALiteSSL
|
||||||
default:
|
default:
|
||||||
return ca
|
return ca
|
||||||
}
|
}
|
||||||
@@ -56,6 +61,8 @@ func DirectoryURL(ca string) (string, error) {
|
|||||||
return zeroSSLDirectoryURL, nil
|
return zeroSSLDirectoryURL, nil
|
||||||
case CABuypass:
|
case CABuypass:
|
||||||
return buypassDirectoryURL, nil
|
return buypassDirectoryURL, nil
|
||||||
|
case CALiteSSL:
|
||||||
|
return liteSSLDirectoryURL, nil
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("不支持的颁发机构:%s", ca)
|
return "", fmt.Errorf("不支持的颁发机构:%s", ca)
|
||||||
}
|
}
|
||||||
@@ -71,6 +78,8 @@ func CALabel(ca string) string {
|
|||||||
return "ZeroSSL"
|
return "ZeroSSL"
|
||||||
case CABuypass:
|
case CABuypass:
|
||||||
return "Buypass"
|
return "Buypass"
|
||||||
|
case CALiteSSL:
|
||||||
|
return "LiteSSL"
|
||||||
default:
|
default:
|
||||||
if ca == "" {
|
if ca == "" {
|
||||||
return "手动导入"
|
return "手动导入"
|
||||||
@@ -84,6 +93,8 @@ func ValidateCA(ca string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateCADomains 校验各 CA 自己的限制。
|
||||||
|
// LiteSSL 官方支持单域名 / 通配符 / 多域名,因此这里不额外限制。
|
||||||
func ValidateCADomains(ca string, domains []string) error {
|
func ValidateCADomains(ca string, domains []string) error {
|
||||||
switch NormalizeCA(ca) {
|
switch NormalizeCA(ca) {
|
||||||
case CABuypass:
|
case CABuypass:
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package acme
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LiteSSL 接替 Buypass 的位置:目录地址实测可用,且强制 EAB。
|
||||||
|
func TestLiteSSLDirectoryAndLabel(t *testing.T) {
|
||||||
|
url, err := DirectoryURL(CALiteSSL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LiteSSL 目录地址解析失败:%v", err)
|
||||||
|
}
|
||||||
|
if url != "https://acme.litessl.com/acme/v2/directory" {
|
||||||
|
t.Fatalf("LiteSSL 目录地址不对:%q", url)
|
||||||
|
}
|
||||||
|
if label := CALabel(CALiteSSL); label != "LiteSSL" {
|
||||||
|
t.Fatalf("LiteSSL 展示名不对:%q", label)
|
||||||
|
}
|
||||||
|
// 别名与归一化
|
||||||
|
if got := NormalizeCA("trustasia"); got != CALiteSSL {
|
||||||
|
t.Fatalf("别名 trustasia 应归一化为 %s,实际 %q", CALiteSSL, got)
|
||||||
|
}
|
||||||
|
if reason := CAUnavailableReason(CALiteSSL); reason != "" {
|
||||||
|
t.Fatalf("LiteSSL 是可用的 CA,不该有停用原因:%q", reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LiteSSL 支持通配符与多域名,因此 ValidateCADomains 不应拦它。
|
||||||
|
func TestValidateCADomainsLiteSSLAllowsWildcard(t *testing.T) {
|
||||||
|
domains := []string{"*.36800.cc", "a.com", "b.com", "c.com", "d.com", "e.com", "f.com"}
|
||||||
|
if err := ValidateCADomains(CALiteSSL, domains); err != nil {
|
||||||
|
t.Fatalf("LiteSSL 应允许通配符与多域名:%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buypass 保留在停用表里(历史记录要能展示),但提示里应给出可用的替代。
|
||||||
|
func TestBuypassIsRetiredWithAlternatives(t *testing.T) {
|
||||||
|
reason := CAUnavailableReason(CABuypass)
|
||||||
|
if reason == "" {
|
||||||
|
t.Fatal("Buypass 应仍被标记为已停用")
|
||||||
|
}
|
||||||
|
if !strings.Contains(reason, "LiteSSL") {
|
||||||
|
t.Fatalf("停用提示里应包含 LiteSSL 这个替代项:%q", reason)
|
||||||
|
}
|
||||||
|
// 旧记录仍能正常取到目录地址(不报「不支持的颁发机构」)
|
||||||
|
if _, err := DirectoryURL(CABuypass); err != nil {
|
||||||
|
t.Fatalf("历史 Buypass 记录的目录地址仍应可解析:%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -416,6 +416,27 @@ func (s *Service) registerACMEAccount(ctx context.Context, client *lego.Client,
|
|||||||
Kid: kid,
|
Kid: kid,
|
||||||
HmacEncoded: hmac,
|
HmacEncoded: hmac,
|
||||||
})
|
})
|
||||||
|
case CALiteSSL:
|
||||||
|
// LiteSSL 的目录里 meta.externalAccountRequired = true:必须带 EAB。
|
||||||
|
// 与 ZeroSSL 不同,不经过厂商 API 换取,而是用户在 FreeSSL 平台自建 EAB 后直接填 kid/hmac。
|
||||||
|
kid, err := s.settings.Get(ctx, settings.KeyLiteSSLEABKid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
hmac, err := s.settings.Get(ctx, settings.KeyLiteSSLEABHMAC)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
kid = strings.TrimSpace(kid)
|
||||||
|
hmac = strings.TrimSpace(hmac)
|
||||||
|
if kid == "" || hmac == "" {
|
||||||
|
return nil, fmt.Errorf("LiteSSL 要求 EAB 凭据:请在设置里填写 EAB Kid 与 EAB HMAC(在 FreeSSL 的「证书自动化 → EAB 管理」创建后原样粘贴)")
|
||||||
|
}
|
||||||
|
return client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||||
|
TermsOfServiceAgreed: true,
|
||||||
|
Kid: kid,
|
||||||
|
HmacEncoded: hmac,
|
||||||
|
})
|
||||||
default:
|
default:
|
||||||
return client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
return client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,12 @@ import (
|
|||||||
// 0.12.2:新增域名 vhost 的版本列表 / 对比 / 回滚端点;写配置前留版本,校验失败时恢复上一个可用版本。
|
// 0.12.2:新增域名 vhost 的版本列表 / 对比 / 回滚端点;写配置前留版本,校验失败时恢复上一个可用版本。
|
||||||
// 0.12.3:frps 日志尾巴改为最近 20 行、最新在前,并用 short-iso 时间戳(默认格式不带年份、分不清哪一天)。
|
// 0.12.3:frps 日志尾巴改为最近 20 行、最新在前,并用 short-iso 时间戳(默认格式不带年份、分不清哪一天)。
|
||||||
// 0.12.4:日志行重写成「2026-09-22 18:43:34 进程: 消息」,去掉 T 分隔、时区偏移与每行重复的主机名。
|
// 0.12.4:日志行重写成「2026-09-22 18:43:34 进程: 消息」,去掉 T 分隔、时区偏移与每行重复的主机名。
|
||||||
const AgentVersion = "0.12.4"
|
// 0.12.5:修正 frps 官方校验文件名(frp 一直叫 frp_sha256_checksums.txt,原代码拼的是 frp_<版本>_sha256sums.txt,导致首次安装报 HTTP 404)。
|
||||||
|
// 0.12.6:nginxPath 探测不到时返回空串(原来回落到配置里的裸名字 "nginx"),
|
||||||
|
// 修掉「VPS 未安装 Nginx」告警与一键安装按钮永远不出现、以及把它误报成「配置校验未通过」的问题。
|
||||||
|
// 0.12.7:vhost 目录探测不再要求被加载文件带 .conf 后缀(Debian 的 sites-enabled/default 不带),
|
||||||
|
// 并在失败信息里带上最后一次的具体原因与常见的显式指定目录。
|
||||||
|
const AgentVersion = "0.12.7"
|
||||||
|
|
||||||
// Config 是 fonu-agent 的运行配置。agent 只在 VPS 本地管理自己的 Nginx 片段、证书和 frps 配置。
|
// Config 是 fonu-agent 的运行配置。agent 只在 VPS 本地管理自己的 Nginx 片段、证书和 frps 配置。
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -259,6 +264,25 @@ var frpsDownloadProxies = map[string]struct{}{
|
|||||||
|
|
||||||
const frpsReleaseBaseURL = "https://github.com/fatedier/frp/releases/download/"
|
const frpsReleaseBaseURL = "https://github.com/fatedier/frp/releases/download/"
|
||||||
|
|
||||||
|
// fetchChecksumForCandidates 依次尝试官方校验文件名的候选。
|
||||||
|
// frp 的校验文件一直叫 frp_sha256_checksums.txt(不带版本号),旧名字保留兜底,
|
||||||
|
// 避开上游改名后整个安装流程直接 404。
|
||||||
|
func fetchChecksumForCandidates(releaseURL, version, asset string) (string, error) {
|
||||||
|
candidates := []string{
|
||||||
|
"frp_sha256_checksums.txt",
|
||||||
|
"frp_" + version + "_sha256sums.txt",
|
||||||
|
}
|
||||||
|
var lastErr error
|
||||||
|
for _, name := range candidates {
|
||||||
|
sum, err := fetchChecksumFor(releaseURL+name, asset)
|
||||||
|
if err == nil {
|
||||||
|
return sum, nil
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("%v(已尝试:%s)", lastErr, strings.Join(candidates, "、"))
|
||||||
|
}
|
||||||
|
|
||||||
// installFRPS 下载指定版本的 frp release、安装 frps 二进制并注册 systemd 服务(幂等,重复调用=升级)。
|
// installFRPS 下载指定版本的 frp release、安装 frps 二进制并注册 systemd 服务(幂等,重复调用=升级)。
|
||||||
func (s *Server) installFRPS(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) installFRPS(w http.ResponseWriter, r *http.Request) {
|
||||||
var req frpsInstallRequest
|
var req frpsInstallRequest
|
||||||
@@ -292,7 +316,7 @@ func (s *Server) installFRPS(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 先取官方校验文件,再下载安装包并按 SHA-256 校验:避免装上来路不明的 frps 可执行文件
|
// 先取官方校验文件,再下载安装包并按 SHA-256 校验:避免装上来路不明的 frps 可执行文件
|
||||||
wantSum, err := fetchChecksumFor(releaseURL+"frp_"+version+"_sha256sums.txt", asset)
|
wantSum, err := fetchChecksumForCandidates(releaseURL, version, asset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": "获取 frps 官方校验信息失败: " + err.Error()})
|
writeJSON(w, http.StatusBadGateway, map[string]string{"error": "获取 frps 官方校验信息失败: " + err.Error()})
|
||||||
return
|
return
|
||||||
@@ -646,11 +670,13 @@ func detectNginxConfDir(binary string) (string, error) {
|
|||||||
line = strings.TrimSpace(line)
|
line = strings.TrimSpace(line)
|
||||||
if value, found := strings.CutPrefix(line, "# configuration file "); found {
|
if value, found := strings.CutPrefix(line, "# configuration file "); found {
|
||||||
p := strings.TrimSuffix(strings.TrimSpace(value), ":")
|
p := strings.TrimSuffix(strings.TrimSpace(value), ":")
|
||||||
if strings.HasSuffix(p, ".conf") {
|
// 不要求 .conf 后缀:Debian/Ubuntu 的 sites-enabled/default 本身不带后缀,
|
||||||
|
// 而 nginx.conf 里是 include /etc/nginx/sites-enabled/*(也无后缀限制);
|
||||||
|
// 只认 .conf 会把这个真正该放 vhost 的目录漏掉,只剩主配置目录可试,
|
||||||
|
// 而主配置目录里的新文件并不被 include(表现为「均无法放置完整 server 配置」)。
|
||||||
loadedDirs[filepath.Dir(p)] = true
|
loadedDirs[filepath.Dir(p)] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
delete(loadedDirs, ".")
|
delete(loadedDirs, ".")
|
||||||
if len(loadedDirs) == 0 {
|
if len(loadedDirs) == 0 {
|
||||||
return "", fmt.Errorf("nginx -T 输出中未发现任何被加载的 .conf 目录")
|
return "", fmt.Errorf("nginx -T 输出中未发现任何被加载的 .conf 目录")
|
||||||
@@ -667,13 +693,16 @@ func detectNginxConfDir(binary string) (string, error) {
|
|||||||
}
|
}
|
||||||
return dirs[i] < dirs[j]
|
return dirs[i] < dirs[j]
|
||||||
})
|
})
|
||||||
|
var lastReason string
|
||||||
for _, dir := range dirs {
|
for _, dir := range dirs {
|
||||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
|
lastReason = fmt.Sprintf("%s: %v", dir, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
probe := filepath.Join(dir, "fonu-probe.conf")
|
probe := filepath.Join(dir, "fonu-probe.conf")
|
||||||
probeBody := "# Managed by fonu-agent. Do not edit.\nserver {\n listen 127.0.0.1:1;\n server_name fonu-probe.invalid;\n}\n"
|
probeBody := "# Managed by fonu-agent. Do not edit.\nserver {\n listen 127.0.0.1:1;\n server_name fonu-probe.invalid;\n}\n"
|
||||||
if err := atomicWrite(probe, []byte(probeBody), 0o644); err != nil {
|
if err := atomicWrite(probe, []byte(probeBody), 0o644); err != nil {
|
||||||
|
lastReason = fmt.Sprintf("%s: %v", dir, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// 两级验证:-t 仅验证语法,不保证文件被 include 加载(主配置目录等无 include 的目录也能通过)。
|
// 两级验证:-t 仅验证语法,不保证文件被 include 加载(主配置目录等无 include 的目录也能通过)。
|
||||||
@@ -685,6 +714,7 @@ func detectNginxConfDir(binary string) (string, error) {
|
|||||||
return dir, nil
|
return dir, nil
|
||||||
}
|
}
|
||||||
// -t 通过但 -T 不含探针:该目录不被 include 加载,删除探针换下一个
|
// -t 通过但 -T 不含探针:该目录不被 include 加载,删除探针换下一个
|
||||||
|
lastReason = fmt.Sprintf("%s: 目录未被 include 加载(nginx -T 输出中无探针文件)", dir)
|
||||||
_ = os.Remove(probe)
|
_ = os.Remove(probe)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -692,13 +722,19 @@ func detectNginxConfDir(binary string) (string, error) {
|
|||||||
// 若 -t 失败信息指向历史残留的 fonu-*.conf,也一并清理(仅限 Agent 生成的文件)。
|
// 若 -t 失败信息指向历史残留的 fonu-*.conf,也一并清理(仅限 Agent 生成的文件)。
|
||||||
_ = os.Remove(probe)
|
_ = os.Remove(probe)
|
||||||
if tOut, tErr := exec.Command(nginx, "-t").CombinedOutput(); tErr != nil {
|
if tOut, tErr := exec.Command(nginx, "-t").CombinedOutput(); tErr != nil {
|
||||||
|
lastReason = fmt.Sprintf("%s: nginx -t 失败:%s", dir, tailText(string(tOut), 300))
|
||||||
cleanupLegacyFonuConf(string(tOut))
|
cleanupLegacyFonuConf(string(tOut))
|
||||||
if err := runCommand(nginx, "-t"); err == nil {
|
if err := runCommand(nginx, "-t"); err == nil {
|
||||||
continue // 清理残留后配置恢复可用,换下一个目录
|
continue // 清理残留后配置恢复可用,换下一个目录
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
lastReason = fmt.Sprintf("%s: 探针写入后 nginx -t 未通过", dir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf("在 %d 个被加载目录中均无法放置完整 server 配置(已尝试: %s);可用 FONU_AGENT_NGINX_CONF_DIR 显式指定", len(dirs), strings.Join(dirs, ", "))
|
if lastReason == "" {
|
||||||
|
lastReason = "没有可尝试的目录"
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("在 %d 个被加载目录中均无法放置完整 server 配置(已尝试: %s);最后一次失败原因:%s;也可用 FONU_AGENT_NGINX_CONF_DIR 显式指定(Debian/Ubuntu 通常是 /etc/nginx/conf.d 或 /etc/nginx/sites-enabled)", len(dirs), strings.Join(dirs, ", "), lastReason)
|
||||||
}
|
}
|
||||||
|
|
||||||
// dirHasVhostConf 判断目录下是否已有含 server_name 的 .conf(HTTP vhost 语义)。
|
// dirHasVhostConf 判断目录下是否已有含 server_name 的 .conf(HTTP vhost 语义)。
|
||||||
@@ -1178,11 +1214,18 @@ func (s *Server) validateNginx() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) reloadNginx() error {
|
func (s *Server) reloadNginx() error {
|
||||||
return runCommand(s.nginxPath(), "-s", "reload")
|
nginx := s.nginxPath()
|
||||||
|
if nginx == "" {
|
||||||
|
return errors.New("未找到 nginx 可执行文件:请先在「公网反代」页安装 Nginx,或用 FONU_AGENT_NGINX_BIN 指定路径")
|
||||||
|
}
|
||||||
|
return runCommand(nginx, "-s", "reload")
|
||||||
}
|
}
|
||||||
|
|
||||||
// nginxPath 返回可用的 nginx 绝对路径:配置的路径可用则用之;否则探测常见安装位置
|
// nginxPath 返回**可用的** nginx 可执行路径:配置的路径确实可用才用它,否则探测常见安装位置
|
||||||
// (宝塔的 nginx 不在 systemd PATH 里,直接用名字会 exec 失败)。
|
// (宝塔的 nginx 不在 systemd PATH 里,直接用名字会 exec 失败)。
|
||||||
|
// 探测不到时返回空串。注意:不要回落到配置里的裸名字(如 "nginx"),
|
||||||
|
// 那会让 probeNginx 的 Missing 判定永远不成立——「未安装」会被当成「配置校验失败」
|
||||||
|
// (页面显示 nginx -t 执行失败),且一键安装入口永远不出现。
|
||||||
func (s *Server) nginxPath() string {
|
func (s *Server) nginxPath() string {
|
||||||
if p := strings.TrimSpace(s.cfg.NginxBinary); p != "" {
|
if p := strings.TrimSpace(s.cfg.NginxBinary); p != "" {
|
||||||
if filepath.IsAbs(p) {
|
if filepath.IsAbs(p) {
|
||||||
@@ -1198,7 +1241,7 @@ func (s *Server) nginxPath() string {
|
|||||||
return candidate
|
return candidate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return s.cfg.NginxBinary
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// probeNginx 真实探测 Nginx 可用性:解析可执行文件并执行 nginx -t;结果缓存 30 秒。
|
// probeNginx 真实探测 Nginx 可用性:解析可执行文件并执行 nginx -t;结果缓存 30 秒。
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ func (h *CertHandler) Options(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, http.StatusOK, []map[string]string{
|
writeJSON(w, http.StatusOK, []map[string]string{
|
||||||
{"value": acme.CALetsEncrypt, "label": acme.CALabel(acme.CALetsEncrypt)},
|
{"value": acme.CALetsEncrypt, "label": acme.CALabel(acme.CALetsEncrypt)},
|
||||||
{"value": acme.CAZeroSSL, "label": acme.CALabel(acme.CAZeroSSL)},
|
{"value": acme.CAZeroSSL, "label": acme.CALabel(acme.CAZeroSSL)},
|
||||||
// Buypass 已于 2025-10-15 停止签发、2026-04-15 终止 ACME 服务,
|
// Buypass 已于 2025-10-15 停止签发、2026-04-15 终止 ACME 服务,不再列为可选 CA
|
||||||
// 不再列为可选 CA(历史记录仍能展示,直接提交会被 CAUnavailableReason 抦下)。
|
// (历史记录仍能展示,直接提交会被 CAUnavailableReason 抦下);LiteSSL 接替其位置。
|
||||||
|
{"value": acme.CALiteSSL, "label": acme.CALabel(acme.CALiteSSL)},
|
||||||
{"value": acme.CALetsEncryptStaging, "label": acme.CALabel(acme.CALetsEncryptStaging)},
|
{"value": acme.CALetsEncryptStaging, "label": acme.CALabel(acme.CALetsEncryptStaging)},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ func (h *SettingsHandler) Put(w http.ResponseWriter, r *http.Request) {
|
|||||||
settings.KeyACMECA: true,
|
settings.KeyACMECA: true,
|
||||||
settings.KeyZeroSSLAPIKey: true,
|
settings.KeyZeroSSLAPIKey: true,
|
||||||
settings.KeyACMEDNSResolvers: true,
|
settings.KeyACMEDNSResolvers: true,
|
||||||
|
settings.KeyLiteSSLEABKid: true,
|
||||||
|
settings.KeyLiteSSLEABHMAC: true,
|
||||||
settings.KeyNotifyWebhookURL: true,
|
settings.KeyNotifyWebhookURL: true,
|
||||||
settings.KeyNotifyType: true,
|
settings.KeyNotifyType: true,
|
||||||
settings.KeyNotifyEmailJSON: true,
|
settings.KeyNotifyEmailJSON: true,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,42 @@
|
|||||||
|
package frp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 官方校验文件名不带版本号;这里钉住候选顺序,避免以后又被改回不存在的名字。
|
||||||
|
func TestChecksumAssetCandidates(t *testing.T) {
|
||||||
|
got := checksumAssetCandidates("0.71.0")
|
||||||
|
if len(got) < 2 {
|
||||||
|
t.Fatalf("应至少有一个候选与一个兜底,实际 %v", got)
|
||||||
|
}
|
||||||
|
if got[0] != "frp_sha256_checksums.txt" {
|
||||||
|
t.Fatalf("首选应为官方现行名字,实际 %q", got[0])
|
||||||
|
}
|
||||||
|
if got[1] != "frp_0.71.0_sha256sums.txt" {
|
||||||
|
t.Fatalf("兜底名字应带版本号,实际 %q", got[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析真实格式:<hash> <文件名>(也兼容 * 前缀与多余空白)
|
||||||
|
func TestFindChecksumLine(t *testing.T) {
|
||||||
|
content := []byte(strings.Join([]string{
|
||||||
|
"845b486c63686990e671f13cc5e3bd130ce7be659ab3c6f043008353451858cb frp_0.71.0_android_arm64.tar.gz",
|
||||||
|
"84f27e39f11169f7adcef8e8b70c9329de17747b1f14dad9fb95eef5682ea716 frp_0.71.0_linux_amd64.tar.gz",
|
||||||
|
"96b8a4a09d7cd6a5ea0e1f9f0b1e4a35b0e0f0b4a4f7c0b0e0f0b4a4f7c0b0e0 *frp_0.71.0_linux_arm64.tar.gz",
|
||||||
|
"",
|
||||||
|
}, "\n"))
|
||||||
|
|
||||||
|
sum, ok := findChecksumLine(content, "frp_0.71.0_linux_amd64.tar.gz")
|
||||||
|
if !ok || sum != "84f27e39f11169f7adcef8e8b70c9329de17747b1f14dad9fb95eef5682ea716" {
|
||||||
|
t.Fatalf("未解析出 linux_amd64 的哈希:%q ok=%v", sum, ok)
|
||||||
|
}
|
||||||
|
sum, ok = findChecksumLine(content, "frp_0.71.0_linux_arm64.tar.gz")
|
||||||
|
if !ok || sum != "96b8a4a09d7cd6a5ea0e1f9f0b1e4a35b0e0f0b4a4f7c0b0e0f0b4a4f7c0b0e0" {
|
||||||
|
t.Fatalf("带 * 前缀的文件名应能匹配:%q ok=%v", sum, ok)
|
||||||
|
}
|
||||||
|
if _, ok := findChecksumLine(content, "frp_0.71.0_windows_amd64.zip"); ok {
|
||||||
|
t.Fatal("不存在的文件不应匹配到哈希")
|
||||||
|
}
|
||||||
|
}
|
||||||
+32
-7
@@ -168,21 +168,46 @@ func downloadOfficialBinary(ctx context.Context, frpDir string, release Release,
|
|||||||
return extractClientBinary(archivePath, frpDir, release)
|
return extractClientBinary(archivePath, frpDir, release)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checksumAssetNames 官方校验文件名候选(按优先级)。
|
||||||
|
// frp 从早期版本起就一直是 frp_sha256_checksums.txt(不带版本号),
|
||||||
|
// 下面保留一个旧名字兼底,避开上游再次改名时整条下载链路直接 404。
|
||||||
|
func checksumAssetCandidates(version string) []string {
|
||||||
|
return []string{
|
||||||
|
"frp_sha256_checksums.txt",
|
||||||
|
"frp_" + version + "_sha256sums.txt",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func fetchChecksum(ctx context.Context, release Release) (string, error) {
|
func fetchChecksum(ctx context.Context, release Release) (string, error) {
|
||||||
version := strings.TrimPrefix(release.Version, "v")
|
version := strings.TrimPrefix(release.Version, "v")
|
||||||
name := "frp_" + version + "_sha256sums.txt"
|
candidates := checksumAssetCandidates(version)
|
||||||
url := officialAssetURL(release.Version, name)
|
var lastErr error
|
||||||
data, err := fetchBytes(ctx, url, 1<<20)
|
for _, name := range candidates {
|
||||||
|
data, err := fetchBytes(ctx, officialAssetURL(release.Version, name), 1<<20)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("下载 FRP 官方校验文件失败:%w", err)
|
lastErr = err
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
if sum, ok := findChecksumLine(data, release.AssetName); ok {
|
||||||
|
return sum, nil
|
||||||
|
}
|
||||||
|
lastErr = fmt.Errorf("FRP 官方校验文件 %s 中未找到 %s", name, release.AssetName)
|
||||||
|
}
|
||||||
|
if lastErr == nil {
|
||||||
|
lastErr = fmt.Errorf("没有可用的官方校验文件")
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("下载 FRP 官方校验文件失败:%v(已尝试:%s)", lastErr, strings.Join(candidates, "、"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// findChecksumLine 解析 sha256sum 格式(<hash> <文件名>,文件名可能带 * 前缀)。
|
||||||
|
func findChecksumLine(data []byte, assetName string) (string, bool) {
|
||||||
for _, line := range strings.Split(string(data), "\n") {
|
for _, line := range strings.Split(string(data), "\n") {
|
||||||
fields := strings.Fields(line)
|
fields := strings.Fields(line)
|
||||||
if len(fields) >= 2 && strings.TrimPrefix(fields[1], "*") == release.AssetName {
|
if len(fields) >= 2 && strings.TrimPrefix(fields[1], "*") == assetName {
|
||||||
return fields[0], nil
|
return fields[0], true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf("FRP 官方校验文件中未找到 %s", release.AssetName)
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadArchive(ctx context.Context, frpDir string, release Release, checksum, proxy string, report downloadReporter) (string, error) {
|
func downloadArchive(ctx context.Context, frpDir string, release Release, checksum, proxy string, report downloadReporter) (string, error) {
|
||||||
|
|||||||
@@ -474,6 +474,10 @@ func SanitizeSettings(values map[string]string) {
|
|||||||
if values[settings.KeyZeroSSLAPIKey] != "" {
|
if values[settings.KeyZeroSSLAPIKey] != "" {
|
||||||
values[settings.KeyZeroSSLAPIKey] = MaskedSecret
|
values[settings.KeyZeroSSLAPIKey] = MaskedSecret
|
||||||
}
|
}
|
||||||
|
// LiteSSL 的 EAB HMAC 同理:属于证书申请凭据,只回掩码(EAB Kid 不是秘密,照常回显)
|
||||||
|
if values[settings.KeyLiteSSLEABHMAC] != "" {
|
||||||
|
values[settings.KeyLiteSSLEABHMAC] = MaskedSecret
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseSaveInputFromMap(values map[string]string) (SaveInput, bool) {
|
func ParseSaveInputFromMap(values map[string]string) (SaveInput, bool) {
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ const (
|
|||||||
KeyZeroSSLAPIKey = "zerossl_api_key"
|
KeyZeroSSLAPIKey = "zerossl_api_key"
|
||||||
// KeyACMEDNSResolvers DNS-01 传播检查用的递归解析器(逗号分隔;system = 用系统解析器)
|
// KeyACMEDNSResolvers DNS-01 传播检查用的递归解析器(逗号分隔;system = 用系统解析器)
|
||||||
KeyACMEDNSResolvers = "acme_dns_resolvers"
|
KeyACMEDNSResolvers = "acme_dns_resolvers"
|
||||||
|
// LiteSSL 强制 EAB:kid 与 hmac 在 FreeSSL 的「证书自动化 → EAB 管理」创建
|
||||||
|
KeyLiteSSLEABKid = "litessl_eab_kid"
|
||||||
|
KeyLiteSSLEABHMAC = "litessl_eab_hmac"
|
||||||
KeyNotifyWebhookURL = "notify_webhook_url"
|
KeyNotifyWebhookURL = "notify_webhook_url"
|
||||||
KeyNotifyType = "notify_type"
|
KeyNotifyType = "notify_type"
|
||||||
KeyNotifyEmailJSON = "notify_email_json"
|
KeyNotifyEmailJSON = "notify_email_json"
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "fonu-web",
|
"name": "fonu-web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.2.13",
|
"version": "1.2.14",
|
||||||
"packageManager": "npm@10.9.4",
|
"packageManager": "npm@10.9.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -216,8 +216,12 @@
|
|||||||
<p v-if="applyCA === 'zerossl' && !zerosslApiKey" class="ca-hint ca-hint--warn">
|
<p v-if="applyCA === 'zerossl' && !zerosslApiKey" class="ca-hint ca-hint--warn">
|
||||||
使用 ZeroSSL 需在「设置」中配置 API Key
|
使用 ZeroSSL 需在「设置」中配置 API Key
|
||||||
</p>
|
</p>
|
||||||
|
<p v-if="applyCA === 'litessl'" class="ca-hint">
|
||||||
|
LiteSSL(亚数 TrustAsia)免费 DV 证书,支持单域名 / 通配符 / 多域名;
|
||||||
|
首次使用需在设置里填 EAB 凭据(FreeSSL 平台「证书自动化 → EAB 管理」创建)
|
||||||
|
</p>
|
||||||
<p v-if="applyCA === 'buypass'" class="ca-hint ca-hint--warn">
|
<p v-if="applyCA === 'buypass'" class="ca-hint ca-hint--warn">
|
||||||
Buypass 已于 2025-10-15 停止签发证书,请改选 Let's Encrypt 或 ZeroSSL
|
Buypass 已于 2025-10-15 停止签发证书,请改选 Let's Encrypt 或 LiteSSL
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
@@ -486,7 +490,7 @@ const caOptions = ref<CertificateCAOption[]>([])
|
|||||||
const applyCAOptions = [
|
const applyCAOptions = [
|
||||||
{ value: 'letsencrypt', label: "Let's Encrypt" },
|
{ value: 'letsencrypt', label: "Let's Encrypt" },
|
||||||
{ value: 'zerossl', label: 'ZeroSSL' },
|
{ value: 'zerossl', label: 'ZeroSSL' },
|
||||||
{ value: 'buypass', label: 'Buypass' },
|
{ value: 'litessl', label: 'LiteSSL' },
|
||||||
]
|
]
|
||||||
const applyCA = ref('letsencrypt')
|
const applyCA = ref('letsencrypt')
|
||||||
const zerosslApiKey = ref('')
|
const zerosslApiKey = ref('')
|
||||||
@@ -547,7 +551,9 @@ const caCardMeta: Record<string, { title: string; sub: string }> = {
|
|||||||
letsencrypt: { title: "Let's Encrypt", sub: '免费 · 自动续期' },
|
letsencrypt: { title: "Let's Encrypt", sub: '免费 · 自动续期' },
|
||||||
'letsencrypt-staging': { title: "Let's Encrypt 测试", sub: '仅用于验证流程' },
|
'letsencrypt-staging': { title: "Let's Encrypt 测试", sub: '仅用于验证流程' },
|
||||||
zerossl: { title: 'ZeroSSL', sub: '免费 · 稳定' },
|
zerossl: { title: 'ZeroSSL', sub: '免费 · 稳定' },
|
||||||
buypass: { title: 'Buypass', sub: '免费 · 备用' },
|
// Buypass 已停服:仍保留元数据,历史证书记录要能正常显示
|
||||||
|
buypass: { title: 'Buypass', sub: '已停止签发' },
|
||||||
|
litessl: { title: 'LiteSSL', sub: '免费 · 国内根' },
|
||||||
}
|
}
|
||||||
|
|
||||||
function providerLabel(v: string) {
|
function providerLabel(v: string) {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
<div class="settings-field">
|
<div class="settings-field">
|
||||||
<div class="settings-field__label">ACME 邮箱</div>
|
<div class="settings-field__label">ACME 邮箱</div>
|
||||||
<n-input v-model:value="acmeEmail" placeholder="admin@example.com" />
|
<n-input v-model:value="acmeEmail" placeholder="admin@example.com" />
|
||||||
<p class="field-hint">用于 Let's Encrypt / ZeroSSL 账户注册</p>
|
<p class="field-hint">用于 Let's Encrypt / ZeroSSL / LiteSSL 账户注册</p>
|
||||||
</div>
|
</div>
|
||||||
<ConfiguredSecretField
|
<ConfiguredSecretField
|
||||||
v-model="zerosslApiKey"
|
v-model="zerosslApiKey"
|
||||||
@@ -88,6 +88,20 @@
|
|||||||
多个用逗号分隔,不写端口默认 53。
|
多个用逗号分隔,不写端口默认 53。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings-field">
|
||||||
|
<div class="settings-field__label">LiteSSL EAB Kid</div>
|
||||||
|
<n-input v-model:value="litesslEabKid" placeholder="FreeSSL → 证书自动化 → EAB 管理 里创建后粘贴" />
|
||||||
|
<p class="field-hint">
|
||||||
|
只在用 LiteSSL 申请时必填。LiteSSL 的目录要求 EAB(externalAccountRequired),
|
||||||
|
凭据在 FreeSSL 平台的「证书自动化 → EAB 管理」创建;EAB Kid 不是密钥,可回显。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ConfiguredSecretField
|
||||||
|
v-model="litesslEabHmac"
|
||||||
|
label="LiteSSL EAB HMAC"
|
||||||
|
:configured="litesslEabHmacConfigured"
|
||||||
|
placeholder="用 LiteSSL 申请证书时填写"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</FonuCard>
|
</FonuCard>
|
||||||
|
|
||||||
@@ -1026,6 +1040,10 @@ const certThreshold = ref(30)
|
|||||||
const logRetention = ref(30)
|
const logRetention = ref(30)
|
||||||
const acmeEmail = ref('')
|
const acmeEmail = ref('')
|
||||||
const acmeResolvers = ref('')
|
const acmeResolvers = ref('')
|
||||||
|
// LiteSSL 的 EAB 凭据:kid 不是秘密(可回显),hmac 是秘密(只回掩码、重输才提交)
|
||||||
|
const litesslEabKid = ref('')
|
||||||
|
const litesslEabHmac = ref('')
|
||||||
|
const litesslEabHmacConfigured = ref(false)
|
||||||
const zerosslApiKey = ref('')
|
const zerosslApiKey = ref('')
|
||||||
const zerosslHasKey = ref(false)
|
const zerosslHasKey = ref(false)
|
||||||
const trustedProxyEnabled = ref(false)
|
const trustedProxyEnabled = ref(false)
|
||||||
@@ -1120,6 +1138,9 @@ function applySettingsToForm(settings: Record<string, string>) {
|
|||||||
statusPageConfigured.value = statusPageCode.value === MASKED_SECRET
|
statusPageConfigured.value = statusPageCode.value === MASKED_SECRET
|
||||||
acmeEmail.value = settings.acme_email ?? ''
|
acmeEmail.value = settings.acme_email ?? ''
|
||||||
acmeResolvers.value = settings.acme_dns_resolvers ?? ''
|
acmeResolvers.value = settings.acme_dns_resolvers ?? ''
|
||||||
|
litesslEabKid.value = settings.litessl_eab_kid ?? ''
|
||||||
|
litesslEabHmacConfigured.value = Boolean(settings.litessl_eab_hmac?.trim())
|
||||||
|
litesslEabHmac.value = ''
|
||||||
zerosslHasKey.value = Boolean(settings.zerossl_api_key?.trim())
|
zerosslHasKey.value = Boolean(settings.zerossl_api_key?.trim())
|
||||||
zerosslApiKey.value = ''
|
zerosslApiKey.value = ''
|
||||||
chinaCIDRHours.value = Number(settings.china_cidr_update_interval_hours ?? 24)
|
chinaCIDRHours.value = Number(settings.china_cidr_update_interval_hours ?? 24)
|
||||||
@@ -1592,6 +1613,7 @@ function buildSavePayload() {
|
|||||||
log_retention_days: String(logRetention.value),
|
log_retention_days: String(logRetention.value),
|
||||||
acme_email: acmeEmail.value.trim(),
|
acme_email: acmeEmail.value.trim(),
|
||||||
acme_dns_resolvers: acmeResolvers.value.trim(),
|
acme_dns_resolvers: acmeResolvers.value.trim(),
|
||||||
|
litessl_eab_kid: litesslEabKid.value.trim(),
|
||||||
trusted_proxy_json: buildTrustedProxyJSON(),
|
trusted_proxy_json: buildTrustedProxyJSON(),
|
||||||
global_ip_blacklist: buildGlobalIPListJSON(globalIPBlacklistText.value),
|
global_ip_blacklist: buildGlobalIPListJSON(globalIPBlacklistText.value),
|
||||||
global_ip_whitelist: buildGlobalWhitelistJSON(),
|
global_ip_whitelist: buildGlobalWhitelistJSON(),
|
||||||
@@ -1604,6 +1626,10 @@ function buildSavePayload() {
|
|||||||
if (zerosslKey) {
|
if (zerosslKey) {
|
||||||
payload.zerossl_api_key = zerosslKey
|
payload.zerossl_api_key = zerosslKey
|
||||||
}
|
}
|
||||||
|
const litesslHmac = litesslEabHmac.value.trim()
|
||||||
|
if (litesslHmac) {
|
||||||
|
payload.litessl_eab_hmac = litesslHmac
|
||||||
|
}
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user