Files
mrlee dda0eb876e feat(cert,scripts,docs): Linux 原生安装包与证书申请三处修复(DNS 解析器 / Buypass 下线 / ZeroSSL 报错与密钥脱敏)(v1.2.13)
- Linux 安装包:scripts/build-linux.ps1 交叉编译出 fonu-<版本>-linux-{amd64,arm64}.tar.gz(二进制 + migrations + systemd 安装脚本 + 运行时资源 + sha256),scripts/fonu-install.sh 负责装机(/opt/fonu、/var/lib/fonu、/etc/fonu/fonu.env 首次生成随机会话密钥)并新增 --reset-admin;装完直接打印初始管理员口令

- 证书 DNS-01:lego 默认读系统 resolv.conf(容器里是 Docker 的 127.0.0.11,不返回权威 NS)导致 could not determine authoritative nameservers;现显式传 AddRecursiveNameservers,默认 223.5.5.5/1.1.1.1/8.8.8.8,设置项 acme_dns_resolvers 可覆盖(system = 回到系统解析器)

- Buypass 已停止签发(2025-10-15)并终止服务(2026-04-15):从可选 CA 移除,历史记录仍可展示,直接提交会被拦下并给出中文原因

- ZeroSSL:422 时带出 error.type/error.code(非 JSON 退回截断正文);同时修掉网络错误把含 access_key 的完整 URL 写进任务日志与 app.log 的密钥泄露

- 测试:解析器列表解析 3 例 + ZeroSSL EAB 4 例(含不泄露密钥断言);gofmt / go build / go vet / go test 全绿
2026-09-27 10:10:15 +08:00

95 lines
3.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package acme
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
type zeroSSLEABResponse struct {
Success int `json:"success"`
EABKid string `json:"eab_kid"`
EABHmacKey string `json:"eab_hmac_key"`
Error struct {
Code int `json:"code"`
Type string `json:"type"`
} `json:"error"`
}
// zeroSSLEABEndpoint 单独成变量,测试可以替换成本地 httptest 服务。
var zeroSSLEABEndpoint = "https://api.zerossl.com/acme/eab-credentials"
func zerosslEABCredentials(ctx context.Context, apiKey string) (kid, hmac string, err error) {
apiKey = strings.TrimSpace(apiKey)
if apiKey == "" {
return "", "", fmt.Errorf("请在设置中配置 ZeroSSL API Key")
}
endpoint := zeroSSLEABEndpoint + "?access_key=" + url.QueryEscape(apiKey)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return "", "", err
}
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
// 不能把 url.Error 整个包进错误:它的字符串里是带 access_key 的完整 URL,
// 而这条错误会进任务日志与 <数据目录>/logs/app.log,等于把密钥写进日志。
return "", "", fmt.Errorf("请求 ZeroSSL API 失败:%s", redactURLError(err))
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return "", "", err
}
var payload zeroSSLEABResponse
parseErr := json.Unmarshal(body, &payload)
if resp.StatusCode != http.StatusOK {
// 422 基本都是 access_key 无效(也可能是账号/权限问题)。ZeroSSL 会在
// error.type 与 error.code 里写清原因,所以带上它,而不是只回状态码让用户猜。
if parseErr == nil && payload.Error.Type != "" {
return "", "", fmt.Errorf("ZeroSSL API 返回 %d:%s(错误码 %d)", resp.StatusCode, payload.Error.Type, payload.Error.Code)
}
return "", "", fmt.Errorf("ZeroSSL API 返回 %d:%s", resp.StatusCode, snippet(body))
}
if parseErr != nil {
return "", "", fmt.Errorf("解析 ZeroSSL 响应失败:%w", parseErr)
}
if payload.Success != 1 || payload.EABKid == "" || payload.EABHmacKey == "" {
if payload.Error.Type != "" {
return "", "", fmt.Errorf("ZeroSSL API 错误:%s(错误码 %d)", payload.Error.Type, payload.Error.Code)
}
return "", "", fmt.Errorf("ZeroSSL API 未返回有效的 EAB 凭据")
}
return payload.EABKid, payload.EABHmacKey, nil
}
// redactURLError 只保留底层错误原因,避免把带 access_key 的 URL 写进日志。
func redactURLError(err error) string {
var urlErr *url.Error
if errors.As(err, &urlErr) && urlErr.Err != nil {
return urlErr.Err.Error()
}
return err.Error()
}
// snippet 压平并截断响应正文:既不把整页 HTML 灌进日志,也不让换行破坏排版。
func snippet(body []byte) string {
text := strings.Join(strings.Fields(string(body)), " ")
if text == "" {
return "(响应体为空)"
}
if len(text) > 200 {
text = text[:200] + "…"
}
return text
}