diff options
Diffstat (limited to 'internal/ipc/ipc.go')
| -rw-r--r-- | internal/ipc/ipc.go | 126 |
1 files changed, 126 insertions, 0 deletions
diff --git a/internal/ipc/ipc.go b/internal/ipc/ipc.go new file mode 100644 index 0000000..c9d92b4 --- /dev/null +++ b/internal/ipc/ipc.go @@ -0,0 +1,126 @@ +// 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 +} |