Переглянути джерело

Replaced http wrapper field in plugin Options with HTTPServer. (#229)

Replaced http wrapper field in plugin Options with HTTPServer.

HTTPServer allows somebody embedding pprof to control how the
hosting HTTP server is constructed which makes pprof work in
more environments.
Sanjay Ghemawat 7 роки тому
джерело
коміт
43bdac25ec
3 змінених файлів з 94 додано та 128 видалено
  1. 51
    51
      internal/driver/webui.go
  2. 21
    70
      internal/driver/webui_test.go
  3. 22
    7
      internal/plugin/plugin.go

+ 51
- 51
internal/driver/webui.go Переглянути файл

@@ -23,6 +23,7 @@ import (
23 23
 	gourl "net/url"
24 24
 	"os"
25 25
 	"os/exec"
26
+	"strconv"
26 27
 	"strings"
27 28
 	"time"
28 29
 
@@ -81,6 +82,18 @@ type webArgs struct {
81 82
 }
82 83
 
83 84
 func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options) error {
85
+	host, portStr, err := net.SplitHostPort(hostport)
86
+	if err != nil {
87
+		return fmt.Errorf("could not split http address: %v", err)
88
+	}
89
+	port, err := strconv.Atoi(portStr)
90
+	if err != nil {
91
+		return fmt.Errorf("invalid port number: %v", err)
92
+	}
93
+	if host == "" {
94
+		host = "localhost"
95
+	}
96
+
84 97
 	interactiveMode = true
85 98
 	ui := makeWebInterface(p, o)
86 99
 	for n, c := range pprofCommands {
@@ -93,47 +106,51 @@ func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options) e
93 106
 	ui.help["graph"] = "Display profile as a directed graph"
94 107
 	ui.help["reset"] = "Show the entire profile"
95 108
 
96
-	ln, url, isLocal, err := newListenerAndURL(hostport)
97
-	if err != nil {
98
-		return err
109
+	server := o.HTTPServer
110
+	if server == nil {
111
+		server = defaultWebServer
99 112
 	}
100
-
101
-	// authorization wrapper
102
-	wrap := o.HTTPWrapper
103
-	if wrap == nil {
104
-		if isLocal {
105
-			// Only allow requests from local host.
106
-			wrap = checkLocalHost
107
-		} else {
108
-			wrap = func(h http.Handler) http.Handler { return h }
109
-		}
113
+	args := &plugin.HTTPServerArgs{
114
+		Hostport: net.JoinHostPort(host, portStr),
115
+		Host:     host,
116
+		Port:     port,
117
+		Handlers: map[string]http.Handler{
118
+			"/":       http.HandlerFunc(ui.dot),
119
+			"/top":    http.HandlerFunc(ui.top),
120
+			"/disasm": http.HandlerFunc(ui.disasm),
121
+			"/source": http.HandlerFunc(ui.source),
122
+			"/peek":   http.HandlerFunc(ui.peek),
123
+		},
110 124
 	}
111 125
 
112
-	mux := http.NewServeMux()
113
-	mux.Handle("/", wrap(http.HandlerFunc(ui.dot)))
114
-	mux.Handle("/top", wrap(http.HandlerFunc(ui.top)))
115
-	mux.Handle("/disasm", wrap(http.HandlerFunc(ui.disasm)))
116
-	mux.Handle("/source", wrap(http.HandlerFunc(ui.source)))
117
-	mux.Handle("/peek", wrap(http.HandlerFunc(ui.peek)))
118
-
119
-	s := &http.Server{Handler: mux}
120
-	go openBrowser(url, o)
121
-	return s.Serve(ln)
126
+	go openBrowser("http://"+args.Hostport, o)
127
+	return server(args)
122 128
 }
123 129
 
124
-func newListenerAndURL(hostport string) (ln net.Listener, url string, isLocal bool, err error) {
125
-	host, _, err := net.SplitHostPort(hostport)
130
+func defaultWebServer(args *plugin.HTTPServerArgs) error {
131
+	ln, err := net.Listen("tcp", args.Hostport)
126 132
 	if err != nil {
127
-		return nil, "", false, err
128
-	}
129
-	if host == "" {
130
-		host = "localhost"
131
-	}
132
-	if ln, err = net.Listen("tcp", hostport); err != nil {
133
-		return nil, "", false, err
133
+		return err
134 134
 	}
135
-	url = fmt.Sprint("http://", net.JoinHostPort(host, fmt.Sprint(ln.Addr().(*net.TCPAddr).Port)))
136
-	return ln, url, isLocalhost(host), nil
135
+	isLocal := isLocalhost(args.Host)
136
+	handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
137
+		if isLocal {
138
+			// Only allow local clients
139
+			host, _, err := net.SplitHostPort(req.RemoteAddr)
140
+			if err != nil || !isLocalhost(host) {
141
+				http.Error(w, "permission denied", http.StatusForbidden)
142
+				return
143
+			}
144
+		}
145
+		h := args.Handlers[req.URL.Path]
146
+		if h == nil {
147
+			// Fall back to default behavior
148
+			h = http.DefaultServeMux
149
+		}
150
+		h.ServeHTTP(w, req)
151
+	})
152
+	s := &http.Server{Handler: handler}
153
+	return s.Serve(ln)
137 154
 }
138 155
 
139 156
 func isLocalhost(host string) bool {
@@ -179,17 +196,6 @@ func openBrowser(url string, o *plugin.Options) {
179 196
 	o.UI.PrintErr(u.String())
180 197
 }
181 198
 
182
-func checkLocalHost(h http.Handler) http.Handler {
183
-	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
184
-		host, _, err := net.SplitHostPort(req.RemoteAddr)
185
-		if err != nil || !isLocalhost(host) {
186
-			http.Error(w, "permission denied", http.StatusForbidden)
187
-			return
188
-		}
189
-		h.ServeHTTP(w, req)
190
-	})
191
-}
192
-
193 199
 func varsFromURL(u *gourl.URL) variables {
194 200
 	vars := pprofVariables.makeCopy()
195 201
 	vars["focus"].value = u.Query().Get("f")
@@ -241,12 +247,6 @@ func (ui *webInterface) render(w http.ResponseWriter, baseURL, tmpl string,
241 247
 
242 248
 // dot generates a web page containing an svg diagram.
243 249
 func (ui *webInterface) dot(w http.ResponseWriter, req *http.Request) {
244
-	// Disable prefix matching behavior of net/http.
245
-	if req.URL.Path != "/" {
246
-		http.NotFound(w, req)
247
-		return
248
-	}
249
-
250 250
 	rpt, errList := ui.makeReport(w, req, []string{"svg"})
251 251
 	if rpt == nil {
252 252
 		return // error already reported

+ 21
- 70
internal/driver/webui_test.go Переглянути файл

@@ -23,7 +23,6 @@ import (
23 23
 	"net/url"
24 24
 	"os/exec"
25 25
 	"regexp"
26
-	"runtime"
27 26
 	"testing"
28 27
 
29 28
 	"github.com/google/pprof/internal/plugin"
@@ -32,27 +31,28 @@ import (
32 31
 
33 32
 func TestWebInterface(t *testing.T) {
34 33
 	prof := makeFakeProfile()
35
-	ui := makeWebInterface(prof, &plugin.Options{
36
-		Obj: fakeObjTool{},
37
-		UI:  &stdUI{},
38
-	})
39 34
 
40
-	// Start test server.
41
-	server := httptest.NewServer(http.HandlerFunc(
42
-		func(w http.ResponseWriter, r *http.Request) {
43
-			switch r.URL.Path {
44
-			case "/":
45
-				ui.dot(w, r)
46
-			case "/top":
47
-				ui.top(w, r)
48
-			case "/disasm":
49
-				ui.disasm(w, r)
50
-			case "/peek":
51
-				ui.peek(w, r)
52
-			case "/source":
53
-				ui.source(w, r)
54
-			}
55
-		}))
35
+	// Custom http server creator
36
+	var server *httptest.Server
37
+	serverCreated := make(chan bool)
38
+	creator := func(a *plugin.HTTPServerArgs) error {
39
+		server = httptest.NewServer(http.HandlerFunc(
40
+			func(w http.ResponseWriter, r *http.Request) {
41
+				if h := a.Handlers[r.URL.Path]; h != nil {
42
+					h.ServeHTTP(w, r)
43
+				}
44
+			}))
45
+		serverCreated <- true
46
+		return nil
47
+	}
48
+
49
+	// Start server and wait for it to be initialized
50
+	go serveWebInterface("unused:1234", prof, &plugin.Options{
51
+		Obj:        fakeObjTool{},
52
+		UI:         &stdUI{},
53
+		HTTPServer: creator,
54
+	})
55
+	<-serverCreated
56 56
 	defer server.Close()
57 57
 
58 58
 	haveDot := false
@@ -195,55 +195,6 @@ func makeFakeProfile() *profile.Profile {
195 195
 	}
196 196
 }
197 197
 
198
-func TestNewListenerAndURL(t *testing.T) {
199
-	if runtime.GOOS == "nacl" {
200
-		t.Skip("test assumes tcp available")
201
-	}
202
-
203
-	tests := []struct {
204
-		hostport  string
205
-		wantErr   bool
206
-		wantURLRe *regexp.Regexp
207
-		wantLocal bool
208
-	}{
209
-		{
210
-			hostport:  ":",
211
-			wantURLRe: regexp.MustCompile(`http://localhost:\d+`),
212
-			wantLocal: true,
213
-		},
214
-		{
215
-			hostport:  "localhost:",
216
-			wantURLRe: regexp.MustCompile(`http://localhost:\d+`),
217
-			wantLocal: true,
218
-		},
219
-		{
220
-			hostport: "http://localhost:12345",
221
-			wantErr:  true,
222
-		},
223
-	}
224
-
225
-	for _, tt := range tests {
226
-		t.Run(tt.hostport, func(t *testing.T) {
227
-			_, url, isLocal, err := newListenerAndURL(tt.hostport)
228
-			if tt.wantErr {
229
-				if err == nil {
230
-					t.Fatalf("newListenerAndURL(%v) want error; got none", tt.hostport)
231
-				}
232
-				return
233
-			}
234
-			if err != nil {
235
-				t.Fatalf("newListenerAndURL(%v) = %v", tt.hostport, err)
236
-			}
237
-			if !tt.wantURLRe.MatchString(url) {
238
-				t.Errorf("newListenerAndURL(%v) URL = %v; want URL matching %v", tt.hostport, url, tt.wantURLRe)
239
-			}
240
-			if got, want := isLocal, tt.wantLocal; got != want {
241
-				t.Errorf("newListenerAndURL(%v) isLocal = %v; want %v", tt.hostport, got, want)
242
-			}
243
-		})
244
-	}
245
-}
246
-
247 198
 func TestIsLocalHost(t *testing.T) {
248 199
 	for _, s := range []string{"localhost:10000", "[::1]:10000", "127.0.0.1:10000"} {
249 200
 		host, _, err := net.SplitHostPort(s)

+ 22
- 7
internal/plugin/plugin.go Переглянути файл

@@ -33,14 +33,15 @@ type Options struct {
33 33
 	Obj     ObjTool
34 34
 	UI      UI
35 35
 
36
-	// HTTPWrapper takes a pprof http handler as an argument and
37
-	// returns the actual handler that should be invoked by http.
38
-	// A typical use is to add authentication before calling the
39
-	// pprof handler.
36
+	// HTTPServer is a function that should block serving http requests,
37
+	// including the handlers specfied in args.  If non-nil, pprof will
38
+	// invoke this function if necessary to provide a web interface.
40 39
 	//
41
-	// If HTTPWrapper is nil, a default wrapper will be used that
42
-	// disallows all requests except from the localhost.
43
-	HTTPWrapper func(http.Handler) http.Handler
40
+	// If HTTPServer is nil, pprof will use its own internal HTTP server.
41
+	//
42
+	// A common use for a custom HTTPServer is to provide custom
43
+	// authentication checks.
44
+	HTTPServer func(args *HTTPServerArgs) error
44 45
 }
45 46
 
46 47
 // Writer provides a mechanism to write data under a certain name,
@@ -195,3 +196,17 @@ type UI interface {
195 196
 	// the auto-completion of cmd, if the UI supports auto-completion at all.
196 197
 	SetAutoComplete(complete func(string) string)
197 198
 }
199
+
200
+// HTTPServerArgs contains arguments needed by an HTTP server that
201
+// is exporting a pprof web interface.
202
+type HTTPServerArgs struct {
203
+	// Hostport contains the http server address (derived from flags).
204
+	Hostport string
205
+
206
+	Host string // Host portion of Hostport
207
+	Port int    // Port portion of Hostport
208
+
209
+	// Handlers maps from URL paths to the handler to invoke to
210
+	// serve that path.
211
+	Handlers map[string]http.Handler
212
+}