package server import ( "context" "crypto/x509" "net" "net/http" "slices" "strconv" "go.alanpearce.eu/x/listenfd" "github.com/ardanlabs/conf/v3" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/certmagic" "github.com/libdns/acmedns" certmagic_redis "github.com/pberkel/caddy-storage-redis" "gitlab.com/tozd/go/errors" ) type redisConfig struct { Address string `conf:"required"` Username string `conf:"default:default"` Password string `conf:"required"` EncryptionKey string `conf:"required"` KeyPrefix string `conf:"default:certmagic"` } type acmeConfig struct { Username string `conf:"required"` Password string `conf:"required"` Subdomain string `conf:"required"` ServerURL string `conf:"env:SERVER_URL,default:https://acme.alanpearce.eu"` } func (s *Server) serveTLS() (err error) { log := s.log.Named("tls") wildcardDomain := "*." + s.config.WildcardDomain certificateDomains := slices.Clone(s.config.Domains) // setting cfg.Logger is too late somehow certmagic.Default.Logger = log.GetLogger().Named("certmagic") cfg := certmagic.NewDefault() cfg.DefaultServerName = s.config.Domains[0] var issuer *certmagic.ACMEIssuer if s.runtimeConfig.Development { ca := s.runtimeConfig.ACMECA if ca == "" { return errors.New("can't enable tls in development without an ACME_CA") } cp, err := x509.SystemCertPool() if err != nil { log.Warn("could not get system certificate pool", "error", err) cp = x509.NewCertPool() } if cacert := s.runtimeConfig.ACMECACert; cacert != "" { cp.AppendCertsFromPEM([]byte(cacert)) } // caddy's ACME server (step-ca) doesn't specify an OCSP server cfg.OCSP.DisableStapling = true issuer = certmagic.NewACMEIssuer(cfg, certmagic.ACMEIssuer{ CA: s.runtimeConfig.ACMECA, TrustedRoots: cp, DisableTLSALPNChallenge: true, ListenHost: s.runtimeConfig.ListenAddress, AltHTTPPort: s.runtimeConfig.Port, AltTLSALPNPort: s.runtimeConfig.TLSPort, Logger: certmagic.Default.Logger, }) } else { rc := &redisConfig{} _, err = conf.Parse("REDIS", rc) if err != nil { return errors.Wrap(err, "could not parse redis config") } acme := &acmedns.Provider{} _, err = conf.Parse("ACME", acme) if err != nil { return errors.Wrap(err, "could not parse ACME config") } issuer = certmagic.NewACMEIssuer(cfg, certmagic.ACMEIssuer{ CA: certmagic.LetsEncryptProductionCA, Email: s.config.Email, Agreed: true, Logger: certmagic.Default.Logger, DNS01Solver: &certmagic.DNS01Solver{ DNSManager: certmagic.DNSManager{ DNSProvider: acme, Logger: certmagic.Default.Logger, }, }, }) certificateDomains = append(slices.Clone(s.config.Domains), wildcardDomain) log.Info("acme", "username", acme.Username, "subdomain", acme.Subdomain, "server_url", acme.ServerURL) rs := certmagic_redis.New() rs.Address = []string{rc.Address} rs.Username = rc.Username rs.Password = rc.Password rs.EncryptionKey = rc.EncryptionKey rs.KeyPrefix = rc.KeyPrefix cfg.Storage = rs err = rs.Provision(caddy.Context{ Context: context.Background(), }) if err != nil { return errors.Wrap(err, "could not provision redis storage") } } cfg.Issuers[0] = issuer ln, err := listenfd.GetListener( 1, net.JoinHostPort(s.runtimeConfig.ListenAddress, strconv.Itoa(s.runtimeConfig.Port)), log.Named("listenfd"), ) if err != nil { return errors.Wrap(err, "could not bind plain socket") } go func(ln net.Listener, srv *http.Server) { httpMux := http.NewServeMux() httpMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if certmagic.LooksLikeHTTPChallenge(r) && issuer.HandleHTTPChallenge(w, r) { return } url := r.URL url.Scheme = "https" port := s.config.BaseURL.Port() if port == "" { url.Host = r.Host } else { host, _, err := net.SplitHostPort(r.Host) if err != nil { log.Warn("error splitting host and port", "error", err) host = r.Host } url.Host = net.JoinHostPort(host, s.config.BaseURL.Port()) } http.Redirect(w, r, url.String(), http.StatusMovedPermanently) }) srv.Handler = httpMux if err := srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Error("error in http handler", "error", err) } }(ln, &http.Server{ ReadHeaderTimeout: s.ReadHeaderTimeout, ReadTimeout: s.ReadTimeout, WriteTimeout: s.WriteTimeout, IdleTimeout: s.IdleTimeout, }) log.Debug( "starting certmagic", "http_port", s.runtimeConfig.Port, "https_port", s.runtimeConfig.TLSPort, ) err = cfg.ManageAsync(context.TODO(), certificateDomains) if err != nil { return errors.Wrap(err, "could not enable TLS") } tlsConfig := cfg.TLSConfig() tlsConfig.NextProtos = append([]string{"h2", "http/1.1"}, tlsConfig.NextProtos...) sln, err := listenfd.GetListenerTLS( 0, net.JoinHostPort(s.runtimeConfig.ListenAddress, strconv.Itoa(s.runtimeConfig.TLSPort)), tlsConfig, log.Named("listenfd"), ) if err != nil { return errors.Wrap(err, "could not bind tls socket") } return s.Serve(sln) }