Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JSON output support #131

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 136 additions & 81 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package main

import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"encoding/pem"
"flag"
"fmt"
Expand All @@ -16,6 +18,7 @@ import (
"net/url"
"os"
"path"
"reflect"
"runtime"
"sort"
"strconv"
Expand All @@ -27,25 +30,47 @@ import (
"github.com/fatih/color"
)

type Report struct {
Address string
Header http.Header
Proto string
Status string
Timing Timing
}

type Timing struct {
DNS int
TCP int
TLS int
Server int
Transfer int

Lookup int
Connect int
PreTransfer int
StartTransfer int
Total int
}

const (
HTTPSTemplate = `` +
` DNS Lookup TCP Connection TLS Handshake Server Processing Content Transfer` + "\n" +
`[%s | %s | %s | %s | %s ]` + "\n" +
`[ %>DNS | %>TCP | %>TLS | %>Server | %>Transfer ]` + "\n" +
` | | | | |` + "\n" +
` namelookup:%s | | | |` + "\n" +
` connect:%s | | |` + "\n" +
` pretransfer:%s | |` + "\n" +
` starttransfer:%s |` + "\n" +
` total:%s` + "\n"
` namelookup:%<Lookup | | | |` + "\n" +
` connect:%<Connect | | |` + "\n" +
` pretransfer:%<PreTransfer | |` + "\n" +
` starttransfer:%<StartTransfer |` + "\n" +
` total:%<Total` + "\n"

HTTPTemplate = `` +
` DNS Lookup TCP Connection Server Processing Content Transfer` + "\n" +
`[ %s | %s | %s | %s ]` + "\n" +
` | | | |` + "\n" +
` namelookup:%s | | |` + "\n" +
` connect:%s | |` + "\n" +
` starttransfer:%s |` + "\n" +
` total:%s` + "\n"
` DNS Lookup TCP Connection Server Processing Content Transfer` + "\n" +
`[ %>DNS | %>TCP | %>Server | %>Transfer ]` + "\n" +
` | | | |` + "\n" +
` namelookup:%<Lookup | | |` + "\n" +
` connect:%<Connect | |` + "\n" +
` starttransfer:%<StartTransfer |` + "\n" +
` total:%<Total` + "\n"
)

var (
Expand All @@ -62,6 +87,7 @@ var (
clientCertFile string
fourOnly bool
sixOnly bool
jsonOutput bool

// number of redirects followed
redirectsFollowed int
Expand All @@ -84,6 +110,7 @@ func init() {
flag.StringVar(&clientCertFile, "E", "", "client cert file for tls config")
flag.BoolVar(&fourOnly, "4", false, "resolve IPv4 addresses only")
flag.BoolVar(&sixOnly, "6", false, "resolve IPv6 addresses only")
flag.BoolVar(&jsonOutput, "J", false, "use JSON to output results")

flag.Usage = usage
}
Expand Down Expand Up @@ -221,29 +248,47 @@ func dialContext(network string) func(ctx context.Context, network, addr string)
func visit(url *url.URL) {
req := newRequest(httpMethod, url, postBody)

var t0, t1, t2, t3, t4, t5, t6 time.Time
var tStart, tDNSStart, tConnectStart, tTLSStart, tConnected, tTTFB time.Time
var report Report

trace := &httptrace.ClientTrace{
DNSStart: func(_ httptrace.DNSStartInfo) { t0 = time.Now() },
DNSDone: func(_ httptrace.DNSDoneInfo) { t1 = time.Now() },
GetConn: func(_ string) { tStart = time.Now() },
DNSStart: func(_ httptrace.DNSStartInfo) { tDNSStart = time.Now() },
DNSDone: func(_ httptrace.DNSDoneInfo) {
report.Timing.DNS = msSince(tDNSStart)
report.Timing.Lookup = msSince(tStart)
},
ConnectStart: func(_, _ string) {
if t1.IsZero() {
if tConnectStart.IsZero() {
// connecting to IP
t1 = time.Now()
tConnectStart = time.Now()
}
},
ConnectDone: func(net, addr string, err error) {
if err != nil {
log.Fatalf("unable to connect to host %v: %v", addr, err)
}
t2 = time.Now()
report.Timing.TCP = msSince(tConnectStart)
report.Timing.Connect = msSince(tStart)

printf("\n%s%s\n", color.GreenString("Connected to "), color.CyanString(addr))
report.Address = addr
if !jsonOutput {
printf("\n%s%s\n", color.GreenString("Connected to "), color.CyanString(addr))
}
},
TLSHandshakeStart: func() { tTLSStart = time.Now() },
TLSHandshakeDone: func(_ tls.ConnectionState, _ error) {
report.Timing.TLS = msSince(tTLSStart)
},
GotConn: func(_ httptrace.GotConnInfo) {
tConnected = time.Now()
report.Timing.PreTransfer = msSince(tStart)
},
GotFirstResponseByte: func() {
tTTFB = time.Now()
report.Timing.Server = msSince(tConnected)
report.Timing.StartTransfer = msSince(tStart)
},
GotConn: func(_ httptrace.GotConnInfo) { t3 = time.Now() },
GotFirstResponseByte: func() { t4 = time.Now() },
TLSHandshakeStart: func() { t5 = time.Now() },
TLSHandshakeDone: func(_ tls.ConnectionState, _ error) { t6 = time.Now() },
}
req = req.WithContext(httptrace.WithClientTrace(context.Background(), trace))

Expand Down Expand Up @@ -300,69 +345,45 @@ func visit(url *url.URL) {
bodyMsg := readResponseBody(req, resp)
resp.Body.Close()

t7 := time.Now() // after read body
if t0.IsZero() {
// we skipped DNS
t0 = t1
}
// after read body
report.Timing.Transfer = msSince(tTTFB)
report.Timing.Total = msSince(tStart)

// print status line and headers
printf("\n%s%s%s\n", color.GreenString("HTTP"), grayscale(14)("/"), color.CyanString("%d.%d %s", resp.ProtoMajor, resp.ProtoMinor, resp.Status))
report.Proto = resp.Proto
report.Status = resp.Status
report.Header = resp.Header

names := make([]string, 0, len(resp.Header))
for k := range resp.Header {
names = append(names, k)
}
sort.Sort(headers(names))
for _, k := range names {
printf("%s %s\n", grayscale(14)(k+":"), color.CyanString(strings.Join(resp.Header[k], ",")))
}

if bodyMsg != "" {
printf("\n%s\n", bodyMsg)
}

fmta := func(d time.Duration) string {
return color.CyanString("%7dms", int(d/time.Millisecond))
}
// print status line and headers
if jsonOutput {
b, err := json.Marshal(report)
if err != nil {
log.Fatalf("unable to marshal json report: %v", err)
}
fmt.Printf("%s\n", b)
} else {
printf("\n%s%s%s\n", color.GreenString("HTTP"), grayscale(14)("/"), color.CyanString("%d.%d %s", resp.ProtoMajor, resp.ProtoMinor, resp.Status))

fmtb := func(d time.Duration) string {
return color.CyanString("%-9s", strconv.Itoa(int(d/time.Millisecond))+"ms")
}
names := make([]string, 0, len(resp.Header))
for k := range resp.Header {
names = append(names, k)
}
sort.Sort(headers(names))
for _, k := range names {
printf("%s %s\n", grayscale(14)(k+":"), color.CyanString(strings.Join(resp.Header[k], ",")))
}

colorize := func(s string) string {
v := strings.Split(s, "\n")
v[0] = grayscale(16)(v[0])
return strings.Join(v, "\n")
}
if bodyMsg != "" {
printf("\n%s\n", bodyMsg)
}

fmt.Println()
fmt.Println()

switch url.Scheme {
case "https":
printf(colorize(HTTPSTemplate),
fmta(t1.Sub(t0)), // dns lookup
fmta(t2.Sub(t1)), // tcp connection
fmta(t6.Sub(t5)), // tls handshake
fmta(t4.Sub(t3)), // server processing
fmta(t7.Sub(t4)), // content transfer
fmtb(t1.Sub(t0)), // namelookup
fmtb(t2.Sub(t0)), // connect
fmtb(t3.Sub(t0)), // pretransfer
fmtb(t4.Sub(t0)), // starttransfer
fmtb(t7.Sub(t0)), // total
)
case "http":
printf(colorize(HTTPTemplate),
fmta(t1.Sub(t0)), // dns lookup
fmta(t3.Sub(t1)), // tcp connection
fmta(t4.Sub(t3)), // server processing
fmta(t7.Sub(t4)), // content transfer
fmtb(t1.Sub(t0)), // namelookup
fmtb(t3.Sub(t0)), // connect
fmtb(t4.Sub(t0)), // starttransfer
fmtb(t7.Sub(t0)), // total
)
switch url.Scheme {
case "https":
printTemplate(HTTPSTemplate, report.Timing)
case "http":
printTemplate(HTTPTemplate, report.Timing)
}
}

if followRedirects && isRedirect(resp) {
Expand All @@ -384,6 +405,40 @@ func visit(url *url.URL) {
}
}

func msSince(t time.Time) int {
return int(time.Now().Sub(t) / time.Millisecond)
}

func printTemplate(tmpl string, vars Timing) {
rvars := reflect.ValueOf(vars)
b := []byte(tmpl)
for idx := bytes.IndexByte(b, '%'); idx != -1; idx = bytes.IndexByte(b, '%') {
dir := b[idx+1]
end := idx + 2
for ; end < len(b) && ((b[end] >= 'a' && b[end] <= 'z') || (b[end] >= 'A' && b[end] <= 'Z')); end++ {
}
vnam := string(b[idx+2 : end])
copy(b[idx:end], bytes.Repeat([]byte{' '}, end-idx))
val := rvars.FieldByName(vnam)
if !val.IsValid() {
panic("invalid template variable: " + vnam)
}
v := strconv.Itoa(val.Interface().(int)) + "ms"
vlen := len(v)
v = color.CyanString(v)
switch dir {
case '>':
b = append(append(append([]byte{}, b[:end-vlen]...), []byte(v)...), b[end:]...)
case '<':
b = append(append(append([]byte{}, b[:idx]...), []byte(v)...), b[idx+vlen:]...)
default:
panic("invalid direction: " + string(dir))
}
idx = end
}
print(string(b))
}

func isRedirect(resp *http.Response) bool {
return resp.StatusCode > 299 && resp.StatusCode < 400
}
Expand Down