about summary refs log tree commit diff stats
path: root/internal/server/tls.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/server/tls.go')
-rw-r--r--internal/server/tls.go148
1 files changed, 128 insertions, 20 deletions
diff --git a/internal/server/tls.go b/internal/server/tls.go
index 370134c..85eaac9 100644
--- a/internal/server/tls.go
+++ b/internal/server/tls.go
@@ -2,12 +2,18 @@ package server
 
 import (
 	"context"
+	"crypto/x509"
+	"net"
+	"net/http"
+	"strconv"
+
+	"go.alanpearce.eu/x/listenfd"
 
 	"github.com/ardanlabs/conf/v3"
 	"github.com/caddyserver/caddy/v2"
 	"github.com/caddyserver/certmagic"
 	certmagic_redis "github.com/pberkel/caddy-storage-redis"
-	"github.com/pkg/errors"
+	"gitlab.com/tozd/go/errors"
 )
 
 type redisConfig struct {
@@ -19,32 +25,134 @@ type redisConfig struct {
 }
 
 func (s *Server) serveTLS() (err error) {
-	rc := &redisConfig{}
-	_, err = conf.Parse("REDIS", rc)
+	var issuer *certmagic.ACMEIssuer
+	log := s.log.Named("tls")
+
+	// setting cfg.Logger is too late somehow
+	certmagic.Default.Logger = log.GetLogger().Named("certmagic")
+	cfg := certmagic.NewDefault()
+	cfg.DefaultServerName = s.config.Domains[0]
+
+	issuer = &certmagic.DefaultACME
+	certmagic.DefaultACME.Agreed = true
+	certmagic.DefaultACME.Email = s.config.Email
+
+	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,
+		})
+		cfg.Issuers[0] = issuer
+	} else {
+		rc := &redisConfig{}
+		_, err = conf.Parse("REDIS", rc)
+		if err != nil {
+			return errors.Wrap(err, "could not parse redis config")
+		}
+
+		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")
+		}
+	}
+
+	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 parse redis config")
+		return errors.Wrap(err, "could not bind plain socket")
 	}
 
-	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
+	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
 
-	certmagic.Default.Storage = rs
-	err = rs.Provision(caddy.Context{
-		Context: context.Background(),
+		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.ManageSync(context.TODO(), s.config.Domains)
 	if err != nil {
-		return errors.Wrap(err, "could not provision redis storage")
+		return errors.Wrap(err, "could not enable TLS")
 	}
+	tlsConfig := cfg.TLSConfig()
+	tlsConfig.NextProtos = append([]string{"h2", "http/1.1"}, tlsConfig.NextProtos...)
 
-	certmagic.DefaultACME.Agreed = true
-	certmagic.DefaultACME.Email = s.config.Email
-	certmagic.Default.DefaultServerName = s.config.Domains[0]
-	certmagic.HTTPPort = s.runtimeConfig.Port
-	certmagic.HTTPSPort = s.runtimeConfig.TLSPort
+	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 certmagic.HTTPS(s.config.Domains, s.Server.Handler)
+	return s.Serve(sln)
 }