From 754a6d05142ce71f128107f952327f8110ff6322 Mon Sep 17 00:00:00 2001 From: David Anderson Date: Tue, 1 Feb 2022 20:09:12 -0800 Subject: [PATCH] tsweb: add a Server, which serves HTTP/HTTPS with various good defaults. Updates #3797 Signed-off-by: David Anderson --- cmd/tailscaled/depaware.txt | 3 +- tsweb/server.go | 385 ++++++++++++++++++++++++++++++++++++ tsweb/tsweb.go | 15 ++ 3 files changed, 402 insertions(+), 1 deletion(-) create mode 100644 tsweb/server.go diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 1c8ecc4ed..19d4e4b17 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -265,7 +265,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal tailscale.com/wgengine/wglog from tailscale.com/wgengine W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router - golang.org/x/crypto/acme from tailscale.com/ipn/localapi + golang.org/x/crypto/acme from tailscale.com/ipn/localapi+ + golang.org/x/crypto/acme/autocert from tailscale.com/tsweb golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box golang.org/x/crypto/blake2s from golang.zx2c4.com/wireguard/device L golang.org/x/crypto/blowfish from golang.org/x/crypto/ssh/internal/bcrypt_pbkdf diff --git a/tsweb/server.go b/tsweb/server.go new file mode 100644 index 000000000..16fa9aafc --- /dev/null +++ b/tsweb/server.go @@ -0,0 +1,385 @@ +// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tsweb + +import ( + "context" + "crypto/tls" + "errors" + "expvar" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "golang.org/x/crypto/acme/autocert" + "tailscale.com/metrics" + "tailscale.com/types/logger" +) + +const defaultTimeout = 10 * time.Second + +// ServerConfig is the initial configuration of a Server, for +// consumption by NewServer. +type ServerConfig struct { + // Name is a human-readable name for the HTTP server. + // + // It must be valid as both a directory name and the user portion + // of an email address, so it's best to stick to a single + // alphanumeric word, such as "derper" or "control". + // + // The name is for internal use only, for things like naming cache + // directories, identifying the source of autocert emails, + // human-readable debug pages, ... + Name string + // Addr specifies the TCP address for the server to listen on, in + // the form "host:port". If blank, ":http" is used. + // + // If Addr specifies port 443, TLS serving is automatically + // configured and a second listener is set up on port 80 to handle + // HTTP to HTTPS redirection. All other ports result in HTTP-only + // serving. + Addr string + // Handler is the HTTP.Handler to invoke to handle + // requests. Cannot be nil. + Handler http.Handler + // AllowedHostnames is a function that reports whether autocert is + // allowed to obtain TLS certificates for a given client request. + // + // If the list of valid hostnames is static, you probably want to + // use autocert.HostWhitelist(...). + // + // If nil, autocert will not allow any TLS requests, and you + // should override Server.HTTPS.TLSConfig.GetCertificate to handle + // TLS certificates yourself. + AllowedHostnames autocert.HostPolicy + + // HSTSNoSubdomains is whether to tell browsers to only apply + // strict HTTPS serving to the current domain being requested, + // rather than the request domain and all its subdomains (the + // default). + HSTSNoSubdomains bool + + // RequestLog, if non-nil, receives one AccessLogRecord for every + // completed HTTP request (unless the request handler invoked + // SuppressLogging). + RequestLog func(*AccessLogRecord) + + // ForceTLS is whether to configure Server for TLS serving only, + // regardless of Addr's port number. This disables autocert (since + // autocert can't function on non-443 ports) and HTTP serving, and + // requires you to adjust Server.HTTPS.TLSConfig yourself. + ForceTLS bool + + // TODO: some kind of dev mode? Alter how request logging and + // error handling is done to be more human-friendly? +} + +// Server is an HTTP+HTTPS server. Callers should get a Server by +// calling NewServer, then applying any desired customizations to the +// prefilled fields prior to calling ListenAndServe. +type Server struct { + // Handler is the root handler for the Server. + Handler http.Handler + + // HTTPS is the HTTPS serving component of the server. + // When non-nil, it defaults to getting TLS certs from + // CertManager, and enforces TLS 1.2 as the minimum protocol + // version. + HTTPS *http.Server + // CertManager, if non-nil, manages TLS certificates for HTTPS. + CertManager *autocert.Manager + + // HTTP is the HTTP serving component of the server. If HTTPS + // serving is disabled, this is the main HTTP server. Otherwise, + // it redirects everything to HTTPS, except for debug handlers + // which are served directly to authorized clients. + HTTP *http.Server + + // Debug is the handler for /debug/ on HTTP and HTTPS. + Debug *DebugHandler + + // AlwaysHeaders are HTTP headers that are set on all requests + // prior to invoking Handler. + AlwaysHeaders http.Header + // TLSHeaders are HTTP headers that are set on all TLS requests + // prior to invoking Handler. + TLSHeaders http.Header + + // RequestLog is where HTTP request logs are written. If nil, + // request logging is disabled. + RequestLog func(*AccessLogRecord) + + now func() time.Time // normally time.Now, modified for tests + + vars metrics.Set + httpRequests expvar.Int // counter, completed requests + httpActive expvar.Int // gauge, currently alive requests + tlsRequests metrics.LabelMap // counter, completed requests + tlsActive metrics.LabelMap // gauge, currently alive requests + statusCode metrics.LabelMap // status code of completed requests + statusFamily metrics.LabelMap // like statusCode, but bucketed by 1st digit of status code +} + +// NewServer returns a Server, initialized by cfg with good defaults +// for serving. +func NewServer(cfg ServerConfig) *Server { + s := &Server{ + Handler: cfg.Handler, + Debug: Debugger(http.NewServeMux()), + AlwaysHeaders: http.Header{}, + TLSHeaders: http.Header{}, + + tlsRequests: metrics.LabelMap{Label: "version"}, + tlsActive: metrics.LabelMap{Label: "version"}, + statusCode: metrics.LabelMap{Label: "code"}, + statusFamily: metrics.LabelMap{Label: "code_family"}, + } + + s.vars.Set("http_requests", &s.httpRequests) + s.vars.Set("gauge_http_active", &s.httpActive) + s.vars.Set("tls_request_version", &s.tlsRequests) + s.vars.Set("gauge_tls_active_version", &s.tlsActive) + s.vars.Set("http_status", &s.statusCode) + s.vars.Set("http_status_family", &s.statusFamily) + + switch { + case cfg.ForceTLS: + s.HTTPS = &http.Server{ + Addr: cfg.Addr, + Handler: s, + ReadTimeout: defaultTimeout, + WriteTimeout: defaultTimeout, + IdleTimeout: defaultTimeout, + TLSConfig: &tls.Config{ + NextProtos: []string{ + "h2", "http/1.1", // enable HTTP/2 + }, + MinVersion: tls.VersionTLS12, + }, + } + // Deliberately don't forcibly enable HSTS here, this is a + // configuration where the caller has requested very manual + // TLS management, we shouldn't impose HSTS. + case IsProd443(cfg.Addr): + s.CertManager = &autocert.Manager{ + Prompt: autocert.AcceptTOS, + Cache: autocert.DirCache(DefaultCertDir(cfg.Name)), + Email: fmt.Sprintf("infra+autocert-%s@tailscale.com", cfg.Name), + HostPolicy: cfg.AllowedHostnames, + } + if s.CertManager.HostPolicy == nil { + s.CertManager.HostPolicy = func(context.Context, string) error { + return errors.New("no TLS serving allowed") + } + } + s.HTTPS = &http.Server{ + Addr: cfg.Addr, + Handler: s, + ReadTimeout: defaultTimeout, + WriteTimeout: defaultTimeout, + IdleTimeout: defaultTimeout, + TLSConfig: s.CertManager.TLSConfig(), + } + s.HTTPS.TLSConfig.MinVersion = tls.VersionTLS12 + s.HTTP = &http.Server{ + Addr: ":80", + Handler: s.CertManager.HTTPHandler(Port80Handler{s}), + ReadTimeout: defaultTimeout, + WriteTimeout: defaultTimeout, + IdleTimeout: defaultTimeout, + } + hstsVal := "max-age=63072000" + if !cfg.HSTSNoSubdomains { + hstsVal += "; includeSubDomains" + } + s.TLSHeaders.Set("Strict-Transport-Security", hstsVal) + default: + s.HTTP = &http.Server{ + Addr: cfg.Addr, + Handler: s, + ReadTimeout: defaultTimeout, + WriteTimeout: defaultTimeout, + IdleTimeout: defaultTimeout, + } + } + s.AlwaysHeaders.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; form-action 'none'; base-uri 'self'; block-all-mixed-content; plugin-types 'none'") + s.AlwaysHeaders.Set("X-Frame-Options", "DENY") + s.AlwaysHeaders.Set("X-Content-Type-Options", "nosniff") + + return s +} + +// ListenAndServe listens on the TCP network addresses s.HTTP.Addr and +// s.HTTPS.Addr (if any) and then calls Serve or ServeTLS to handle +// incoming requests. +// +// If s.HTTP.Addr is blank, ":http" is used. +// If s.HTTPS.Addr is blank, ":https" is used. +func (s *Server) ListenAndServe() error { + errCh := make(chan error, 2) + + if s.HTTP != nil { + go func() { errCh <- s.HTTP.ListenAndServe() }() + } + if s.HTTPS != nil { + go func() { errCh <- s.HTTPS.ListenAndServeTLS("", "") }() + } + + err := <-errCh + if err == http.ErrServerClosed { + return nil + } + return err +} + +// close immediately closes all listeners and connections, except +// hijacked Conns. Returns any error returned from closing underlying +// listeners. +func (s *Server) Close() error { + var err error + if s.HTTP != nil { + err = s.HTTP.Close() + } + if s.HTTPS != nil { + err2 := s.HTTPS.Close() + if err == nil { + err = err2 + } + } + return err +} + +// ServeHTTP wraps s.Handler and adds the following features: +// - Initializes default values for response headers from +// s.AlwaysHeaders and s.TLSHeaders (if the request is over TLS). +// - Sends requests for /debug/* to s.Debug. +// - Maintains HTTP and TLS expvar metrics. +// - If s.RequestLog is non-nil, writes out JSON AccessLogRecord +// structs when requests complete. +// - Injects tailscale helpers into the request context, so that +// helpers like Err and SuppressLogging work. +func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + defer s.httpRequests.Add(1) + s.httpActive.Add(1) + defer s.httpActive.Add(-1) + if r.TLS != nil { + label := "unknown" + switch r.TLS.Version { + case tls.VersionTLS10: + label = "1.0" + case tls.VersionTLS11: + label = "1.1" + case tls.VersionTLS12: + label = "1.2" + case tls.VersionTLS13: + label = "1.3" + } + defer s.tlsRequests.Add(label, 1) + s.tlsActive.Add(label, 1) + defer s.tlsActive.Add(label, -1) + } + + for k, v := range s.AlwaysHeaders { + w.Header()[k] = v + } + if r.TLS != nil { + for k, v := range s.TLSHeaders { + w.Header()[k] = v + } + } + + // always throw in a loggingResponseWriter, even when not writing + // access logs, so that we can record the response code and push + // it to metrics. + lw := &loggingResponseWriter{ResponseWriter: w, logf: logger.Discard} + + path := r.RequestURI + switch { + case path == "/debug" || strings.HasPrefix(path, "/debug/"): + s.Debug.ServeHTTP(lw, r) + case s.RequestLog != nil: + s.serveAndLog(lw, r) + default: + s.Handler.ServeHTTP(lw, r) + } + + s.statusCode.Add(strconv.Itoa(lw.httpCode()), 1) + key := fmt.Sprintf("%dxx", lw.httpCode()/100) + s.statusFamily.Add(key, 1) +} + +// serveAndLog invokes s.Handler and writes an access log entry to +// s.RequestLog, which must be non-nil. +// +// s.Handler can provide a detailed error value, which is only logged +// and not sent to the client, using the Err helper. +// +// s.Handler can suppress the writing of an access log entry by using +// the SuppressLogging helper, for example to not log successful +// requests on very high-volume API endpoints. +func (s *Server) serveAndLog(lw *loggingResponseWriter, r *http.Request) { + msg := &AccessLogRecord{ + When: s.now(), + RemoteAddr: r.RemoteAddr, + Proto: r.Proto, + TLS: r.TLS != nil, + Host: r.Host, + Method: r.Method, + RequestURI: r.URL.RequestURI(), + UserAgent: r.UserAgent(), + Referer: r.Referer(), + } + + var ( + detailedErr error + suppressLogging bool + ) + ctx := context.WithValue(r.Context(), ctxRecordError, &detailedErr) + ctx = context.WithValue(ctx, ctxSuppressLogging, &suppressLogging) + r = r.WithContext(ctx) + s.Handler.ServeHTTP(lw, r) + + if suppressLogging { + return + } + + msg.Seconds = s.now().Sub(msg.When).Seconds() + msg.Bytes = lw.bytes + msg.Code = lw.httpCode() + if detailedErr != nil { + msg.Err = detailedErr.Error() + } + + s.RequestLog(msg) +} + +var ( + // ctxRecordError is a context.WithValue key that stores a pointer + // to an error, which http.Handlers can use to record a detailed + // error that will be attached to the request's log entry. + ctxRecordError = struct{}{} + // ctxSuppressLogging is a context.WithValue key that stores a + // pointer to a bool, which http.Handlers can set to true if they + // want to suppress the writing of the current request's log + // entry. + ctxSuppressLogging = struct{}{} +) + +// Err records err as a detailed internal error for r, for possible +// logging. +func Err(r *http.Request, err error) { + if perr, ok := r.Context().Value(ctxRecordError).(*error); ok { + *perr = err + } +} + +// SuppressLogging requests that no access log entry be written for r. +func SuppressLogging(r *http.Request) { + if pb, ok := r.Context().Value(ctxSuppressLogging).(*bool); ok { + *pb = true + } +} diff --git a/tsweb/tsweb.go b/tsweb/tsweb.go index 09658ca41..f97ac9d3a 100644 --- a/tsweb/tsweb.go +++ b/tsweb/tsweb.go @@ -280,6 +280,7 @@ func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // response code that gets sent, if any. type loggingResponseWriter struct { http.ResponseWriter + ctx context.Context code int bytes int hijacked bool @@ -330,6 +331,20 @@ func (l loggingResponseWriter) Flush() { f.Flush() } +func (l *loggingResponseWriter) httpCode() int { + switch { + case l.code != 0: + return l.code + case l.hijacked: + return http.StatusSwitchingProtocols + case l.ctx.Err() == context.Canceled: + return 499 // nginx convention: Client Closed Request + default: + // Handler didn't write a body or send a header, that means 200. + return 200 + } +} + // HTTPError is an error with embedded HTTP response information. // // It is the error type to be (optionally) used by Handler.ServeHTTPReturn.