diff --git a/guestbook/go.mod b/guestbook/go.mod index 439111e..55fdd63 100644 --- a/guestbook/go.mod +++ b/guestbook/go.mod @@ -2,16 +2,22 @@ module github.com/cert-manager/print-your-cert/guestbook go 1.22 -require modernc.org/sqlite v1.29.1 +require ( + golang.org/x/crypto v0.21.0 + modernc.org/sqlite v1.29.1 +) require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/uuid v1.3.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/inconshreveable/go-vhost v1.0.0 github.com/mattn/go-isatty v0.0.16 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - golang.org/x/sys v0.16.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect modernc.org/libc v1.41.0 // indirect modernc.org/mathutil v1.6.0 // indirect diff --git a/guestbook/go.sum b/guestbook/go.sum index 5aba5c1..59c4b44 100644 --- a/guestbook/go.sum +++ b/guestbook/go.sum @@ -6,6 +6,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/inconshreveable/go-vhost v1.0.0 h1:IK4VZTlXL4l9vz2IZoiSFbYaaqUW7dXJAiPriUN5Ur8= +github.com/inconshreveable/go-vhost v1.0.0/go.mod h1:aA6DnFhALT3zH0y+A39we+zbrdMC2N0X/q21e6FI0LU= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= @@ -16,11 +18,17 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= diff --git a/guestbook/main.go b/guestbook/main.go index 9e21a48..677924e 100644 --- a/guestbook/main.go +++ b/guestbook/main.go @@ -19,6 +19,8 @@ import ( "text/tabwriter" "time" + "github.com/inconshreveable/go-vhost" + "golang.org/x/crypto/acme/autocert" _ "modernc.org/sqlite" ) @@ -27,6 +29,12 @@ var ( caCertPath = flag.String("ca-cert", "", "Path to CA certs to trust for client certs") chainPath = flag.String("tls-chain", "", "Path to TLS cert chain") privateKeyPath = flag.String("tls-key", "", "Path to TLS private key") + mainDomain = flag.String("domain", "guestbook.print-your-cert.cert-manager.io", "Domain used to access the guestbook. Used for SNI routing.") + + readOnlyDomain = flag.String("readonly-domain", "readonly-guestbook.print-your-cert.cert-manager.io", "Domain used to access the guestbook in read-only mode") + readOnlyListenInsecure = flag.String("readonly-listen-insecure", "0.0.0.0:8080", "Address and port to listen on. Must be 80 if -prod is set.") + readOnlyProd = flag.Bool("prod", false, "If true, enables HTTPS for the readonly domain using Let's Encrypt.") + readOnlyAutocertDir = flag.String("autocert-dir", ".", "The directory used to cache the certificate and temporary files to work with Let's Encrypt.") dbPath = flag.String("db-path", "guestbook.sqlite", "Path to sqlite database") initDB = flag.Bool("init-db", false, "If set, initialise a fresh database at db-path") @@ -224,8 +232,15 @@ func run(ctx context.Context) error { return fmt.Errorf("missing required path to CA cert") } + if *readOnlyProd && !strings.HasSuffix(*readOnlyListenInsecure, ":80") { + return fmt.Errorf("with -prod, use -readonly-listen-insecure=:80 so that Let's Encrypt can verify the domain") + } + logger := LoggerFromContext(ctx) + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT) + db, err := sql.Open("sqlite", *dbPath) if err != nil { return err @@ -241,43 +256,96 @@ func run(ctx context.Context) error { return err } - serveMux := http.NewServeMux() + listener, err := net.Listen("tcp", *listen) + if err != nil { + return fmt.Errorf("failed to create TCP listener: %s", err) + } + tlsMux, err := vhost.NewTLSMuxer(listener, time.Second*5) + if err != nil { + return fmt.Errorf("failed to create vhost muxer: %s", err) + } + serveMux := http.NewServeMux() serveMux.Handle("GET /", certExtractMiddleware(indexPage(db))) serveMux.Handle("POST /write", certExtractMiddleware(writePage(db))) - server := &http.Server{ Handler: serveMux, - BaseContext: func(_ net.Listener) context.Context { return ctx }, - ErrorLog: slog.NewLogLogger(logger.With("handler", "http.Server").Handler(), slog.LevelError), + BaseContext: func(l net.Listener) context.Context { return ctx }, + ErrorLog: slog.NewLogLogger(logger.With("handler", "http.Server", "server", "main-server").Handler(), slog.LevelError), } - - listener, err := net.Listen("tcp", *listen) + serverListener, err := tlsMux.Listen(*mainDomain) if err != nil { - return fmt.Errorf("failed to create TCP listener: %s", err) + return fmt.Errorf("failed to create vhost listener: %s", err) } + go func() { + logger.Info("main-server listening", "address", *listen, "sni", *mainDomain) + err := server.Serve(tls.NewListener(serverListener, &tls.Config{ + Certificates: []tls.Certificate{cert}, + ClientCAs: certPool, + ClientAuth: tls.RequireAndVerifyClientCert, + })) + if err != nil && err != http.ErrServerClosed { + logger.Info("failed to listen", "error", err) + } + }() - tlsConfig := &tls.Config{ - Certificates: []tls.Certificate{cert}, - ClientCAs: certPool, - ClientAuth: tls.RequireAndVerifyClientCert, + readOnlyMux := http.NewServeMux() + readOnlyMux.Handle("GET /", indexPage(db)) + readOnlySrv := &http.Server{ + Handler: readOnlyMux, + BaseContext: func(_ net.Listener) context.Context { return ctx }, + ErrorLog: slog.NewLogLogger(logger.With("handler", "http.Server", "server", "readonly-server-https").Handler(), slog.LevelError), + } + mgr := &autocert.Manager{ + Prompt: autocert.AcceptTOS, + HostPolicy: func(ctx context.Context, host string) error { + allowedHost := *readOnlyDomain + if host == allowedHost { + return nil + } + return fmt.Errorf("acme/autocert: only %s host is allowed", allowedHost) + }, + Cache: autocert.DirCache(*readOnlyAutocertDir), + } + readOnlyListener, err := tlsMux.Listen(*readOnlyDomain) + if err != nil { + return fmt.Errorf("failed to create vhost listener: %s", err) } - listener = tls.NewListener(listener, tlsConfig) - - logger.Info("listening", "address", *listen) - - sigs := make(chan os.Signal, 1) - - signal.Notify(sigs, syscall.SIGINT) + go func() { + if !*readOnlyProd { + logger.Info("readonly-server not enabled, use -prod to turn it on") + return + } + logger.Info("readonly-server-https listening", "address", *listen, "sni", *readOnlyDomain) + err := readOnlySrv.Serve(tls.NewListener(readOnlyListener, &tls.Config{ + GetCertificate: mgr.GetCertificate, + })) + if err != nil && err != http.ErrServerClosed { + logger.Info("failed to listen", "error", err) + } + }() + readOnlySrvHTTP := &http.Server{ + Handler: readOnlyMux, + BaseContext: func(_ net.Listener) context.Context { return ctx }, + ErrorLog: slog.NewLogLogger(logger.With("handler", "http.Server", "server", "readonly-server-http").Handler(), slog.LevelError), + Addr: *readOnlyListenInsecure, + } + if mgr != nil { + // Allows autocert handle Let's Encrypt HTTP-01 callbacks. + readOnlySrv.Handler = mgr.HTTPHandler(readOnlySrvHTTP.Handler) + } go func() { - err := server.Serve(listener) + logger.Info("readonly-server-http listening", "address", *readOnlyListenInsecure) + err := readOnlySrv.ListenAndServe() if err != nil && err != http.ErrServerClosed { logger.Info("failed to listen", "error", err) } }() + go tlsMux.HandleErrors() + <-sigs logger.Info("shutting down") @@ -285,6 +353,16 @@ func run(ctx context.Context) error { if err != nil { return err } + err = readOnlySrv.Shutdown(context.Background()) + if err != nil { + return err + } + if readOnlySrvHTTP != nil { + err = readOnlySrvHTTP.Shutdown(context.Background()) + if err != nil { + return err + } + } return nil }