diff --git a/commands.go b/commands.go index f312896..c6c0d28 100644 --- a/commands.go +++ b/commands.go @@ -119,6 +119,12 @@ var COMMANDS map[string]func(string, *App) CommandFunc = map[string]func(string, return nil } }, + "printTraceBuffer": func(_ string, a *App) CommandFunc { + return func(g *gocui.Gui, _ *gocui.View) error { + dumpTraceClient(g, a) + return nil + } + }, } func scrollView(v *gocui.View, dy int) error { diff --git a/config/config.go b/config/config.go index f0dec95..faa9085 100644 --- a/config/config.go +++ b/config/config.go @@ -62,6 +62,7 @@ var DefaultKeys = map[string]map[string]string{ "CtrlO": "openEditor", "CtrlT": "toggleContextSpecificSearch", "CtrlX": "clearHistory", + "CtrlB": "printTraceBuffer", "Tab": "nextView", "CtrlJ": "nextView", "CtrlK": "prevView", diff --git a/trace.go b/trace.go new file mode 100644 index 0000000..8ca053e --- /dev/null +++ b/trace.go @@ -0,0 +1,211 @@ +package main + +import ( + "container/ring" + "crypto/tls" + "fmt" + "net/http/httptrace" + "net/textproto" + "strings" + "sync" + "time" + + "github.com/jroimartin/gocui" +) + +const ( + ViewWidth = 80 + ViewHeight = 25 + BufferLength = 20 + TimestampLayout = "15:04:05.000000" +) + +type ClientTrace struct { + sync.Mutex + data *ring.Ring + recChannel chan string + writeChannel chan string +} + +var ( + clientTrace *ClientTrace +) + +func NewClientTrace() *ClientTrace { + result := new(ClientTrace) + result.data = ring.New(BufferLength) + result.recChannel = make(chan string, 0) + result.writeChannel = make(chan string, 1) + + go result.writer() + go result.receiver() + + return result +} + +func (c *ClientTrace) Write(format string, args ...interface{}) { + message := fmt.Sprintf(format, args...) + lineLen := ViewWidth - (len(TimestampLayout) + 3) + if len(message) > lineLen { + sb := strings.Builder{} + prefix := string(make([]rune, len(TimestampLayout)+3)) + + for i, c := range []rune(message) { + if i > 0 && i%lineLen == 0 && i < len(message) { + sb.WriteString(prefix) + } + sb.WriteRune(c) + } + + message = sb.String() + } + message = fmt.Sprintf("[%s] %s", time.Now().Format(TimestampLayout), message) + c.writeChannel <- fmt.Sprintf(message) +} + +func (c *ClientTrace) Dump() string { + c.Lock() + defer c.Unlock() + + sb := strings.Builder{} + c.data.Do(func(v interface{}) { + if v != nil { + s := fmt.Sprintf("%v\n", v) + sb.WriteString(s) + } + }) + return sb.String() +} + +func (c *ClientTrace) receiver() { + for { + select { + case message, more := <-c.recChannel: + c.writeChannel <- message + if !more { + break + } + } + } +} + +func (c *ClientTrace) writer() { + f := func(message string) { + c.Lock() + defer c.Unlock() + c.data.Value = message + c.data = c.data.Next() + } + for { + select { + case message, more := <-c.writeChannel: + f(message) + if !more { + break + } + } + } +} + +func getClientTrace() *httptrace.ClientTrace { + if clientTrace == nil { + clientTrace = NewClientTrace() + } + + return &httptrace.ClientTrace{ + GetConn: func(hostPort string) { + clientTrace.Write("GetConn(%v)", hostPort) + }, + GotConn: func(info httptrace.GotConnInfo) { + clientTrace.Write("GotConn(%v -> %v)", info.Conn.LocalAddr(), info.Conn.RemoteAddr()) + }, + GotFirstResponseByte: func() { + clientTrace.Write("GotFirstResponseByte()") + }, + Got100Continue: func() { + clientTrace.Write("Got100Continue()") + }, + Got1xxResponse: func(code int, header textproto.MIMEHeader) error { + clientTrace.Write("Got1xxResponse(%v, %v)", code, header) + return nil + }, + DNSStart: func(info httptrace.DNSStartInfo) { + clientTrace.Write("DNSStart(%v)", info.Host) + }, + DNSDone: func(info httptrace.DNSDoneInfo) { + if info.Err != nil { + clientTrace.Write("DNSDone(): ", info.Err) + } else { + clientTrace.Write("DNSDone(%v)", info.Addrs) + } + }, + ConnectStart: func(network, addr string) { + clientTrace.Write("ConnectStart(%v)", addr) + }, + ConnectDone: func(network, addr string, err error) { + if err != nil { + clientTrace.Write("ConnectDone(): %v", err) + } else { + clientTrace.Write("ConnectDone(%v)", addr) + } + }, + TLSHandshakeDone: func(state tls.ConnectionState, err error) { + if err != nil { + clientTrace.Write("TLSHandshakeDone(): %v", err) + } else { + sb := strings.Builder{} + sb.WriteString(func(version uint16) string { + for k, v := range TLS_VERSIONS { + if v == state.Version { + return k + } + } + return "Unknown" + }(state.Version)) + sb.WriteRune(' ') + sb.WriteString(state.ServerName) + sb.WriteString(" <=> ") + sb.WriteString(state.PeerCertificates[0].Subject.CommonName) + clientTrace.Write("TLSHandshakeDone(%v)", sb.String()) + } + }, + WroteHeaderField: func(key string, value []string) { + clientTrace.Write("WroteHeaderField(%v, %v)", key, value) + }, + Wait100Continue: func() { + clientTrace.Write("Wait100Continue()") + }, + WroteRequest: func(info httptrace.WroteRequestInfo) { + if info.Err != nil { + clientTrace.Write("WroteRequest(): %v", info) + } else { + clientTrace.Write("WroteRequest()") + } + }, + } +} + +func dumpTraceClient(g *gocui.Gui, a *App) { + if a.currentPopup == TRACE_VIEW { + a.closePopup(g, TRACE_VIEW) + return + } + trace, err := a.CreatePopupView(TRACE_VIEW, ViewWidth, ViewHeight, g) + if err != nil { + return + } + trace.Title = VIEW_TITLES[TRACE_VIEW] + trace.Highlight = false + trace.Wrap = true + if clientTrace != nil { + go func() { + for a.currentPopup == TRACE_VIEW { + trace.Clear() + _, err = fmt.Fprintf(trace, clientTrace.Dump()) + time.Sleep(100 * time.Millisecond) + } + }() + } + _, _ = g.SetViewOnTop(TRACE_VIEW) + _, _ = g.SetCurrentView(TRACE_VIEW) +} diff --git a/wuzz.go b/wuzz.go index 258bcb7..c0d9140 100644 --- a/wuzz.go +++ b/wuzz.go @@ -12,6 +12,7 @@ import ( "log" "mime/multipart" "net/http" + "net/http/httptrace" "net/url" "os" "path" @@ -65,6 +66,7 @@ const ( SAVE_RESULT_VIEW = "save-result" METHOD_LIST_VIEW = "method-list" HELP_VIEW = "help" + TRACE_VIEW = "trace" ) var VIEW_TITLES = map[string]string{ @@ -78,6 +80,7 @@ var VIEW_TITLES = map[string]string{ SAVE_RESULT_VIEW: "Save Result (press enter to close)", METHOD_LIST_VIEW: "Methods", HELP_VIEW: "Help", + TRACE_VIEW: "Trace", } type position struct { @@ -312,6 +315,7 @@ var TLS_VERSIONS = map[string]uint16{ "TLS1.0": tls.VersionTLS10, "TLS1.1": tls.VersionTLS11, "TLS1.2": tls.VersionTLS12, + "TLS1.3": tls.VersionTLS13, } var defaultEditor ViewEditor @@ -859,6 +863,9 @@ func (a *App) SubmitRequest(g *gocui.Gui, _ *gocui.View) error { req.Host = headers.Get("Host") } + // trace http call + req = req.WithContext(httptrace.WithClientTrace(req.Context(), getClientTrace())) + // do request start := time.Now() response, err := CLIENT.Do(req)