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