// 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
}
