From 98eceae55e2ac29cca4b29179feba7745dede808 Mon Sep 17 00:00:00 2001 From: David Anderson Date: Tue, 17 Mar 2020 20:35:59 -0700 Subject: [PATCH] tsweb: add a Handler type. Handler is like http.Handler, but returns errors. ErrHandler converts back to an http.Handler, with added error handling and logging. Signed-off-by: David Anderson --- tsweb/log.go | 4 +++- tsweb/tsweb.go | 63 +++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/tsweb/log.go b/tsweb/log.go index 43494b45b..619537118 100644 --- a/tsweb/log.go +++ b/tsweb/log.go @@ -43,7 +43,9 @@ func (m Msg) String() string { return strings.TrimRight(buf.String(), "\n") } -// HTTPError is an error with embedded HTTP response information. +// HTTPError is an error with embedded HTTP response information. When +// received by an ErrHandler, the Code and Msg are sent to the client, +// while Err is logged on the server. type HTTPError struct { Code int Msg string // sent to the end-user diff --git a/tsweb/tsweb.go b/tsweb/tsweb.go index 5c7a06b23..e7409f34f 100644 --- a/tsweb/tsweb.go +++ b/tsweb/tsweb.go @@ -2,11 +2,12 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Package tsweb contains code between various Tailscale webservers. +// Package tsweb contains code used in various Tailscale webservers. package tsweb import ( "bytes" + "context" "expvar" "fmt" "io" @@ -22,6 +23,7 @@ import ( "tailscale.com/metrics" "tailscale.com/net/interfaces" + "tailscale.com/types/logger" ) // DevMode controls whether extra output in shown, for when the binary is being run in dev mode. @@ -30,12 +32,12 @@ var DevMode bool // NewMux returns a new ServeMux with debugHandler registered (and protected) at /debug/. func NewMux(debugHandler http.Handler) *http.ServeMux { mux := http.NewServeMux() - RegisterCommonDebug(mux) + registerCommonDebug(mux) mux.Handle("/debug/", Protected(debugHandler)) return mux } -func RegisterCommonDebug(mux *http.ServeMux) { +func registerCommonDebug(mux *http.ServeMux) { expvar.Publish("counter_uptime_sec", expvar.Func(func() interface{} { return int64(Uptime().Seconds()) })) mux.Handle("/debug/pprof/", Protected(http.DefaultServeMux)) // to net/http/pprof mux.Handle("/debug/vars", Protected(http.DefaultServeMux)) // to expvar @@ -137,6 +139,61 @@ func stripPort(hostport string) string { return net.JoinHostPort(host, "443") } +// Handler is like net/http.Handler, but the handler can return an +// error. +type Handler interface { + ServeHTTP(http.ResponseWriter, *http.Request) error +} + +// handler is an http.Handler that wraps a Handler and handles errors. +type handler struct { + h Handler + logf logger.Logf +} + +func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + msg := Msg{ + Where: "http", + When: time.Now(), + HTTP: &MsgHTTP{ + Path: r.URL.Path, + RemoteAddr: r.RemoteAddr, + UserAgent: r.UserAgent(), + Referer: r.Referer(), + }, + } + err := h.h.ServeHTTP(w, r) + if err == context.Canceled { + // No need to inform client, but still log the + // cancellation. + msg.HTTP.Code = 499 // nginx convention: Client Closed Request + msg.Err = err + } else if hErr, ok := err.(HTTPError); ok { + msg.HTTP.Code = hErr.Code + msg.Err = hErr.Err + msg.Msg = hErr.Msg + http.Error(w, hErr.Msg, hErr.Code) + } else if err != nil { + msg.HTTP.Code = 500 + msg.Err = err + msg.Msg = "internal server error" + http.Error(w, msg.Msg, msg.HTTP.Code) + } + + // TODO(danderson): needed? Copied from existing code, but + // doesn't HTTPServer do this by itself? + if f, _ := w.(http.Flusher); f != nil { + f.Flush() + } + h.logf("%s", msg) +} + +// ErrHandler returns an http.Handler that logs requests and handles +// errors from the inner Handler. +func ErrHandler(h Handler, logf logger.Logf) http.Handler { + return handler{h, logf} +} + // varzHandler is an HTTP handler to write expvar values into the // prometheus export format: //