Signed-off-by: David Anderson <danderson@tailscale.com>main
parent
b461ba9554
commit
0022c3d2e2
@ -0,0 +1,136 @@ |
||||
// Copyright (c) 2021 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 ( |
||||
"expvar" |
||||
"fmt" |
||||
"html" |
||||
"io" |
||||
"net/http" |
||||
"net/http/pprof" |
||||
"net/url" |
||||
"os" |
||||
"runtime" |
||||
|
||||
"tailscale.com/version" |
||||
) |
||||
|
||||
// DebugHandler is an http.Handler that serves a debugging "homepage",
|
||||
// and provides helpers to register more debug endpoints and reports.
|
||||
//
|
||||
// The rendered page consists of three sections: informational
|
||||
// key/value pairs, links to other pages, and additional
|
||||
// program-specific HTML. Callers can add to these sections using the
|
||||
// KV, URL and Section helpers respectively.
|
||||
//
|
||||
// Additionally, the Handle method offers a shorthand for correctly
|
||||
// registering debug handlers and cross-linking them from /debug/.
|
||||
type DebugHandler struct { |
||||
mux *http.ServeMux // where this handler is registered
|
||||
kvs []func(io.Writer) // output one <li>...</li> each, see KV()
|
||||
urls []string // one <li>...</li> block with link each
|
||||
sections []func(io.Writer, *http.Request) // invoked in registration order prior to outputting </body>
|
||||
} |
||||
|
||||
// Debugger returns the DebugHandler registered on mux at /debug/,
|
||||
// creating it if necessary.
|
||||
func Debugger(mux *http.ServeMux) *DebugHandler { |
||||
h, pat := mux.Handler(&http.Request{URL: &url.URL{Path: "/debug/"}}) |
||||
if d, ok := h.(*DebugHandler); ok && pat == "/debug/" { |
||||
return d |
||||
} |
||||
ret := &DebugHandler{ |
||||
mux: mux, |
||||
} |
||||
mux.Handle("/debug/", ret) |
||||
|
||||
ret.KVFunc("Uptime", func() interface{} { return Uptime() }) |
||||
ret.KV("Version", version.Long) |
||||
ret.Handle("vars", "Metrics (Go)", expvar.Handler()) |
||||
ret.Handle("varz", "Metrics (Prometheus)", http.HandlerFunc(VarzHandler)) |
||||
ret.Handle("pprof/", "pprof", http.HandlerFunc(pprof.Index)) |
||||
ret.URL("/debug/pprof/goroutine?debug=1", "Goroutines (collapsed)") |
||||
ret.URL("/debug/pprof/goroutine?debug=2", "Goroutines (full)") |
||||
ret.Handle("gc", "force GC", http.HandlerFunc(gcHandler)) |
||||
hostname, err := os.Hostname() |
||||
if err == nil { |
||||
ret.KV("Machine", hostname) |
||||
} |
||||
return ret |
||||
} |
||||
|
||||
// ServeHTTP implements http.Handler.
|
||||
func (d *DebugHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
||||
if !AllowDebugAccess(r) { |
||||
http.Error(w, "debug access denied", http.StatusForbidden) |
||||
return |
||||
} |
||||
if r.URL.Path != "/debug/" { |
||||
// Sub-handlers are handled by the parent mux directly.
|
||||
http.NotFound(w, r) |
||||
return |
||||
} |
||||
|
||||
f := func(format string, args ...interface{}) { fmt.Fprintf(w, format, args...) } |
||||
f("<html><body><h1>%s debug</h1><ul>", version.CmdName()) |
||||
for _, kv := range d.kvs { |
||||
kv(w) |
||||
} |
||||
for _, url := range d.urls { |
||||
io.WriteString(w, url) |
||||
} |
||||
for _, section := range d.sections { |
||||
section(w, r) |
||||
} |
||||
} |
||||
|
||||
// Handle registers handler at /debug/<slug> and creates a descriptive
|
||||
// entry in /debug/ for it.
|
||||
func (d *DebugHandler) Handle(slug, desc string, handler http.Handler) { |
||||
href := "/debug/" + slug |
||||
d.mux.Handle(href, Protected(handler)) |
||||
d.URL(href, desc) |
||||
} |
||||
|
||||
// KV adds a key/value list item to /debug/.
|
||||
func (d *DebugHandler) KV(k string, v interface{}) { |
||||
val := html.EscapeString(fmt.Sprintf("%v", v)) |
||||
d.kvs = append(d.kvs, func(w io.Writer) { |
||||
fmt.Fprintf(w, "<li><b>%s:</b> %s</li>", k, val) |
||||
}) |
||||
} |
||||
|
||||
// KVFunc adds a key/value list item to /debug/. v is called on every
|
||||
// render of /debug/.
|
||||
func (d *DebugHandler) KVFunc(k string, v func() interface{}) { |
||||
d.kvs = append(d.kvs, func(w io.Writer) { |
||||
val := html.EscapeString(fmt.Sprintf("%v", v())) |
||||
fmt.Fprintf(w, "<li><b>%s:</b> %s</li>", k, val) |
||||
}) |
||||
} |
||||
|
||||
// URL adds a URL and description list item to /debug/.
|
||||
func (d *DebugHandler) URL(url, desc string) { |
||||
if desc != "" { |
||||
desc = " (" + desc + ")" |
||||
} |
||||
d.urls = append(d.urls, fmt.Sprintf(`<li><a href="%s">%s</a>%s</li>`, url, url, html.EscapeString(desc))) |
||||
} |
||||
|
||||
// Section invokes f on every render of /debug/ to add supplemental
|
||||
// HTML to the page body.
|
||||
func (d *DebugHandler) Section(f func(w io.Writer, r *http.Request)) { |
||||
d.sections = append(d.sections, f) |
||||
} |
||||
|
||||
func gcHandler(w http.ResponseWriter, r *http.Request) { |
||||
w.Write([]byte("running GC...\n")) |
||||
if f, ok := w.(http.Flusher); ok { |
||||
f.Flush() |
||||
} |
||||
runtime.GC() |
||||
w.Write([]byte("Done.\n")) |
||||
} |
||||
@ -0,0 +1,189 @@ |
||||
// Copyright (c) 2021 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 ( |
||||
"fmt" |
||||
"io" |
||||
"net/http" |
||||
"net/http/httptest" |
||||
"strings" |
||||
"testing" |
||||
) |
||||
|
||||
func TestDebugger(t *testing.T) { |
||||
mux := http.NewServeMux() |
||||
|
||||
dbg1 := Debugger(mux) |
||||
if dbg1 == nil { |
||||
t.Fatal("didn't get a debugger from mux") |
||||
} |
||||
|
||||
dbg2 := Debugger(mux) |
||||
if dbg2 != dbg1 { |
||||
t.Fatal("Debugger returned different debuggers for the same mux") |
||||
} |
||||
} |
||||
|
||||
func get(m http.Handler, path, srcIP string) (int, string) { |
||||
req := httptest.NewRequest("GET", path, nil) |
||||
req.RemoteAddr = srcIP + ":1234" |
||||
rec := httptest.NewRecorder() |
||||
m.ServeHTTP(rec, req) |
||||
return rec.Result().StatusCode, rec.Body.String() |
||||
} |
||||
|
||||
const ( |
||||
tsIP = "100.100.100.100" |
||||
pubIP = "8.8.8.8" |
||||
) |
||||
|
||||
func TestDebuggerKV(t *testing.T) { |
||||
mux := http.NewServeMux() |
||||
dbg := Debugger(mux) |
||||
dbg.KV("Donuts", 42) |
||||
dbg.KV("Secret code", "hunter2") |
||||
val := "red" |
||||
dbg.KVFunc("Condition", func() interface{} { return val }) |
||||
|
||||
code, _ := get(mux, "/debug/", pubIP) |
||||
if code != 403 { |
||||
t.Fatalf("debug access wasn't denied, got %v", code) |
||||
} |
||||
|
||||
code, body := get(mux, "/debug/", tsIP) |
||||
if code != 200 { |
||||
t.Fatalf("debug access failed, got %v", code) |
||||
} |
||||
for _, want := range []string{"Donuts", "42", "Secret code", "hunter2", "Condition", "red"} { |
||||
if !strings.Contains(body, want) { |
||||
t.Errorf("want %q in output, not found", want) |
||||
} |
||||
} |
||||
|
||||
val = "green" |
||||
code, body = get(mux, "/debug/", tsIP) |
||||
if code != 200 { |
||||
t.Fatalf("debug access failed, got %v", code) |
||||
} |
||||
for _, want := range []string{"Condition", "green"} { |
||||
if !strings.Contains(body, want) { |
||||
t.Errorf("want %q in output, not found", want) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestDebuggerURL(t *testing.T) { |
||||
mux := http.NewServeMux() |
||||
dbg := Debugger(mux) |
||||
dbg.URL("https://www.tailscale.com", "Homepage") |
||||
|
||||
code, body := get(mux, "/debug/", tsIP) |
||||
if code != 200 { |
||||
t.Fatalf("debug access failed, got %v", code) |
||||
} |
||||
for _, want := range []string{"https://www.tailscale.com", "Homepage"} { |
||||
if !strings.Contains(body, want) { |
||||
t.Errorf("want %q in output, not found", want) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestDebuggerSection(t *testing.T) { |
||||
mux := http.NewServeMux() |
||||
dbg := Debugger(mux) |
||||
dbg.Section(func(w io.Writer, r *http.Request) { |
||||
fmt.Fprintf(w, "Test output %v", r.RemoteAddr) |
||||
}) |
||||
|
||||
code, body := get(mux, "/debug/", tsIP) |
||||
if code != 200 { |
||||
t.Fatalf("debug access failed, got %v", code) |
||||
} |
||||
want := `Test output 100.100.100.100:1234` |
||||
if !strings.Contains(body, want) { |
||||
t.Errorf("want %q in output, not found", want) |
||||
} |
||||
} |
||||
|
||||
func TestDebuggerHandle(t *testing.T) { |
||||
mux := http.NewServeMux() |
||||
dbg := Debugger(mux) |
||||
dbg.Handle("check", "Consistency check", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
||||
fmt.Fprintf(w, "Test output %v", r.RemoteAddr) |
||||
})) |
||||
|
||||
code, body := get(mux, "/debug/", tsIP) |
||||
if code != 200 { |
||||
t.Fatalf("debug access failed, got %v", code) |
||||
} |
||||
for _, want := range []string{"/debug/check", "Consistency check"} { |
||||
if !strings.Contains(body, want) { |
||||
t.Errorf("want %q in output, not found", want) |
||||
} |
||||
} |
||||
|
||||
code, _ = get(mux, "/debug/check", pubIP) |
||||
if code != 403 { |
||||
t.Fatal("/debug/check should be protected, but isn't") |
||||
} |
||||
|
||||
code, body = get(mux, "/debug/check", tsIP) |
||||
if code != 200 { |
||||
t.Fatal("/debug/check denied debug access") |
||||
} |
||||
want := "Test output " + tsIP |
||||
if !strings.Contains(body, want) { |
||||
t.Errorf("want %q in output, not found", want) |
||||
} |
||||
} |
||||
|
||||
func ExampleDebugHandler_Handle() { |
||||
mux := http.NewServeMux() |
||||
dbg := Debugger(mux) |
||||
// Registers /debug/flushcache with the given handler, and adds a
|
||||
// link to /debug/ with the description "Flush caches".
|
||||
dbg.Handle("flushcache", "Flush caches", http.HandlerFunc(http.NotFound)) |
||||
} |
||||
|
||||
func ExampleDebugHandler_KV() { |
||||
mux := http.NewServeMux() |
||||
dbg := Debugger(mux) |
||||
// Adds two list items to /debug/, showing that the condition is
|
||||
// red and there are 42 donuts.
|
||||
dbg.KV("Conditon", "red") |
||||
dbg.KV("Donuts", 42) |
||||
} |
||||
|
||||
func ExampleDebugHandler_KVFunc() { |
||||
mux := http.NewServeMux() |
||||
dbg := Debugger(mux) |
||||
// Adds an count of page renders to /debug/. Note this example
|
||||
// isn't concurrency-safe.
|
||||
views := 0 |
||||
dbg.KVFunc("Debug pageviews", func() interface{} { |
||||
views = views + 1 |
||||
return views |
||||
}) |
||||
dbg.KV("Donuts", 42) |
||||
} |
||||
|
||||
func ExampleDebugHandler_URL() { |
||||
mux := http.NewServeMux() |
||||
dbg := Debugger(mux) |
||||
// Links to the Tailscale website from /debug/.
|
||||
dbg.URL("https://www.tailscale.com", "Homepage") |
||||
} |
||||
|
||||
func ExampleDebugHandler_Section() { |
||||
mux := http.NewServeMux() |
||||
dbg := Debugger(mux) |
||||
// Adds a section to /debug/ that dumps the HTTP request of the
|
||||
// visitor.
|
||||
dbg.Section(func(w io.Writer, r *http.Request) { |
||||
io.WriteString(w, "<h3>Dump of your HTTP request</h3>") |
||||
fmt.Fprintf(w, "<code>%#v</code>", r) |
||||
}) |
||||
} |
||||
Loading…
Reference in new issue