From 855ed52a8b652071241dfd9683a6811840e4f28c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Thu, 8 Jan 2026 18:17:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E8=AF=81=E4=B9=A6?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/acme/solvers.go | 123 +++++++++++++++++++++++++++++--------------- 1 file changed, 81 insertions(+), 42 deletions(-) diff --git a/pkg/acme/solvers.go b/pkg/acme/solvers.go index 87c79013..dc0c6ce6 100644 --- a/pkg/acme/solvers.go +++ b/pkg/acme/solvers.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "strings" + "sync" "time" "github.com/libdns/alidns" @@ -27,56 +28,65 @@ import ( "github.com/acepanel/panel/pkg/systemctl" ) +var panelSolverGlobal sync.Mutex + type panelSolver struct { ip []string conf string server *http.Server + // tokens 存储所有待验证的 challenge,key 为路径,value 为 token + tokens map[string]string + // presentCount Present 调用计数 + presentCount int + // cleanupCount CleanUp 调用计数 + cleanupCount int + // useBuiltin 标记是否使用内置 HTTP 服务器 + useBuiltin bool } func (s *panelSolver) Present(_ context.Context, challenge acme.Challenge) error { - // 如果 80 端口没有被占用,则直接起一个内置的 HTTP 服务器验证 + if s.presentCount == 0 { + panelSolverGlobal.Lock() + } + + path := challenge.HTTP01ResourcePath() + token := challenge.KeyAuthorization + + // 初始化 tokens map + if s.tokens == nil { + s.tokens = make(map[string]string) + } + + // 收集所有域名的 token + s.tokens[path] = token + s.presentCount++ + if s.presentCount < len(s.ip) { + return nil + } + + // 如果 80 端口没有被占用,则使用内置的 HTTP 服务器 if !pkgos.TCPPortInUse(80) { - return s.presentPanel(challenge) + s.useBuiltin = true + return s.startServer() } - conf := fmt.Sprintf(`server { - listen 80; - server_name %s; - location = %s { - default_type text/plain; - return 200 %q; - } -} -`, s.ip, challenge.HTTP01ResourcePath(), challenge.KeyAuthorization) - - f, err := os.OpenFile(s.conf, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return fmt.Errorf("failed to open nginx config %q: %w", s.conf, err) - } - defer func(f *os.File) { _ = f.Close() }(f) - - if _, err = f.Write([]byte(conf)); err != nil { - return fmt.Errorf("failed to write to nginx config %q: %w", s.conf, err) - } - - if err = systemctl.Reload("nginx"); err != nil { - _, err = shell.Execf("nginx -t") - return fmt.Errorf("failed to reload nginx: %w", err) - } - - return nil + // 否则使用 nginx 配置 + s.useBuiltin = false + return s.writeNginxConfig() } -func (s *panelSolver) presentPanel(challenge acme.Challenge) error { - mux := http.NewServeMux() - mux.HandleFunc(challenge.HTTP01ResourcePath(), func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/plain") - _, _ = w.Write([]byte(challenge.KeyAuthorization)) - }) - +func (s *panelSolver) startServer() error { s.server = &http.Server{ - Addr: ":80", - Handler: mux, + Addr: ":80", + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token, ok := s.tokens[r.URL.Path] + if !ok { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "text/plain") + _, _ = w.Write([]byte(token)) + }), } errChan := make(chan error, 1) @@ -90,16 +100,45 @@ func (s *panelSolver) presentPanel(challenge acme.Challenge) error { // 等待一小段时间确保服务器启动成功 select { case err := <-errChan: + s.server = nil return fmt.Errorf("failed to start HTTP server: %w", err) case <-time.After(100 * time.Millisecond): return nil } } -// CleanUp cleans up the HTTP server if it is the last one to finish. +func (s *panelSolver) writeNginxConfig() error { + var conf strings.Builder + conf.WriteString(fmt.Sprintf("server {\n listen 80;\n server_name %s;\n", strings.Join(s.ip, " "))) + for path, token := range s.tokens { + conf.WriteString(fmt.Sprintf(" location = %s {\n default_type text/plain;\n return 200 %q;\n }\n", path, token)) + } + conf.WriteString("}\n") + + if err := os.WriteFile(s.conf, []byte(conf.String()), 0644); err != nil { + return fmt.Errorf("failed to write nginx config %q: %w", s.conf, err) + } + + if err := systemctl.Reload("nginx"); err != nil { + _, err = shell.Execf("nginx -t") + return fmt.Errorf("failed to reload nginx: %w", err) + } + + return nil +} + +// CleanUp cleans up the HTTP server on last call. func (s *panelSolver) CleanUp(ctx context.Context, _ acme.Challenge) error { - // 如果启动了内置 HTTP 服务器,则关闭它 - if s.server != nil { + s.cleanupCount++ + + // 等待最后一次 CleanUp + if s.cleanupCount < len(s.ip) { + return nil + } + + defer panelSolverGlobal.Unlock() + + if s.useBuiltin && s.server != nil { shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() if err := s.server.Shutdown(shutdownCtx); err != nil { @@ -109,13 +148,13 @@ func (s *panelSolver) CleanUp(ctx context.Context, _ acme.Challenge) error { return nil } - // 否则清理 nginx 配置 + // 清理 nginx 配置 if err := os.WriteFile(s.conf, []byte(""), 0644); err != nil { return fmt.Errorf("failed to write to nginx config %q: %w", s.conf, err) } if err := systemctl.Reload("nginx"); err != nil { - _, err = shell.Execf("nginx -t") + _, _ = shell.Execf("nginx -t") return fmt.Errorf("failed to reload nginx: %w", err) }