internal/export/har/har.go

// Package har writes captured traffic as an HTTP Archive 1.2 document.
// It implements only the fields httptap populates; many optional fields
// (e.g. WebSocket messages) are omitted.
//
// mercemay.top/src/httptap/
package har

import (
	"encoding/json"
	"io"
	"time"

	"mercemay.top/httptap/internal/parser"
)

// Log is the top-level HAR container.
type Log struct {
	Version string    `json:"version"`
	Creator Creator   `json:"creator"`
	Entries []Entry   `json:"entries"`
}

// Creator identifies the program that produced the HAR.
type Creator struct {
	Name    string `json:"name"`
	Version string `json:"version"`
}

// Entry is one request/response pair.
type Entry struct {
	StartedDateTime string   `json:"startedDateTime"`
	Time            int64    `json:"time"`
	Request         Request  `json:"request"`
	Response        Response `json:"response"`
	Cache           struct{} `json:"cache"`
	Timings         Timings  `json:"timings"`
}

// Request is the HAR request object.
type Request struct {
	Method      string          `json:"method"`
	URL         string          `json:"url"`
	HTTPVersion string          `json:"httpVersion"`
	Headers     []NameValue     `json:"headers"`
	QueryString []NameValue     `json:"queryString"`
	HeadersSize int             `json:"headersSize"`
	BodySize    int             `json:"bodySize"`
	PostData    *PostData       `json:"postData,omitempty"`
}

// Response is the HAR response object.
type Response struct {
	Status      int          `json:"status"`
	StatusText  string       `json:"statusText"`
	HTTPVersion string       `json:"httpVersion"`
	Headers     []NameValue  `json:"headers"`
	Content     Content      `json:"content"`
	RedirectURL string       `json:"redirectURL"`
	HeadersSize int          `json:"headersSize"`
	BodySize    int          `json:"bodySize"`
}

// NameValue is the HAR generic name/value pair.
type NameValue struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}

// PostData carries a request body.
type PostData struct {
	MimeType string `json:"mimeType"`
	Text     string `json:"text"`
}

// Content carries a response body.
type Content struct {
	Size     int64  `json:"size"`
	MimeType string `json:"mimeType"`
	Text     string `json:"text"`
	Encoding string `json:"encoding,omitempty"`
}

// Timings is the HAR timing record. Missing fields are -1 per spec.
type Timings struct {
	Blocked int64 `json:"blocked"`
	DNS     int64 `json:"dns"`
	Connect int64 `json:"connect"`
	Send    int64 `json:"send"`
	Wait    int64 `json:"wait"`
	Receive int64 `json:"receive"`
	SSL     int64 `json:"ssl"`
}

// Write renders entries as a full HAR document and writes it to w.
func Write(w io.Writer, entries []Entry) error {
	doc := struct {
		Log Log `json:"log"`
	}{
		Log: Log{
			Version: "1.2",
			Creator: Creator{Name: "httptap", Version: "dev"},
			Entries: entries,
		},
	}
	enc := json.NewEncoder(w)
	enc.SetIndent("", "  ")
	return enc.Encode(doc)
}

// FromPair builds an Entry from a request/response message pair.
func FromPair(req, resp parser.Message, at time.Time, dur time.Duration) Entry {
	return Entry{
		StartedDateTime: at.UTC().Format(time.RFC3339Nano),
		Time:            dur.Milliseconds(),
		Request: Request{
			Method:      parser.Method(req),
			URL:         parser.URL(req),
			HTTPVersion: req.Protocol,
			Headers:     toNameValues(req.Headers),
			HeadersSize: -1,
			BodySize:    len(req.Body),
		},
		Response: Response{
			Status:      parser.StatusCode(resp),
			StatusText:  parser.StatusReason(resp),
			HTTPVersion: resp.Protocol,
			Headers:     toNameValues(resp.Headers),
			Content: Content{
				Size:     int64(len(resp.Body)),
				MimeType: parser.Header(resp, "Content-Type"),
				Text:     string(resp.Body),
			},
			HeadersSize: -1,
			BodySize:    len(resp.Body),
		},
		Timings: Timings{Blocked: -1, DNS: -1, Connect: -1, SSL: -1,
			Send: 0, Wait: dur.Milliseconds(), Receive: 0},
	}
}

func toNameValues(h [][2]string) []NameValue {
	out := make([]NameValue, len(h))
	for i, kv := range h {
		out[i] = NameValue{Name: kv[0], Value: kv[1]}
	}
	return out
}