// Package ipc is the protocol between mitmuxd (which owns the proxy and // the history database) and a client such as the TUI, spoken as // newline-agnostic JSON messages over a Unix domain socket. This keeps // the proxy engine running independently of any UI attached to it. package ipc import ( "encoding/json" "errors" "fmt" "net" "mitmux/internal/store" ) // Request is sent by a client to the daemon. type Request struct { Type string `json:"type"` // "list", "get", or "subscribe" Limit int `json:"limit,omitempty"` BeforeID int64 `json:"before_id,omitempty"` ID int64 `json:"id,omitempty"` } // Response is sent by the daemon to a client. type Response struct { Type string `json:"type"` // "list", "get", "new", or "error" Entries []store.Summary `json:"entries,omitempty"` // for "list" Detail *EntryDetail `json:"detail,omitempty"` // for "get" New *store.Summary `json:"new,omitempty"` // for "new" (subscribe push) Error string `json:"error,omitempty"` } // EntryDetail is a full history entry, raw bytes included. type EntryDetail struct { store.Summary RequestRaw []byte `json:"request_raw"` ResponseRaw []byte `json:"response_raw"` RequestExact bool `json:"request_exact"` ResponseExact bool `json:"response_exact"` } // Client talks to a mitmuxd instance for request/response queries // (list, get). Use Subscribe separately for the live-update stream. type Client struct { conn net.Conn dec *json.Decoder enc *json.Encoder } // Dial connects to the daemon's control socket at path. func Dial(path string) (*Client, error) { conn, err := net.Dial("unix", path) if err != nil { return nil, fmt.Errorf("dial %s: %w", path, err) } return &Client{conn: conn, dec: json.NewDecoder(conn), enc: json.NewEncoder(conn)}, nil } // Close closes the connection to the daemon. func (c *Client) Close() error { return c.conn.Close() } // List returns up to limit history summaries older than beforeID (0 for // the most recent), newest first. func (c *Client) List(limit int, beforeID int64) ([]store.Summary, error) { if err := c.enc.Encode(Request{Type: "list", Limit: limit, BeforeID: beforeID}); err != nil { return nil, err } var resp Response if err := c.dec.Decode(&resp); err != nil { return nil, err } if resp.Type == "error" { return nil, errors.New(resp.Error) } return resp.Entries, nil } // Get returns the full entry (raw bytes included) for id. func (c *Client) Get(id int64) (*EntryDetail, error) { if err := c.enc.Encode(Request{Type: "get", ID: id}); err != nil { return nil, err } var resp Response if err := c.dec.Decode(&resp); err != nil { return nil, err } if resp.Type == "error" { return nil, errors.New(resp.Error) } return resp.Detail, nil } // Subscribe opens a dedicated connection that streams newly captured // history entries as they happen. The returned channel is closed when // the connection ends; call the returned close func to stop early. func Subscribe(path string) (<-chan store.Summary, func() error, error) { conn, err := net.Dial("unix", path) if err != nil { return nil, nil, fmt.Errorf("dial %s: %w", path, err) } if err := json.NewEncoder(conn).Encode(Request{Type: "subscribe"}); err != nil { conn.Close() return nil, nil, err } ch := make(chan store.Summary, 64) go func() { defer close(ch) dec := json.NewDecoder(conn) for { var resp Response if err := dec.Decode(&resp); err != nil { return } if resp.Type == "new" && resp.New != nil { select { case ch <- *resp.New: default: } } } }() return ch, conn.Close, nil }