summaryrefslogtreecommitdiffhomepage
path: root/tsweb/debug.go
blob: 4c0fabaff4aeae9aa96bbd927e20cc1fb6fe3e88 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package tsweb

import (
	"expvar"
	"fmt"
	"html"
	"io"
	"net/http"
	"net/url"
	"os"
	"runtime"

	"tailscale.com/feature"
	"tailscale.com/tsweb/varz"
	"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>
	title    string                           // title displayed on index page
}

// PrometheusHandler is an optional hook to enable native Prometheus
// support in the debug handler. It is disabled by default. Import the
// tailscale.com/tsweb/promvarz package to enable this feature.
var PrometheusHandler feature.Hook[func(*DebugHandler)]

// 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,
		title: fmt.Sprintf("%s debug", version.CmdName()),
	}
	mux.Handle("/debug/", ret)

	ret.KVFunc("Uptime", func() any { return varz.Uptime() })
	ret.KV("Version", version.Long())
	ret.Handle("vars", "Metrics (Go)", expvar.Handler())
	if PrometheusHandler.IsSet() {
		PrometheusHandler.Get()(ret)
	} else {
		ret.Handle("varz", "Metrics (Prometheus)", http.HandlerFunc(varz.Handler))
	}

	addProfilingHandlers(ret)
	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
	}

	AddBrowserHeaders(w)
	f := func(format string, args ...any) { fmt.Fprintf(w, format, args...) }
	f("<html><body><h1>%s</h1><ul>", html.EscapeString(d.title))
	for _, kv := range d.kvs {
		kv(w)
	}
	for _, url := range d.urls {
		io.WriteString(w, url)
	}
	for _, section := range d.sections {
		section(w, r)
	}
}

func (d *DebugHandler) handle(slug string, handler http.Handler) string {
	href := "/debug/" + slug
	d.mux.Handle(href, Protected(debugBrowserHeaderHandler(handler)))
	return href
}

// Handle registers handler at /debug/<slug> and adds a link to it
// on /debug/ with the provided description.
func (d *DebugHandler) Handle(slug, desc string, handler http.Handler) {
	href := d.handle(slug, handler)
	d.URL(href, desc)
}

// Handle registers handler at /debug/<slug> and adds a link to it
// on /debug/ with the provided description.
func (d *DebugHandler) HandleFunc(slug, desc string, handler http.HandlerFunc) {
	d.Handle(slug, desc, handler)
}

// HandleSilent registers handler at /debug/<slug>. It does not add
// a descriptive entry in /debug/ for it. This should be used
// sparingly, for things that need to be registered but would pollute
// the list of debug links.
func (d *DebugHandler) HandleSilent(slug string, handler http.Handler) {
	d.handle(slug, handler)
}

// HandleSilent registers handler at /debug/<slug>. It does not add
// a descriptive entry in /debug/ for it. This should be used
// sparingly, for things that need to be registered but would pollute
// the list of debug links.
func (d *DebugHandler) HandleSilentFunc(slug string, handler http.HandlerFunc) {
	d.HandleSilent(slug, handler)
}

// KV adds a key/value list item to /debug/.
func (d *DebugHandler) KV(k string, v any) {
	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() any) {
	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)
}

// Title sets the title at the top of the debug page.
func (d *DebugHandler) Title(title string) {
	d.title = title
}

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"))
}

// debugBrowserHeaderHandler is a wrapper around BrowserHeaderHandler with a
// more relaxed Content-Security-Policy that's acceptable for internal debug
// pages. It should not be used on any public-facing handlers!
func debugBrowserHeaderHandler(h http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		AddBrowserHeaders(w)
		// The only difference from AddBrowserHeaders is that this policy
		// allows inline CSS styles. They make debug pages much easier to
		// prototype, while the risk of user-injected CSS is relatively low.
		w.Header().Set("Content-Security-Policy", "default-src 'self'; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; block-all-mixed-content; object-src 'none'; style-src 'self' 'unsafe-inline'")
		h.ServeHTTP(w, r)
	})
}