Files
mailgoserver/internal/acmecert/http01server.go
T

83 lines
2.6 KiB
Go

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
}