83 lines
2.6 KiB
Go
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
|
|
}
|