package server import ( "context" "crypto" "crypto/tls" "crypto/x509" "fmt" "log" "net" "net/http" "github.com/rancher/dynamiclistener" "github.com/rancher/dynamiclistener/factory" "github.com/rancher/dynamiclistener/storage/file" "github.com/rancher/dynamiclistener/storage/kubernetes" "github.com/rancher/dynamiclistener/storage/memory" v1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1" "github.com/sirupsen/logrus" "golang.org/x/crypto/acme/autocert" ) type ListenOpts struct { CAChain []*x509.Certificate // Deprecated: Use CAChain instead CA *x509.Certificate CAKey crypto.Signer Storage dynamiclistener.TLSStorage Secrets v1.SecretController CertNamespace string CertName string CANamespace string CAName string CertBackup string AcmeDomains []string BindHost string NoRedirect bool TLSListenerConfig dynamiclistener.Config // Override legacy behavior where server logs written to the application's logrus object // were dropped unless logrus was set to debug-level (such as by launching steve with '--debug'). // Setting this to true results in server logs appearing at an ERROR level. DisplayServerLogs bool } func ListenAndServe(ctx context.Context, httpsPort, httpPort int, handler http.Handler, opts *ListenOpts) error { logger := logrus.StandardLogger() writer := logger.WriterLevel(logrus.DebugLevel) if opts == nil { opts = &ListenOpts{} } if opts.DisplayServerLogs { writer = logger.WriterLevel(logrus.ErrorLevel) } // Otherwise preserve legacy behaviour of displaying server logs only in debug mode. errorLog := log.New(writer, "", log.LstdFlags) if opts.TLSListenerConfig.TLSConfig == nil { opts.TLSListenerConfig.TLSConfig = &tls.Config{} } if httpsPort > 0 { tlsTCPListener, err := dynamiclistener.NewTCPListener(opts.BindHost, httpsPort) if err != nil { return err } tlsTCPListener, handler, err = getTLSListener(ctx, tlsTCPListener, handler, *opts) if err != nil { return err } if !opts.NoRedirect { handler = dynamiclistener.HTTPRedirect(handler) } tlsServer := http.Server{ Handler: handler, BaseContext: func(listener net.Listener) context.Context { return ctx }, ErrorLog: errorLog, } go func() { logrus.Infof("Listening on %s:%d", opts.BindHost, httpsPort) err := tlsServer.Serve(tlsTCPListener) if err != http.ErrServerClosed && err != nil { logrus.Fatalf("https server failed: %v", err) } }() go func() { <-ctx.Done() tlsServer.Shutdown(context.Background()) }() } if httpPort > 0 { httpServer := http.Server{ Addr: fmt.Sprintf("%s:%d", opts.BindHost, httpPort), Handler: handler, ErrorLog: errorLog, BaseContext: func(listener net.Listener) context.Context { return ctx }, } go func() { logrus.Infof("Listening on %s:%d", opts.BindHost, httpPort) err := httpServer.ListenAndServe() if err != http.ErrServerClosed && err != nil { logrus.Fatalf("http server failed: %v", err) } }() go func() { <-ctx.Done() httpServer.Shutdown(context.Background()) }() } return nil } func getTLSListener(ctx context.Context, tcp net.Listener, handler http.Handler, opts ListenOpts) (net.Listener, http.Handler, error) { if len(opts.TLSListenerConfig.TLSConfig.NextProtos) == 0 { opts.TLSListenerConfig.TLSConfig.NextProtos = []string{"h2", "http/1.1"} } if len(opts.TLSListenerConfig.TLSConfig.Certificates) > 0 { return tls.NewListener(tcp, opts.TLSListenerConfig.TLSConfig), handler, nil } if len(opts.AcmeDomains) > 0 { return acmeListener(tcp, handler, opts) } storage := opts.Storage if storage == nil { storage = newStorage(ctx, opts) } caCert, caKey, err := getCA(opts) if err != nil { return nil, nil, err } listener, dynHandler, err := dynamiclistener.NewListenerWithChain(tcp, storage, caCert, caKey, opts.TLSListenerConfig) if err != nil { return nil, nil, err } return listener, wrapHandler(dynHandler, handler), nil } func getCA(opts ListenOpts) ([]*x509.Certificate, crypto.Signer, error) { if opts.CAKey != nil { if opts.CAChain != nil { return opts.CAChain, opts.CAKey, nil } else if opts.CA != nil { return []*x509.Certificate{opts.CA}, opts.CAKey, nil } } if opts.Secrets == nil { return factory.LoadOrGenCAChain() } if opts.CAName == "" { opts.CAName = "serving-ca" } if opts.CANamespace == "" { opts.CANamespace = opts.CertNamespace } if opts.CANamespace == "" { opts.CANamespace = "kube-system" } return kubernetes.LoadOrGenCAChain(opts.Secrets, opts.CANamespace, opts.CAName) } func newStorage(ctx context.Context, opts ListenOpts) dynamiclistener.TLSStorage { var result dynamiclistener.TLSStorage if opts.CertBackup == "" { result = memory.New() } else { result = memory.NewBacked(file.New(opts.CertBackup)) } if opts.Secrets == nil { return result } if opts.CertName == "" { opts.CertName = "serving-cert" } if opts.CertNamespace == "" { opts.CertNamespace = "kube-system" } return kubernetes.Load(ctx, opts.Secrets, opts.CertNamespace, opts.CertName, result) } func wrapHandler(handler http.Handler, next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { handler.ServeHTTP(rw, req) next.ServeHTTP(rw, req) }) } func acmeListener(tcp net.Listener, handler http.Handler, opts ListenOpts) (net.Listener, http.Handler, error) { hosts := map[string]bool{} for _, domain := range opts.AcmeDomains { hosts[domain] = true } manager := autocert.Manager{ Cache: autocert.DirCache("certs-cache"), Prompt: func(tosURL string) bool { return true }, HostPolicy: func(ctx context.Context, host string) error { if !hosts[host] { return fmt.Errorf("host %s is not configured", host) } return nil }, } opts.TLSListenerConfig.TLSConfig.GetCertificate = func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { if hello.ServerName == "localhost" || hello.ServerName == "" { newHello := *hello newHello.ServerName = opts.AcmeDomains[0] return manager.GetCertificate(&newHello) } return manager.GetCertificate(hello) } return tls.NewListener(tcp, opts.TLSListenerConfig.TLSConfig), manager.HTTPHandler(handler), nil }