diff options
| author | srdusr <[email protected]> | 2026-05-24 09:50:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2026-05-24 09:50:00 +0200 |
| commit | 0d1ce88962fe31ce1d204634e6ea10998a02827a (patch) | |
| tree | d1ccafa01f166dea867c6eca5a02fbaa477f86f6 /internal | |
| parent | d522b1177a9e1fdd04888121975f2b3509d19564 (diff) | |
| download | mitmux-0d1ce88962fe31ce1d204634e6ea10998a02827a.tar.gz mitmux-0d1ce88962fe31ce1d204634e6ea10998a02827a.zip | |
Import: bring a HAR file's entries into history
Closes the interop loop HAR export opened - traffic can now move both
directions between mitmux and any other HAR-producing tool (browser
DevTools, Burp, Postman), not just out.
cmd/mitmux/har.go: rawRequestFromHAR/rawResponseFromHAR reconstruct
HTTP/1.1 wire bytes from HAR's structured fields - the mirror image of
harEntryFromDetail on the export side. Deliberately tolerant of a HAR
file that didn't come from mitmux at all: lowercase header names,
"HTTP/2" in httpVersion, missing optional fields like postData, a
redirectURL nobody filled in. Content-Encoding and Transfer-Encoding
headers are stripped from the reconstructed response before writing it
- HAR's content.text is already decoded per spec, so re-emitting those
headers would describe framing the body no longer has and break any
client that tried to decode it again - and a Content-Length is computed
if the HAR didn't carry a consistent one. importEntriesFromHAR converts
a whole document, skipping (not failing on) any entry that fails to
convert, same reasoning as export's own skip-and-continue for a
malformed capture.
Imported entries are always request_exact=false/response_exact=false:
reconstructed from structured HAR fields, the same situation an HTTP/2
capture is already in, never claiming to be the literal bytes that were
actually on the wire for the original request.
internal/ipc: ImportEntry (the slim shape the client sends - the daemon
just stores what it's given, all HAR parsing happens client-side) and
an "import" request type; Client.Import returns how many entries were
actually inserted, a per-entry store failure is skipped rather than
aborting the batch. Server-side, imported entries are tagged
source="import" so source:import finds them in search, same as
source:repeater/source:intruder already work.
TUI: 'I' from the history list prompts for a HAR path (same modal
pattern as export, in reverse - reading instead of writing), then
reloads the list and status once the import completes.
Verified live: exported real captured traffic to HAR, cleared history
entirely, imported the same file back and got both entries with
correct content (byte-different after the round trip - headers get
reordered/reformatted - but semantically identical, correctly labeled
"reconstructed" rather than falsely "exact"); hand-built a HAR mimicking
a real Chrome DevTools export (lowercase headers, HTTP/2, a base64-
encoded binary PNG body, several optional fields omitted) and confirmed
it imports cleanly with the binary body decoded correctly (PNG magic
bytes verified byte-for-byte); confirmed a missing file and invalid
JSON both fail with a clear error and no crash, history left untouched.
go build/vet/gofmt/test/mod tidy all clean.
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/ipc/ipc.go | 50 | ||||
| -rw-r--r-- | internal/ipc/server.go | 21 |
2 files changed, 70 insertions, 1 deletions
diff --git a/internal/ipc/ipc.go b/internal/ipc/ipc.go index 2abce89..89a8343 100644 --- a/internal/ipc/ipc.go +++ b/internal/ipc/ipc.go @@ -67,11 +67,34 @@ type Request struct { // For "set_flagged" and "delete_entry": ID identifies the history // entry. "clear_history" needs no fields at all. Flagged bool `json:"flagged,omitempty"` + + // For "import": entries to insert directly into history - the + // client has already parsed/converted them (e.g. from a HAR file, + // see cmd/mitmux/har.go); the daemon just stores them. + ImportEntries []ImportEntry `json:"import_entries,omitempty"` +} + +// ImportEntry is one entry to insert directly into history via +// "import". RequestExact/ResponseExact are always false once stored: +// an imported entry is reconstructed from whatever structured format it +// came from (HAR, say), never the literal bytes that were on the wire +// for the original request - the same situation an HTTP/2 capture is +// already in. +type ImportEntry struct { + Method string `json:"method"` + Scheme string `json:"scheme"` + Host string `json:"host"` + Path string `json:"path"` + StatusCode int `json:"status_code"` + RequestRaw []byte `json:"request_raw"` + ResponseRaw []byte `json:"response_raw"` + StartedAt time.Time `json:"started_at"` + Duration time.Duration `json:"duration"` } // Response is sent by the daemon to a client. type Response struct { - Type string `json:"type"` // "list", "get", "new", "repeat", "rules", "scope_rules", "intrude_result", "intrude_done", "status", "flagged", "deleted", "cleared", or "error" + Type string `json:"type"` // "list", "get", "new", "repeat", "rules", "scope_rules", "intrude_result", "intrude_done", "import_done", "status", "flagged", "deleted", "cleared", or "error" Entries []store.Summary `json:"entries,omitempty"` // for "list" Detail *EntryDetail `json:"detail,omitempty"` // for "get" and "repeat" New *store.Summary `json:"new,omitempty"` // for "new" (subscribe push) @@ -79,6 +102,10 @@ type Response struct { ScopeRules []scope.Rule `json:"scope_rules,omitempty"` // for "scope_rules" Status *StatusMsg `json:"status,omitempty"` // for "status" + // For "import_done": how many entries were actually inserted (a + // per-entry insert failure is skipped, not fatal to the batch). + Imported int `json:"imported,omitempty"` + // For "intrude_result": one completed attack request. IntrudeResult *IntrudeResultMsg `json:"intrude_result,omitempty"` @@ -203,6 +230,27 @@ func (c *Client) ClearHistory() error { return nil } +// Import inserts entries directly into history - used to bring in +// traffic from an external source (a HAR file, say) rather than +// something mitmux itself captured. Returns how many were actually +// inserted; a per-entry insert failure is skipped rather than aborting +// the whole batch. +func (c *Client) Import(entries []ImportEntry) (int, error) { + c.mu.Lock() + defer c.mu.Unlock() + if err := c.enc.Encode(Request{Type: "import", ImportEntries: entries}); err != nil { + return 0, err + } + var resp Response + if err := c.dec.Decode(&resp); err != nil { + return 0, err + } + if resp.Type == "error" { + return 0, errors.New(resp.Error) + } + return resp.Imported, nil +} + // Status returns basic daemon info for a status bar. func (c *Client) Status() (*StatusMsg, error) { c.mu.Lock() diff --git a/internal/ipc/server.go b/internal/ipc/server.go index 303fdef..c83254e 100644 --- a/internal/ipc/server.go +++ b/internal/ipc/server.go @@ -230,6 +230,27 @@ func (s *Server) handleConn(conn net.Conn) { } enc.Encode(Response{Type: "cleared"}) + case "import": + imported := 0 + for _, ie := range req.ImportEntries { + e := &store.Entry{ + StartedAt: ie.StartedAt, + Duration: ie.Duration, + Method: ie.Method, + Scheme: ie.Scheme, + Host: ie.Host, + Path: ie.Path, + StatusCode: ie.StatusCode, + RequestRaw: ie.RequestRaw, + ResponseRaw: ie.ResponseRaw, + Source: "import", + } + if _, err := s.db.Insert(e); err == nil { + imported++ + } + } + enc.Encode(Response{Type: "import_done", Imported: imported}) + case "rules_list": rs, err := s.db.ListRules() if err != nil { |