package acmecert import ( "net/http" "strings" "sync" "mailgoserver/internal/toolbox" ) // HTTP01Server is a long-lived HTTP server dedicated to the [LetsEncryptHTTP] HTTP-01 // challenge — started once at boot (if enabled) and kept running for the whole process // lifetime, unlike lego's own http01.ProviderServer (challenge/http01), which binds and // unbinds the port on every single obtain/renew. Staying up lets an operator behind // NAT/a reverse proxy verify their port-forwarding actually reaches this host (curl it // directly, get a 200) without waiting for or burning a real, rate-limited ACME attempt. // It implements lego's challenge.Provider interface (Present/CleanUp) so a Manager hands // it token/keyAuth pairs as they come, instead of each obtain spinning up its own // server. type HTTP01Server struct { mu sync.RWMutex tokens map[string]string // token -> keyAuth server *http.Server } func NewHTTP01Server() *HTTP01Server { s := &HTTP01Server{tokens: map[string]string{}} mux := http.NewServeMux() mux.HandleFunc("/.well-known/acme-challenge/", s.serveChallenge) mux.HandleFunc("/", s.serveHealth) s.server = &http.Server{Handler: mux} return s } func (s *HTTP01Server) serveHealth(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusOK) w.Write([]byte("ok")) } func (s *HTTP01Server) serveChallenge(w http.ResponseWriter, r *http.Request) { token := strings.TrimPrefix(r.URL.Path, "/.well-known/acme-challenge/") s.mu.RLock() keyAuth, ok := s.tokens[token] s.mu.RUnlock() if !ok { http.NotFound(w, r) return } w.Header().Set("Content-Type", "text/plain") w.Write([]byte(keyAuth)) } // Start binds addr (e.g. ":80") and serves in the background until the process exits. // Call once at boot. A bind failure (port already in use, missing // CAP_NET_BIND_SERVICE) is logged rather than crashing the process — HTTP-01 // obtain/renew attempts then fail with a clear error from lego instead, same as any // other misconfiguration surfaced via Status.LastError. func (s *HTTP01Server) Start(addr string, logger *toolbox.Logger) { s.server.Addr = addr go func() { if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Error("HTTP-01 challenge server: %v", err) } }() } // Present and CleanUp implement github.com/go-acme/lego/v4/challenge.Provider. func (s *HTTP01Server) Present(domain, token, keyAuth string) error { s.mu.Lock() s.tokens[token] = keyAuth s.mu.Unlock() return nil } func (s *HTTP01Server) CleanUp(domain, token, keyAuth string) error { s.mu.Lock() delete(s.tokens, token) s.mu.Unlock() return nil }