github.com/ck00004/CobaltStrikeParser-Go@v1.0.14/lib/http/pprof/pprof.go (about)

     1  // Copyright 2010 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package pprof serves via its HTTP server runtime profiling data
     6  // in the format expected by the pprof visualization tool.
     7  //
     8  // The package is typically only imported for the side effect of
     9  // registering its HTTP handlers.
    10  // The handled paths all begin with /debug/pprof/.
    11  //
    12  // To use pprof, link this package into your program:
    13  //	import _ "net/http/pprof"
    14  //
    15  // If your application is not already running an http server, you
    16  // need to start one. Add "github.com/ck00004/CobaltStrikeParser-Go/lib/http" and "log" to your imports and
    17  // the following code to your main function:
    18  //
    19  // 	go func() {
    20  // 		log.Println(http.ListenAndServe("localhost:6060", nil))
    21  // 	}()
    22  //
    23  // If you are not using DefaultServeMux, you will have to register handlers
    24  // with the mux you are using.
    25  //
    26  // Then use the pprof tool to look at the heap profile:
    27  //
    28  //	go tool pprof http://localhost:6060/debug/pprof/heap
    29  //
    30  // Or to look at a 30-second CPU profile:
    31  //
    32  //	go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
    33  //
    34  // Or to look at the goroutine blocking profile, after calling
    35  // runtime.SetBlockProfileRate in your program:
    36  //
    37  //	go tool pprof http://localhost:6060/debug/pprof/block
    38  //
    39  // Or to look at the holders of contended mutexes, after calling
    40  // runtime.SetMutexProfileFraction in your program:
    41  //
    42  //	go tool pprof http://localhost:6060/debug/pprof/mutex
    43  //
    44  // The package also exports a handler that serves execution trace data
    45  // for the "go tool trace" command. To collect a 5-second execution trace:
    46  //
    47  //	wget -O trace.out http://localhost:6060/debug/pprof/trace?seconds=5
    48  //	go tool trace trace.out
    49  //
    50  // To view all available profiles, open http://localhost:6060/debug/pprof/
    51  // in your browser.
    52  //
    53  // For a study of the facility in action, visit
    54  //
    55  //	https://blog.golang.org/2011/06/profiling-go-programs.html
    56  //
    57  package pprof
    58  
    59  import (
    60  	"bufio"
    61  	"bytes"
    62  	"context"
    63  	"fmt"
    64  	"html"
    65  	"io"
    66  	"log"
    67  	"os"
    68  	"runtime"
    69  	"runtime/pprof"
    70  	"runtime/trace"
    71  	"sort"
    72  	"strconv"
    73  	"strings"
    74  	"time"
    75  
    76  	"github.com/ck00004/CobaltStrikeParser-Go/lib/url"
    77  
    78  	"github.com/ck00004/CobaltStrikeParser-Go/lib/internal/profile"
    79  
    80  	"github.com/ck00004/CobaltStrikeParser-Go/lib/http"
    81  )
    82  
    83  func init() {
    84  	http.HandleFunc("/debug/pprof/", Index)
    85  	http.HandleFunc("/debug/pprof/cmdline", Cmdline)
    86  	http.HandleFunc("/debug/pprof/profile", Profile)
    87  	http.HandleFunc("/debug/pprof/symbol", Symbol)
    88  	http.HandleFunc("/debug/pprof/trace", Trace)
    89  }
    90  
    91  // Cmdline responds with the running program's
    92  // command line, with arguments separated by NUL bytes.
    93  // The package initialization registers it as /debug/pprof/cmdline.
    94  func Cmdline(w http.ResponseWriter, r *http.Request) {
    95  	w.Header().Set("X-Content-Type-Options", "nosniff")
    96  	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
    97  	fmt.Fprint(w, strings.Join(os.Args, "\x00"))
    98  }
    99  
   100  func sleep(r *http.Request, d time.Duration) {
   101  	select {
   102  	case <-time.After(d):
   103  	case <-r.Context().Done():
   104  	}
   105  }
   106  
   107  func durationExceedsWriteTimeout(r *http.Request, seconds float64) bool {
   108  	srv, ok := r.Context().Value(http.ServerContextKey).(*http.Server)
   109  	return ok && srv.WriteTimeout != 0 && seconds >= srv.WriteTimeout.Seconds()
   110  }
   111  
   112  func serveError(w http.ResponseWriter, status int, txt string) {
   113  	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
   114  	w.Header().Set("X-Go-Pprof", "1")
   115  	w.Header().Del("Content-Disposition")
   116  	w.WriteHeader(status)
   117  	fmt.Fprintln(w, txt)
   118  }
   119  
   120  // Profile responds with the pprof-formatted cpu profile.
   121  // Profiling lasts for duration specified in seconds GET parameter, or for 30 seconds if not specified.
   122  // The package initialization registers it as /debug/pprof/profile.
   123  func Profile(w http.ResponseWriter, r *http.Request) {
   124  	w.Header().Set("X-Content-Type-Options", "nosniff")
   125  	sec, err := strconv.ParseInt(r.FormValue("seconds"), 10, 64)
   126  	if sec <= 0 || err != nil {
   127  		sec = 30
   128  	}
   129  
   130  	if durationExceedsWriteTimeout(r, float64(sec)) {
   131  		serveError(w, http.StatusBadRequest, "profile duration exceeds server's WriteTimeout")
   132  		return
   133  	}
   134  
   135  	// Set Content Type assuming StartCPUProfile will work,
   136  	// because if it does it starts writing.
   137  	w.Header().Set("Content-Type", "application/octet-stream")
   138  	w.Header().Set("Content-Disposition", `attachment; filename="profile"`)
   139  	if err := pprof.StartCPUProfile(w); err != nil {
   140  		// StartCPUProfile failed, so no writes yet.
   141  		serveError(w, http.StatusInternalServerError,
   142  			fmt.Sprintf("Could not enable CPU profiling: %s", err))
   143  		return
   144  	}
   145  	sleep(r, time.Duration(sec)*time.Second)
   146  	pprof.StopCPUProfile()
   147  }
   148  
   149  // Trace responds with the execution trace in binary form.
   150  // Tracing lasts for duration specified in seconds GET parameter, or for 1 second if not specified.
   151  // The package initialization registers it as /debug/pprof/trace.
   152  func Trace(w http.ResponseWriter, r *http.Request) {
   153  	w.Header().Set("X-Content-Type-Options", "nosniff")
   154  	sec, err := strconv.ParseFloat(r.FormValue("seconds"), 64)
   155  	if sec <= 0 || err != nil {
   156  		sec = 1
   157  	}
   158  
   159  	if durationExceedsWriteTimeout(r, sec) {
   160  		serveError(w, http.StatusBadRequest, "profile duration exceeds server's WriteTimeout")
   161  		return
   162  	}
   163  
   164  	// Set Content Type assuming trace.Start will work,
   165  	// because if it does it starts writing.
   166  	w.Header().Set("Content-Type", "application/octet-stream")
   167  	w.Header().Set("Content-Disposition", `attachment; filename="trace"`)
   168  	if err := trace.Start(w); err != nil {
   169  		// trace.Start failed, so no writes yet.
   170  		serveError(w, http.StatusInternalServerError,
   171  			fmt.Sprintf("Could not enable tracing: %s", err))
   172  		return
   173  	}
   174  	sleep(r, time.Duration(sec*float64(time.Second)))
   175  	trace.Stop()
   176  }
   177  
   178  // Symbol looks up the program counters listed in the request,
   179  // responding with a table mapping program counters to function names.
   180  // The package initialization registers it as /debug/pprof/symbol.
   181  func Symbol(w http.ResponseWriter, r *http.Request) {
   182  	w.Header().Set("X-Content-Type-Options", "nosniff")
   183  	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
   184  
   185  	// We have to read the whole POST body before
   186  	// writing any output. Buffer the output here.
   187  	var buf bytes.Buffer
   188  
   189  	// We don't know how many symbols we have, but we
   190  	// do have symbol information. Pprof only cares whether
   191  	// this number is 0 (no symbols available) or > 0.
   192  	fmt.Fprintf(&buf, "num_symbols: 1\n")
   193  
   194  	var b *bufio.Reader
   195  	if r.Method == "POST" {
   196  		b = bufio.NewReader(r.Body)
   197  	} else {
   198  		b = bufio.NewReader(strings.NewReader(r.URL.RawQuery))
   199  	}
   200  
   201  	for {
   202  		word, err := b.ReadSlice('+')
   203  		if err == nil {
   204  			word = word[0 : len(word)-1] // trim +
   205  		}
   206  		pc, _ := strconv.ParseUint(string(word), 0, 64)
   207  		if pc != 0 {
   208  			f := runtime.FuncForPC(uintptr(pc))
   209  			if f != nil {
   210  				fmt.Fprintf(&buf, "%#x %s\n", pc, f.Name())
   211  			}
   212  		}
   213  
   214  		// Wait until here to check for err; the last
   215  		// symbol will have an err because it doesn't end in +.
   216  		if err != nil {
   217  			if err != io.EOF {
   218  				fmt.Fprintf(&buf, "reading request: %v\n", err)
   219  			}
   220  			break
   221  		}
   222  	}
   223  
   224  	w.Write(buf.Bytes())
   225  }
   226  
   227  // Handler returns an HTTP handler that serves the named profile.
   228  func Handler(name string) http.Handler {
   229  	return handler(name)
   230  }
   231  
   232  type handler string
   233  
   234  func (name handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   235  	w.Header().Set("X-Content-Type-Options", "nosniff")
   236  	p := pprof.Lookup(string(name))
   237  	if p == nil {
   238  		serveError(w, http.StatusNotFound, "Unknown profile")
   239  		return
   240  	}
   241  	if sec := r.FormValue("seconds"); sec != "" {
   242  		name.serveDeltaProfile(w, r, p, sec)
   243  		return
   244  	}
   245  	gc, _ := strconv.Atoi(r.FormValue("gc"))
   246  	if name == "heap" && gc > 0 {
   247  		runtime.GC()
   248  	}
   249  	debug, _ := strconv.Atoi(r.FormValue("debug"))
   250  	if debug != 0 {
   251  		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
   252  	} else {
   253  		w.Header().Set("Content-Type", "application/octet-stream")
   254  		w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name))
   255  	}
   256  	p.WriteTo(w, debug)
   257  }
   258  
   259  func (name handler) serveDeltaProfile(w http.ResponseWriter, r *http.Request, p *pprof.Profile, secStr string) {
   260  	sec, err := strconv.ParseInt(secStr, 10, 64)
   261  	if err != nil || sec <= 0 {
   262  		serveError(w, http.StatusBadRequest, `invalid value for "seconds" - must be a positive integer`)
   263  		return
   264  	}
   265  	if !profileSupportsDelta[name] {
   266  		serveError(w, http.StatusBadRequest, `"seconds" parameter is not supported for this profile type`)
   267  		return
   268  	}
   269  	// 'name' should be a key in profileSupportsDelta.
   270  	if durationExceedsWriteTimeout(r, float64(sec)) {
   271  		serveError(w, http.StatusBadRequest, "profile duration exceeds server's WriteTimeout")
   272  		return
   273  	}
   274  	debug, _ := strconv.Atoi(r.FormValue("debug"))
   275  	if debug != 0 {
   276  		serveError(w, http.StatusBadRequest, "seconds and debug params are incompatible")
   277  		return
   278  	}
   279  	p0, err := collectProfile(p)
   280  	if err != nil {
   281  		serveError(w, http.StatusInternalServerError, "failed to collect profile")
   282  		return
   283  	}
   284  
   285  	t := time.NewTimer(time.Duration(sec) * time.Second)
   286  	defer t.Stop()
   287  
   288  	select {
   289  	case <-r.Context().Done():
   290  		err := r.Context().Err()
   291  		if err == context.DeadlineExceeded {
   292  			serveError(w, http.StatusRequestTimeout, err.Error())
   293  		} else { // TODO: what's a good status code for canceled requests? 400?
   294  			serveError(w, http.StatusInternalServerError, err.Error())
   295  		}
   296  		return
   297  	case <-t.C:
   298  	}
   299  
   300  	p1, err := collectProfile(p)
   301  	if err != nil {
   302  		serveError(w, http.StatusInternalServerError, "failed to collect profile")
   303  		return
   304  	}
   305  	ts := p1.TimeNanos
   306  	dur := p1.TimeNanos - p0.TimeNanos
   307  
   308  	p0.Scale(-1)
   309  
   310  	p1, err = profile.Merge([]*profile.Profile{p0, p1})
   311  	if err != nil {
   312  		serveError(w, http.StatusInternalServerError, "failed to compute delta")
   313  		return
   314  	}
   315  
   316  	p1.TimeNanos = ts // set since we don't know what profile.Merge set for TimeNanos.
   317  	p1.DurationNanos = dur
   318  
   319  	w.Header().Set("Content-Type", "application/octet-stream")
   320  	w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s-delta"`, name))
   321  	p1.Write(w)
   322  }
   323  
   324  func collectProfile(p *pprof.Profile) (*profile.Profile, error) {
   325  	var buf bytes.Buffer
   326  	if err := p.WriteTo(&buf, 0); err != nil {
   327  		return nil, err
   328  	}
   329  	ts := time.Now().UnixNano()
   330  	p0, err := profile.Parse(&buf)
   331  	if err != nil {
   332  		return nil, err
   333  	}
   334  	p0.TimeNanos = ts
   335  	return p0, nil
   336  }
   337  
   338  var profileSupportsDelta = map[handler]bool{
   339  	"allocs":       true,
   340  	"block":        true,
   341  	"goroutine":    true,
   342  	"heap":         true,
   343  	"mutex":        true,
   344  	"threadcreate": true,
   345  }
   346  
   347  var profileDescriptions = map[string]string{
   348  	"allocs":       "A sampling of all past memory allocations",
   349  	"block":        "Stack traces that led to blocking on synchronization primitives",
   350  	"cmdline":      "The command line invocation of the current program",
   351  	"goroutine":    "Stack traces of all current goroutines",
   352  	"heap":         "A sampling of memory allocations of live objects. You can specify the gc GET parameter to run GC before taking the heap sample.",
   353  	"mutex":        "Stack traces of holders of contended mutexes",
   354  	"profile":      "CPU profile. You can specify the duration in the seconds GET parameter. After you get the profile file, use the go tool pprof command to investigate the profile.",
   355  	"threadcreate": "Stack traces that led to the creation of new OS threads",
   356  	"trace":        "A trace of execution of the current program. You can specify the duration in the seconds GET parameter. After you get the trace file, use the go tool trace command to investigate the trace.",
   357  }
   358  
   359  type profileEntry struct {
   360  	Name  string
   361  	Href  string
   362  	Desc  string
   363  	Count int
   364  }
   365  
   366  // Index responds with the pprof-formatted profile named by the request.
   367  // For example, "/debug/pprof/heap" serves the "heap" profile.
   368  // Index responds to a request for "/debug/pprof/" with an HTML page
   369  // listing the available profiles.
   370  func Index(w http.ResponseWriter, r *http.Request) {
   371  	if strings.HasPrefix(r.URL.Path, "/debug/pprof/") {
   372  		name := strings.TrimPrefix(r.URL.Path, "/debug/pprof/")
   373  		if name != "" {
   374  			handler(name).ServeHTTP(w, r)
   375  			return
   376  		}
   377  	}
   378  
   379  	w.Header().Set("X-Content-Type-Options", "nosniff")
   380  	w.Header().Set("Content-Type", "text/html; charset=utf-8")
   381  
   382  	var profiles []profileEntry
   383  	for _, p := range pprof.Profiles() {
   384  		profiles = append(profiles, profileEntry{
   385  			Name:  p.Name(),
   386  			Href:  p.Name(),
   387  			Desc:  profileDescriptions[p.Name()],
   388  			Count: p.Count(),
   389  		})
   390  	}
   391  
   392  	// Adding other profiles exposed from within this package
   393  	for _, p := range []string{"cmdline", "profile", "trace"} {
   394  		profiles = append(profiles, profileEntry{
   395  			Name: p,
   396  			Href: p,
   397  			Desc: profileDescriptions[p],
   398  		})
   399  	}
   400  
   401  	sort.Slice(profiles, func(i, j int) bool {
   402  		return profiles[i].Name < profiles[j].Name
   403  	})
   404  
   405  	if err := indexTmplExecute(w, profiles); err != nil {
   406  		log.Print(err)
   407  	}
   408  }
   409  
   410  func indexTmplExecute(w io.Writer, profiles []profileEntry) error {
   411  	var b bytes.Buffer
   412  	b.WriteString(`<html>
   413  <head>
   414  <title>/debug/pprof/</title>
   415  <style>
   416  .profile-name{
   417  	display:inline-block;
   418  	width:6rem;
   419  }
   420  </style>
   421  </head>
   422  <body>
   423  /debug/pprof/<br>
   424  <br>
   425  Types of profiles available:
   426  <table>
   427  <thead><td>Count</td><td>Profile</td></thead>
   428  `)
   429  
   430  	for _, profile := range profiles {
   431  		link := &url.URL{Path: profile.Href, RawQuery: "debug=1"}
   432  		fmt.Fprintf(&b, "<tr><td>%d</td><td><a href='%s'>%s</a></td></tr>\n", profile.Count, link, html.EscapeString(profile.Name))
   433  	}
   434  
   435  	b.WriteString(`</table>
   436  <a href="goroutine?debug=2">full goroutine stack dump</a>
   437  <br>
   438  <p>
   439  Profile Descriptions:
   440  <ul>
   441  `)
   442  	for _, profile := range profiles {
   443  		fmt.Fprintf(&b, "<li><div class=profile-name>%s: </div> %s</li>\n", html.EscapeString(profile.Name), html.EscapeString(profile.Desc))
   444  	}
   445  	b.WriteString(`</ul>
   446  </p>
   447  </body>
   448  </html>`)
   449  
   450  	_, err := w.Write(b.Bytes())
   451  	return err
   452  }