From 1cdaedc9f0673c3d2784365c7dcd63072241498d Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 13 Apr 2023 08:43:46 -0500 Subject: [PATCH 1/2] chore: Export all functions used by server cmd Required to make workspace proxy cmd --- cli/agent.go | 8 ++++---- cli/root.go | 4 ++-- cli/server.go | 36 ++++++++++++++++++------------------ 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/cli/agent.go b/cli/agent.go index 697648b4a6c2a..8fc2f3f6e3042 100644 --- a/cli/agent.go +++ b/cli/agent.go @@ -88,9 +88,9 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd { ctx, stopNotify := signal.NotifyContext(ctx, InterruptSignals...) defer stopNotify() - // dumpHandler does signal handling, so we call it after the + // DumpHandler does signal handling, so we call it after the // reaper. - go dumpHandler(ctx) + go DumpHandler(ctx) ljLogger := &lumberjack.Logger{ Filename: filepath.Join(logDir, "coder-agent.log"), @@ -119,7 +119,7 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd { // Enable pprof handler // This prevents the pprof import from being accidentally deleted. _ = pprof.Handler - pprofSrvClose := serveHandler(ctx, logger, nil, pprofAddress, "pprof") + pprofSrvClose := ServeHandler(ctx, logger, nil, pprofAddress, "pprof") defer pprofSrvClose() // Do a best effort here. If this fails, it's not a big deal. if port, err := urlPort(pprofAddress); err == nil { @@ -262,7 +262,7 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd { return cmd } -func serveHandler(ctx context.Context, logger slog.Logger, handler http.Handler, addr, name string) (closeFunc func()) { +func ServeHandler(ctx context.Context, logger slog.Logger, handler http.Handler, addr, name string) (closeFunc func()) { logger.Debug(ctx, "http server listening", slog.F("addr", addr), slog.F("name", name)) // ReadHeaderTimeout is purposefully not enabled. It caused some issues with diff --git a/cli/root.go b/cli/root.go index 368520ac08f70..a71626ccdf3ef 100644 --- a/cli/root.go +++ b/cli/root.go @@ -711,7 +711,7 @@ func (h *headerTransport) RoundTrip(req *http.Request) (*http.Response, error) { return h.transport.RoundTrip(req) } -// dumpHandler provides a custom SIGQUIT and SIGTRAP handler that dumps the +// DumpHandler provides a custom SIGQUIT and SIGTRAP handler that dumps the // stacktrace of all goroutines to stderr and a well-known file in the home // directory. This is useful for debugging deadlock issues that may occur in // production in workspaces, since the default Go runtime will only dump to @@ -723,7 +723,7 @@ func (h *headerTransport) RoundTrip(req *http.Request) (*http.Response, error) { // A SIGQUIT handler will not be registered if GOTRACEBACK=crash. // // On Windows this immediately returns. -func dumpHandler(ctx context.Context) { +func DumpHandler(ctx context.Context) { if runtime.GOOS == "windows" { // free up the goroutine since it'll be permanently blocked anyways return diff --git a/cli/server.go b/cli/server.go index 3726a17a1399a..9e4b1200f2b0b 100644 --- a/cli/server.go +++ b/cli/server.go @@ -169,8 +169,8 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. Short: "Start a Coder server", Options: opts, Middleware: clibase.Chain( - writeConfigMW(cfg), - printDeprecatedOptions(), + WriteConfigMW(cfg), + PrintDeprecatedOptions(), clibase.RequireNArgs(0), ), Handler: func(inv *clibase.Invocation) error { @@ -183,7 +183,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. cliui.Warnf(inv.Stderr, "YAML support is experimental and offers no compatibility guarantees.") } - go dumpHandler(ctx) + go DumpHandler(ctx) // Validate bind addresses. if cfg.Address.String() != "" { @@ -218,8 +218,8 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. filesRateLimit = -1 } - printLogo(inv) - logger, logCloser, err := buildLogger(inv, cfg) + PrintLogo(inv) + logger, logCloser, err := BuildLogger(inv, cfg) if err != nil { return xerrors.Errorf("make logger: %w", err) } @@ -436,7 +436,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. localURL = httpURL } - ctx, httpClient, err := configureHTTPClient( + ctx, httpClient, err := ConfigureHTTPClient( ctx, cfg.TLS.ClientCertFile.String(), cfg.TLS.ClientKeyFile.String(), @@ -486,7 +486,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. } // Warn the user if the access URL appears to be a loopback address. - isLocal, err := isLocalURL(ctx, cfg.AccessURL.Value()) + isLocal, err := IsLocalURL(ctx, cfg.AccessURL.Value()) if isLocal || err != nil { reason := "could not be resolved" if isLocal { @@ -826,7 +826,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. _ = pprof.Handler if cfg.Pprof.Enable { //nolint:revive - defer serveHandler(ctx, logger, nil, cfg.Pprof.Address.String(), "pprof")() + defer ServeHandler(ctx, logger, nil, cfg.Pprof.Address.String(), "pprof")() } if cfg.Prometheus.Enable { options.PrometheusRegistry.MustRegister(collectors.NewGoCollector()) @@ -845,7 +845,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. defer closeWorkspacesFunc() //nolint:revive - defer serveHandler(ctx, logger, promhttp.InstrumentMetricHandler( + defer ServeHandler(ctx, logger, promhttp.InstrumentMetricHandler( options.PrometheusRegistry, promhttp.HandlerFor(options.PrometheusRegistry, promhttp.HandlerOpts{}), ), cfg.Prometheus.Address.String(), "prometheus")() } @@ -872,7 +872,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. } client := codersdk.New(localURL) - if localURL.Scheme == "https" && isLocalhost(localURL.Hostname()) { + if localURL.Scheme == "https" && IsLocalhost(localURL.Hostname()) { // The certificate will likely be self-signed or for a different // hostname, so we need to skip verification. client.HTTPClient.Transport = &http.Transport{ @@ -1186,7 +1186,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. // printDeprecatedOptions loops through all command options, and prints // a warning for usage of deprecated options. -func printDeprecatedOptions() clibase.MiddlewareFunc { +func PrintDeprecatedOptions() clibase.MiddlewareFunc { return func(next clibase.HandlerFunc) clibase.HandlerFunc { return func(inv *clibase.Invocation) error { opts := inv.Command.Options @@ -1222,7 +1222,7 @@ func printDeprecatedOptions() clibase.MiddlewareFunc { // writeConfigMW will prevent the main command from running if the write-config // flag is set. Instead, it will marshal the command options to YAML and write // them to stdout. -func writeConfigMW(cfg *codersdk.DeploymentValues) clibase.MiddlewareFunc { +func WriteConfigMW(cfg *codersdk.DeploymentValues) clibase.MiddlewareFunc { return func(next clibase.HandlerFunc) clibase.HandlerFunc { return func(inv *clibase.Invocation) error { if !cfg.WriteConfig { @@ -1251,7 +1251,7 @@ func writeConfigMW(cfg *codersdk.DeploymentValues) clibase.MiddlewareFunc { // isLocalURL returns true if the hostname of the provided URL appears to // resolve to a loopback address. -func isLocalURL(ctx context.Context, u *url.URL) (bool, error) { +func IsLocalURL(ctx context.Context, u *url.URL) (bool, error) { resolver := &net.Resolver{} ips, err := resolver.LookupIPAddr(ctx, u.Hostname()) if err != nil { @@ -1377,7 +1377,7 @@ func newProvisionerDaemon( } // nolint: revive -func printLogo(inv *clibase.Invocation) { +func PrintLogo(inv *clibase.Invocation) { // Only print the logo in TTYs. if !isTTYOut(inv) { return @@ -1723,7 +1723,7 @@ func startBuiltinPostgres(ctx context.Context, cfg config.Root, logger slog.Logg return connectionURL, ep.Stop, nil } -func configureHTTPClient(ctx context.Context, clientCertFile, clientKeyFile string, tlsClientCAFile string) (context.Context, *http.Client, error) { +func ConfigureHTTPClient(ctx context.Context, clientCertFile, clientKeyFile string, tlsClientCAFile string) (context.Context, *http.Client, error) { if clientCertFile != "" && clientKeyFile != "" { certificates, err := loadCertificates([]string{clientCertFile}, []string{clientKeyFile}) if err != nil { @@ -1783,13 +1783,13 @@ func redirectToAccessURL(handler http.Handler, accessURL *url.URL, tunnel bool, }) } -// isLocalhost returns true if the host points to the local machine. Intended to +// IsLocalhost returns true if the host points to the local machine. Intended to // be called with `u.Hostname()`. -func isLocalhost(host string) bool { +func IsLocalhost(host string) bool { return host == "localhost" || host == "127.0.0.1" || host == "::1" } -func buildLogger(inv *clibase.Invocation, cfg *codersdk.DeploymentValues) (slog.Logger, func(), error) { +func BuildLogger(inv *clibase.Invocation, cfg *codersdk.DeploymentValues) (slog.Logger, func(), error) { var ( sinks = []slog.Sink{} closers = []func() error{} From d925672feb23bc56f77ad98032264e0335eddf41 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 13 Apr 2023 08:51:56 -0500 Subject: [PATCH 2/2] Factor out httpservers and tracer --- cli/server.go | 386 ++++++++++++++++++++++++++++---------------------- 1 file changed, 218 insertions(+), 168 deletions(-) diff --git a/cli/server.go b/cli/server.go index 9e4b1200f2b0b..faff22017f1a9 100644 --- a/cli/server.go +++ b/cli/server.go @@ -255,44 +255,12 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. // which is caught by goleaks. defer http.DefaultClient.CloseIdleConnections() - var ( - tracerProvider trace.TracerProvider - sqlDriver = "postgres" - ) - - // Coder tracing should be disabled if telemetry is disabled unless - // --telemetry-trace was explicitly provided. - shouldCoderTrace := cfg.Telemetry.Enable.Value() && !isTest() - // Only override if telemetryTraceEnable was specifically set. - // By default we want it to be controlled by telemetryEnable. - if inv.ParsedFlags().Changed("telemetry-trace") { - shouldCoderTrace = cfg.Telemetry.Trace.Value() - } - - if cfg.Trace.Enable.Value() || shouldCoderTrace || cfg.Trace.HoneycombAPIKey != "" { - sdkTracerProvider, closeTracing, err := tracing.TracerProvider(ctx, "coderd", tracing.TracerOpts{ - Default: cfg.Trace.Enable.Value(), - Coder: shouldCoderTrace, - Honeycomb: cfg.Trace.HoneycombAPIKey.String(), - }) - if err != nil { - logger.Warn(ctx, "start telemetry exporter", slog.Error(err)) - } else { - // allow time for traces to flush even if command context is canceled - defer func() { - _ = shutdownWithTimeout(closeTracing, 5*time.Second) - }() - - d, err := tracing.PostgresDriver(sdkTracerProvider, "coderd.database") - if err != nil { - logger.Warn(ctx, "start postgres tracing driver", slog.Error(err)) - } else { - sqlDriver = d - } - - tracerProvider = sdkTracerProvider - } + tracerProvider, sqlDriver := ConfigureTraceProvider(ctx, logger, inv, cfg) + httpServers, err := ConfigureHTTPServers(inv, cfg) + if err != nil { + return xerrors.Errorf("configure http(s): %w", err) } + defer httpServers.Close() config := r.createConfig() @@ -322,118 +290,10 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. }() } - var ( - httpListener net.Listener - httpURL *url.URL - ) - if cfg.HTTPAddress.String() != "" { - httpListener, err = net.Listen("tcp", cfg.HTTPAddress.String()) - if err != nil { - return err - } - defer httpListener.Close() - - listenAddrStr := httpListener.Addr().String() - // For some reason if 0.0.0.0:x is provided as the http address, - // httpListener.Addr().String() likes to return it as an ipv6 - // address (i.e. [::]:x). If the input ip is 0.0.0.0, try to - // coerce the output back to ipv4 to make it less confusing. - if strings.Contains(cfg.HTTPAddress.String(), "0.0.0.0") { - listenAddrStr = strings.ReplaceAll(listenAddrStr, "[::]", "0.0.0.0") - } - - // We want to print out the address the user supplied, not the - // loopback device. - _, _ = fmt.Fprintf(inv.Stdout, "Started HTTP listener at %s\n", (&url.URL{Scheme: "http", Host: listenAddrStr}).String()) - - // Set the http URL we want to use when connecting to ourselves. - tcpAddr, tcpAddrValid := httpListener.Addr().(*net.TCPAddr) - if !tcpAddrValid { - return xerrors.Errorf("invalid TCP address type %T", httpListener.Addr()) - } - if tcpAddr.IP.IsUnspecified() { - tcpAddr.IP = net.IPv4(127, 0, 0, 1) - } - httpURL = &url.URL{ - Scheme: "http", - Host: tcpAddr.String(), - } - } - - var ( - tlsConfig *tls.Config - httpsListener net.Listener - httpsURL *url.URL - ) - if cfg.TLS.Enable { - if cfg.TLS.Address.String() == "" { - return xerrors.New("tls address must be set if tls is enabled") - } - - // DEPRECATED: This redirect used to default to true. - // It made more sense to have the redirect be opt-in. - if inv.Environ.Get("CODER_TLS_REDIRECT_HTTP") == "true" || inv.ParsedFlags().Changed("tls-redirect-http-to-https") { - cliui.Warn(inv.Stderr, "--tls-redirect-http-to-https is deprecated, please use --redirect-to-access-url instead") - cfg.RedirectToAccessURL = cfg.TLS.RedirectHTTP - } - - tlsConfig, err = configureTLS( - cfg.TLS.MinVersion.String(), - cfg.TLS.ClientAuth.String(), - cfg.TLS.CertFiles, - cfg.TLS.KeyFiles, - cfg.TLS.ClientCAFile.String(), - ) - if err != nil { - return xerrors.Errorf("configure tls: %w", err) - } - httpsListenerInner, err := net.Listen("tcp", cfg.TLS.Address.String()) - if err != nil { - return err - } - defer httpsListenerInner.Close() - - httpsListener = tls.NewListener(httpsListenerInner, tlsConfig) - defer httpsListener.Close() - - listenAddrStr := httpsListener.Addr().String() - // For some reason if 0.0.0.0:x is provided as the https - // address, httpsListener.Addr().String() likes to return it as - // an ipv6 address (i.e. [::]:x). If the input ip is 0.0.0.0, - // try to coerce the output back to ipv4 to make it less - // confusing. - if strings.Contains(cfg.HTTPAddress.String(), "0.0.0.0") { - listenAddrStr = strings.ReplaceAll(listenAddrStr, "[::]", "0.0.0.0") - } - - // We want to print out the address the user supplied, not the - // loopback device. - _, _ = fmt.Fprintf(inv.Stdout, "Started TLS/HTTPS listener at %s\n", (&url.URL{Scheme: "https", Host: listenAddrStr}).String()) - - // Set the https URL we want to use when connecting to - // ourselves. - tcpAddr, tcpAddrValid := httpsListener.Addr().(*net.TCPAddr) - if !tcpAddrValid { - return xerrors.Errorf("invalid TCP address type %T", httpsListener.Addr()) - } - if tcpAddr.IP.IsUnspecified() { - tcpAddr.IP = net.IPv4(127, 0, 0, 1) - } - httpsURL = &url.URL{ - Scheme: "https", - Host: tcpAddr.String(), - } - } - - // Sanity check that at least one listener was started. - if httpListener == nil && httpsListener == nil { - return xerrors.New("must listen on at least one address") - } - // Prefer HTTP because it's less prone to TLS errors over localhost. - localURL := httpsURL - if httpURL != nil { - localURL = httpURL + localURL := httpServers.TLSUrl + if httpServers.HTTPUrl != nil { + localURL = httpServers.HTTPUrl } ctx, httpClient, err := ConfigureHTTPClient( @@ -607,8 +467,8 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. SSHConfigOptions: configSSHOptions, }, } - if tlsConfig != nil { - options.TLSCertificates = tlsConfig.Certificates + if httpServers.TLSConfig != nil { + options.TLSCertificates = httpServers.TLSConfig.Certificates } if cfg.StrictTransportSecurity > 0 { @@ -956,30 +816,17 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. // We call this in the routine so we can kill the other listeners if // one of them fails. closeListenersNow := func() { - if httpListener != nil { - _ = httpListener.Close() - } - if httpsListener != nil { - _ = httpsListener.Close() - } + httpServers.Close() if tunnel != nil { _ = tunnel.Listener.Close() } } eg := errgroup.Group{} - if httpListener != nil { - eg.Go(func() error { - defer closeListenersNow() - return httpServer.Serve(httpListener) - }) - } - if httpsListener != nil { - eg.Go(func() error { - defer closeListenersNow() - return httpServer.Serve(httpsListener) - }) - } + eg.Go(func() error { + defer closeListenersNow() + return httpServers.Serve(httpServer) + }) if tunnel != nil { eg.Go(func() error { defer closeListenersNow() @@ -1916,3 +1763,206 @@ func connectToPostgres(ctx context.Context, logger slog.Logger, driver string, d ok = true return sqlDB, nil } + +type HTTPServers struct { + HTTPUrl *url.URL + HTTPListener net.Listener + + // TLS + TLSUrl *url.URL + TLSListener net.Listener + TLSConfig *tls.Config +} + +// Serve acts just like http.Serve. It is a blocking call until the server +// is closed, and an error is returned if any underlying Serve call fails. +func (s *HTTPServers) Serve(srv *http.Server) error { + eg := errgroup.Group{} + if s.HTTPListener != nil { + eg.Go(func() error { + defer s.Close() // close all listeners on error + return srv.Serve(s.HTTPListener) + }) + } + if s.TLSListener != nil { + eg.Go(func() error { + defer s.Close() // close all listeners on error + return srv.Serve(s.TLSListener) + }) + } + return eg.Wait() +} + +func (s *HTTPServers) Close() { + if s.HTTPListener != nil { + _ = s.HTTPListener.Close() + } + if s.TLSListener != nil { + _ = s.TLSListener.Close() + } +} + +func ConfigureTraceProvider(ctx context.Context, logger slog.Logger, inv *clibase.Invocation, cfg *codersdk.DeploymentValues) (trace.TracerProvider, string) { + var ( + tracerProvider trace.TracerProvider + sqlDriver = "postgres" + ) + // Coder tracing should be disabled if telemetry is disabled unless + // --telemetry-trace was explicitly provided. + shouldCoderTrace := cfg.Telemetry.Enable.Value() && !isTest() + // Only override if telemetryTraceEnable was specifically set. + // By default we want it to be controlled by telemetryEnable. + if inv.ParsedFlags().Changed("telemetry-trace") { + shouldCoderTrace = cfg.Telemetry.Trace.Value() + } + + if cfg.Trace.Enable.Value() || shouldCoderTrace || cfg.Trace.HoneycombAPIKey != "" { + sdkTracerProvider, closeTracing, err := tracing.TracerProvider(ctx, "coderd", tracing.TracerOpts{ + Default: cfg.Trace.Enable.Value(), + Coder: shouldCoderTrace, + Honeycomb: cfg.Trace.HoneycombAPIKey.String(), + }) + if err != nil { + logger.Warn(ctx, "start telemetry exporter", slog.Error(err)) + } else { + // allow time for traces to flush even if command context is canceled + defer func() { + _ = shutdownWithTimeout(closeTracing, 5*time.Second) + }() + + d, err := tracing.PostgresDriver(sdkTracerProvider, "coderd.database") + if err != nil { + logger.Warn(ctx, "start postgres tracing driver", slog.Error(err)) + } else { + sqlDriver = d + } + + tracerProvider = sdkTracerProvider + } + } + return tracerProvider, sqlDriver +} + +func ConfigureHTTPServers(inv *clibase.Invocation, cfg *codersdk.DeploymentValues) (_ *HTTPServers, err error) { + httpServers := &HTTPServers{} + defer func() { + if err != nil { + // Always close the listeners if we fail. + httpServers.Close() + } + }() + // Validate bind addresses. + if cfg.Address.String() != "" { + if cfg.TLS.Enable { + cfg.HTTPAddress = "" + cfg.TLS.Address = cfg.Address + } else { + _ = cfg.HTTPAddress.Set(cfg.Address.String()) + cfg.TLS.Address.Host = "" + cfg.TLS.Address.Port = "" + } + } + if cfg.TLS.Enable && cfg.TLS.Address.String() == "" { + return nil, xerrors.Errorf("TLS address must be set if TLS is enabled") + } + if !cfg.TLS.Enable && cfg.HTTPAddress.String() == "" { + return nil, xerrors.Errorf("TLS is disabled. Enable with --tls-enable or specify a HTTP address") + } + + if cfg.AccessURL.String() != "" && + !(cfg.AccessURL.Scheme == "http" || cfg.AccessURL.Scheme == "https") { + return nil, xerrors.Errorf("access-url must include a scheme (e.g. 'http://' or 'https://)") + } + + addrString := func(l net.Listener) string { + listenAddrStr := l.Addr().String() + // For some reason if 0.0.0.0:x is provided as the https + // address, httpsListener.Addr().String() likes to return it as + // an ipv6 address (i.e. [::]:x). If the input ip is 0.0.0.0, + // try to coerce the output back to ipv4 to make it less + // confusing. + if strings.Contains(cfg.HTTPAddress.String(), "0.0.0.0") { + listenAddrStr = strings.ReplaceAll(listenAddrStr, "[::]", "0.0.0.0") + } + return listenAddrStr + } + + if cfg.HTTPAddress.String() != "" { + httpServers.HTTPListener, err = net.Listen("tcp", cfg.HTTPAddress.String()) + if err != nil { + return nil, err + } + + // We want to print out the address the user supplied, not the + // loopback device. + _, _ = fmt.Fprintf(inv.Stdout, "Started HTTP listener at %s\n", (&url.URL{Scheme: "http", Host: addrString(httpServers.HTTPListener)}).String()) + + // Set the http URL we want to use when connecting to ourselves. + tcpAddr, tcpAddrValid := httpServers.HTTPListener.Addr().(*net.TCPAddr) + if !tcpAddrValid { + return nil, xerrors.Errorf("invalid TCP address type %T", httpServers.HTTPListener.Addr()) + } + if tcpAddr.IP.IsUnspecified() { + tcpAddr.IP = net.IPv4(127, 0, 0, 1) + } + httpServers.HTTPUrl = &url.URL{ + Scheme: "http", + Host: tcpAddr.String(), + } + } + + if cfg.TLS.Enable { + if cfg.TLS.Address.String() == "" { + return nil, xerrors.New("tls address must be set if tls is enabled") + } + + // DEPRECATED: This redirect used to default to true. + // It made more sense to have the redirect be opt-in. + if inv.Environ.Get("CODER_TLS_REDIRECT_HTTP") == "true" || inv.ParsedFlags().Changed("tls-redirect-http-to-https") { + cliui.Warn(inv.Stderr, "--tls-redirect-http-to-https is deprecated, please use --redirect-to-access-url instead") + cfg.RedirectToAccessURL = cfg.TLS.RedirectHTTP + } + + tlsConfig, err := configureTLS( + cfg.TLS.MinVersion.String(), + cfg.TLS.ClientAuth.String(), + cfg.TLS.CertFiles, + cfg.TLS.KeyFiles, + cfg.TLS.ClientCAFile.String(), + ) + if err != nil { + return nil, xerrors.Errorf("configure tls: %w", err) + } + httpsListenerInner, err := net.Listen("tcp", cfg.TLS.Address.String()) + if err != nil { + return nil, err + } + + httpServers.TLSConfig = tlsConfig + httpServers.TLSListener = tls.NewListener(httpsListenerInner, tlsConfig) + + // We want to print out the address the user supplied, not the + // loopback device. + _, _ = fmt.Fprintf(inv.Stdout, "Started TLS/HTTPS listener at %s\n", (&url.URL{Scheme: "https", Host: addrString(httpServers.TLSListener)}).String()) + + // Set the https URL we want to use when connecting to + // ourselves. + tcpAddr, tcpAddrValid := httpServers.TLSListener.Addr().(*net.TCPAddr) + if !tcpAddrValid { + return nil, xerrors.Errorf("invalid TCP address type %T", httpServers.TLSListener.Addr()) + } + if tcpAddr.IP.IsUnspecified() { + tcpAddr.IP = net.IPv4(127, 0, 0, 1) + } + httpServers.TLSUrl = &url.URL{ + Scheme: "https", + Host: tcpAddr.String(), + } + } + + if httpServers.HTTPListener == nil && httpServers.TLSListener == nil { + return nil, xerrors.New("must listen on at least one address") + } + + return httpServers, nil +}