blob: 284ad30021c7f34ba09642174fa2941eb3d4716c [file] [log] [blame] [edit]
// Copyright 2020 Marc-Antoine Ruel. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
// panicweb implements a simulation of a web server that panics.
//
// It starts a web server, a few handlers and a few hanging clients, then
// panics.
//
// It loads both panicparse's http handler and pprof's one for comparison.
//
// It is separate from the panic tool because importing "net/http" creates a
// background thread, which breaks the "asleep" panic case in tool panic.
package main
import (
"flag"
"fmt"
"log"
"net"
"net/http"
/* #nosec G108 */
_ "net/http/pprof"
"os"
"runtime"
"strings"
"sync"
"time"
"github.com/maruel/panicparse/v2/cmd/panicweb/internal"
"github.com/maruel/panicparse/v2/stack/webstack"
"github.com/mattn/go-colorable"
)
var rootPage = []byte(`<!DOCTYPE html>
<ul>
<li><a href="/panicparse">/panicparse</a></li>
<li><a href="/debug/pprof/goroutine?debug=2">/debug/pprof/goroutine?debug=2</a></li>
<li><a href="/url1">/url1</a></li>
<li><a href="/url2">/url2</a></li>
</ul>
`)
func main() {
allowremote := flag.Bool("allowremote", false, "allows access from non-localhost; implies -wait")
sleep := flag.Bool("wait", false, "sleep instead of crashing")
port := flag.Int("port", 0, "specify a port number, defaults to a ephemeral port; implies -wait")
limit := flag.Bool("limit", false, "throttle, port limit")
flag.Parse()
if *port != 0 || *allowremote {
*sleep = true
}
addr := fmt.Sprintf(":%d", *port)
if !*allowremote {
addr = "localhost" + addr
}
ln, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("Failed to listen on localhost: %v", err)
}
http.HandleFunc("/url1", internal.URL1Handler)
http.HandleFunc("/url2", internal.URL2Handler)
if *limit {
// This is similar to ExampleSnapshotHandler_complex in stack/webstack,
// albeit form values are not altered.
const delay = time.Second
mu := sync.Mutex{}
var last time.Time
http.HandleFunc("/panicparse", func(w http.ResponseWriter, req *http.Request) {
// Only allow requests from localhost or in the 100.64.x.x/10 IPv4 range
// (e.g. Tailscale).
ok := false
if i := strings.LastIndexByte(req.RemoteAddr, ':'); i != -1 {
switch ip := req.RemoteAddr[:i]; ip {
case "localhost", "127.0.0.1", "[::1]", "::1":
ok = true
default:
p := net.ParseIP(ip).To4()
ok = p != nil && p[0] == 100 && p[1] >= 64 && p[1] < 128
}
}
log.Printf("- %s: %t", req.RemoteAddr, ok)
if !ok {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
// Serialize the handler.
mu.Lock()
defer mu.Unlock()
// Throttle requests.
if time.Since(last) < delay {
http.Error(w, "retry later", http.StatusTooManyRequests)
return
}
webstack.SnapshotHandler(w, req)
last = time.Now()
})
} else {
http.HandleFunc("/panicparse", webstack.SnapshotHandler)
}
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write(rootPage)
})
srv := &http.Server{
Handler: http.DefaultServeMux,
ReadHeaderTimeout: 2 * time.Second,
}
go srv.Serve(ln)
// Start many clients.
a := ln.Addr()
url := fmt.Sprintf("http://%s/", a)
if *allowremote {
if h, err := os.Hostname(); err == nil {
if t, ok := a.(*net.TCPAddr); ok {
url = fmt.Sprintf("http://%s:%d/", h, t.Port)
}
}
}
for i := 0; i < 10; i++ {
internal.GetAsync(url + "url1")
}
for i := 0; i < 3; i++ {
internal.GetAsync(url + "url2")
}
// Try to get something hung in package golang.org/x/unix.
wait := make(chan struct{})
go func() {
wait <- struct{}{}
sysHang()
}()
<-wait
// It's convoluted but colorable is the only go module used by panicparse
// that is both versioned and can be hacked to call back user code.
w := writeHang{hung: make(chan struct{}), unblock: make(chan struct{})}
v := colorable.NewNonColorable(&w)
go v.Write([]byte("foo bar"))
<-w.hung
if *sleep {
fmt.Printf("Compare:\n- %spanicparse\n- %sdebug/pprof/goroutine?debug=2\n", url, url)
<-make(chan struct{})
} else {
panic("Here's a snapshot of a normal web server.")
}
}
type writeHang struct {
hung chan struct{}
unblock chan struct{}
}
func (w *writeHang) Write(b []byte) (int, error) {
runtime.LockOSThread()
w.hung <- struct{}{}
<-w.unblock
return 0, nil
}