srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/internal/ipc/ipc.go
blob: c9d92b4715a1089f5209b099e4b40931a36bcdfe (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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
}