mirror of
https://github.com/tailscale/tailscale.git
synced 2024-12-12 11:14:40 +00:00
tsweb: add a Server, which serves HTTP/HTTPS with various good defaults.
Updates #3797 Signed-off-by: David Anderson <danderson@tailscale.com>
This commit is contained in:
parent
fa612c28cf
commit
754a6d0514
@ -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/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
|
||||||
tailscale.com/wgengine/wglog from tailscale.com/wgengine
|
tailscale.com/wgengine/wglog from tailscale.com/wgengine
|
||||||
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
|
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/blake2b from golang.org/x/crypto/nacl/box
|
||||||
golang.org/x/crypto/blake2s from golang.zx2c4.com/wireguard/device
|
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
|
L golang.org/x/crypto/blowfish from golang.org/x/crypto/ssh/internal/bcrypt_pbkdf
|
||||||
|
385
tsweb/server.go
Normal file
385
tsweb/server.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -280,6 +280,7 @@ func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
// response code that gets sent, if any.
|
// response code that gets sent, if any.
|
||||||
type loggingResponseWriter struct {
|
type loggingResponseWriter struct {
|
||||||
http.ResponseWriter
|
http.ResponseWriter
|
||||||
|
ctx context.Context
|
||||||
code int
|
code int
|
||||||
bytes int
|
bytes int
|
||||||
hijacked bool
|
hijacked bool
|
||||||
@ -330,6 +331,20 @@ func (l loggingResponseWriter) Flush() {
|
|||||||
f.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.
|
// HTTPError is an error with embedded HTTP response information.
|
||||||
//
|
//
|
||||||
// It is the error type to be (optionally) used by Handler.ServeHTTPReturn.
|
// It is the error type to be (optionally) used by Handler.ServeHTTPReturn.
|
||||||
|
Loading…
Reference in New Issue
Block a user