package source
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
)
// File replays a capture written to disk. Two formats are supported:
//
// - *.har: HTTP Archive (only the minimum fields needed for display)
// - *.txt: raw framed bytes, the same layout produced by unix.WriteFrame
type File struct {
path string
format string
idx int
har *harRoot
rawFile *os.File
}
type harRoot struct {
Log struct {
Entries []harEntry `json:"entries"`
} `json:"log"`
}
type harEntry struct {
StartedDateTime time.Time `json:"startedDateTime"`
Request struct {
Method string `json:"method"`
URL string `json:"url"`
HTTPVersion string `json:"httpVersion"`
Headers []harHeader `json:"headers"`
PostData harPostData `json:"postData,omitempty"`
} `json:"request"`
Response struct {
Status int `json:"status"`
StatusText string `json:"statusText"`
HTTPVersion string `json:"httpVersion"`
Headers []harHeader `json:"headers"`
Content harBodyFragment `json:"content"`
} `json:"response"`
}
type harHeader struct{ Name, Value string }
type harPostData struct{ Text string `json:"text"` }
type harBodyFragment struct{ Text string `json:"text"` }
// OpenFile inspects the filename suffix to pick a decoder.
func OpenFile(path string) (*File, error) {
f := &File{path: path, format: strings.ToLower(filepath.Ext(path))}
switch f.format {
case ".har", ".json":
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
if err := json.Unmarshal(data, &f.har); err != nil {
return nil, fmt.Errorf("file: parse har: %w", err)
}
case ".txt", ".bin":
fh, err := os.Open(path)
if err != nil {
return nil, err
}
f.rawFile = fh
default:
return nil, fmt.Errorf("file: unsupported extension %q", f.format)
}
return f, nil
}
// Next yields one entry, rendered back to raw HTTP/1.1 wire bytes for
// the HAR backend, or one Event for the raw backend.
func (f *File) Next(ctx context.Context) (*Event, error) {
if f.har != nil {
if f.idx >= len(f.har.Log.Entries) {
return nil, io.EOF
}
ent := f.har.Log.Entries[f.idx]
f.idx++
return &Event{
ConnID: uint64(f.idx),
Direction: 0,
Payload: renderHARRequest(ent),
At: ent.StartedDateTime,
}, nil
}
return nil, io.EOF
}
// Close releases the backing file if any.
func (f *File) Close() error {
if f.rawFile != nil {
return f.rawFile.Close()
}
return nil
}
func renderHARRequest(e harEntry) []byte {
var b strings.Builder
fmt.Fprintf(&b, "%s %s %s\r\n", e.Request.Method, e.Request.URL, e.Request.HTTPVersion)
for _, h := range e.Request.Headers {
fmt.Fprintf(&b, "%s: %s\r\n", h.Name, h.Value)
}
b.WriteString("\r\n")
b.WriteString(e.Request.PostData.Text)
return []byte(b.String())
}