diff options
Diffstat (limited to 'internal/server/tls.go')
-rw-r--r-- | internal/server/tls.go | 191 |
1 files changed, 191 insertions, 0 deletions
diff --git a/internal/server/tls.go b/internal/server/tls.go new file mode 100644 index 0000000..9f22a5e --- /dev/null +++ b/internal/server/tls.go @@ -0,0 +1,191 @@ +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) +} |