srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/internal/ipc
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-09-23 21:33:00 +0200
committersrdusr <[email protected]>2024-09-23 21:33:00 +0200
commitc2443f27ef5a844f045c038c7689d217d1dbf0c4 (patch)
tree24f45e2047dc3bb0f9e34b553d12157adc5e9b39 /internal/ipc
parentaae93b4575e10d223c6cdd8722ca0cce2d47397c (diff)
downloadmitmux-c2443f27ef5a844f045c038c7689d217d1dbf0c4.tar.gz
mitmux-c2443f27ef5a844f045c038c7689d217d1dbf0c4.zip
Match-and-replace: header rewrite rules
Implements build-order step 6, scoped to headers only for this pass - see PLAN.md for why bodies are a separate problem (request-body capture currently depends on streaming straight through, which a body-rewriting rule would have to interrupt; deciding what "exact" means for a rule-modified request needs its own pass, not a rushed add-on to this one). internal/rules: Rule type and ApplyHeaders, which serializes a Header map to a raw "Name: value\r\n" block, runs enabled rules' match/replace over that text, and reparses it - operating on text rather than per-value substitution is what lets a rule add or remove a header, not just rewrite one, matching how Burp's header match/replace works. Invalid rule output (bad regex, unparseable result) leaves the header map untouched rather than corrupting the request. internal/store: rules table + CRUD. internal/proxy: forward() fetches enabled rules for each scope and applies them to outReq.Header / resp.Header, positioned so the existing capture/history pipeline is untouched - request_raw keeps showing what the client actually sent and response_raw what the origin actually sent, while the wire itself reflects the rules. Deliberate split: match-and-replace transforms traffic, it doesn't rewrite the audit trail. internal/ipc gains rules_list/rules_save/rules_delete/rules_toggle. cmd/mitmux gains a rules view ('m' from history) with add/edit/delete/toggle and a small form (name, match, replace, scope, regex). Verified live against real external traffic, not just local echoes: a request-scope rule rewriting User-Agent, confirmed via httpbin.org's own header echo that the origin received the rewritten value while curl sent the real one; a response-scope rule rewriting the Server header, confirmed the client actually received the rewritten value; disabling a rule confirmed via a follow-up request that it stops applying; and throughout, history continued showing the pre-rule original on both sides, confirming the capture/transform split holds.
Diffstat (limited to 'internal/ipc')
-rw-r--r--internal/ipc/ipc.go65
-rw-r--r--internal/ipc/server.go41
2 files changed, 104 insertions, 2 deletions
diff --git a/internal/ipc/ipc.go b/internal/ipc/ipc.go
index f890715..19c9c23 100644
--- a/internal/ipc/ipc.go
+++ b/internal/ipc/ipc.go
@@ -11,12 +11,13 @@ import (
"net"
"sync"
+ "mitmux/internal/rules"
"mitmux/internal/store"
)
// Request is sent by a client to the daemon.
type Request struct {
- Type string `json:"type"` // "list", "get", "subscribe", or "repeat"
+ Type string `json:"type"` // "list", "get", "subscribe", "repeat", "rules_list", "rules_save", "rules_delete", or "rules_toggle"
Limit int `json:"limit,omitempty"`
BeforeID int64 `json:"before_id,omitempty"`
ID int64 `json:"id,omitempty"`
@@ -30,14 +31,22 @@ type Request struct {
Scheme string `json:"scheme,omitempty"`
Host string `json:"host,omitempty"`
Raw []byte `json:"raw,omitempty"`
+
+ // For "rules_save": add (Rule.ID == 0) or update (Rule.ID != 0) a
+ // match-and-replace rule. For "rules_delete"/"rules_toggle": RuleID
+ // (and RuleEnabled for toggle) identify the target.
+ Rule *rules.Rule `json:"rule,omitempty"`
+ RuleID int64 `json:"rule_id,omitempty"`
+ RuleEnabled bool `json:"rule_enabled,omitempty"`
}
// Response is sent by the daemon to a client.
type Response struct {
- Type string `json:"type"` // "list", "get", "new", "repeat", or "error"
+ Type string `json:"type"` // "list", "get", "new", "repeat", "rules", 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)
+ Rules []rules.Rule `json:"rules,omitempty"` // for "rules"
Error string `json:"error,omitempty"`
}
@@ -144,6 +153,58 @@ func (c *Client) Repeat(scheme, host string, raw []byte) (*EntryDetail, error) {
return resp.Detail, nil
}
+// ListRules returns every match-and-replace rule.
+func (c *Client) ListRules() ([]rules.Rule, error) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ return c.rulesRoundTrip(Request{Type: "rules_list"})
+}
+
+// SaveRule adds r (if r.ID == 0) or updates the existing rule with that
+// ID, and returns its ID.
+func (c *Client) SaveRule(r rules.Rule) (int64, error) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ saved, err := c.rulesRoundTrip(Request{Type: "rules_save", Rule: &r})
+ if err != nil {
+ return 0, err
+ }
+ if len(saved) == 0 {
+ return 0, errors.New("rules_save: daemon returned no rule")
+ }
+ return saved[0].ID, nil
+}
+
+// DeleteRule removes a rule.
+func (c *Client) DeleteRule(id int64) error {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ _, err := c.rulesRoundTrip(Request{Type: "rules_delete", RuleID: id})
+ return err
+}
+
+// SetRuleEnabled toggles a rule without touching its other fields.
+func (c *Client) SetRuleEnabled(id int64, enabled bool) error {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ _, err := c.rulesRoundTrip(Request{Type: "rules_toggle", RuleID: id, RuleEnabled: enabled})
+ return err
+}
+
+func (c *Client) rulesRoundTrip(req Request) ([]rules.Rule, error) {
+ if err := c.enc.Encode(req); 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.Rules, 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.
diff --git a/internal/ipc/server.go b/internal/ipc/server.go
index 69044af..1fe8d8f 100644
--- a/internal/ipc/server.go
+++ b/internal/ipc/server.go
@@ -8,6 +8,7 @@ import (
"net"
"sync"
+ "mitmux/internal/rules"
"mitmux/internal/store"
)
@@ -127,6 +128,46 @@ func (s *Server) handleConn(conn net.Conn) {
}
enc.Encode(Response{Type: "repeat", Detail: detailFromEntry(e)})
+ case "rules_list":
+ rs, err := s.db.ListRules()
+ if err != nil {
+ enc.Encode(Response{Type: "error", Error: err.Error()})
+ continue
+ }
+ enc.Encode(Response{Type: "rules", Rules: rs})
+
+ case "rules_save":
+ if req.Rule == nil {
+ enc.Encode(Response{Type: "error", Error: "rules_save: missing rule"})
+ continue
+ }
+ r := *req.Rule
+ var err error
+ if r.ID == 0 {
+ r.ID, err = s.db.AddRule(r)
+ } else {
+ err = s.db.UpdateRule(r)
+ }
+ if err != nil {
+ enc.Encode(Response{Type: "error", Error: err.Error()})
+ continue
+ }
+ enc.Encode(Response{Type: "rules", Rules: []rules.Rule{r}})
+
+ case "rules_delete":
+ if err := s.db.DeleteRule(req.RuleID); err != nil {
+ enc.Encode(Response{Type: "error", Error: err.Error()})
+ continue
+ }
+ enc.Encode(Response{Type: "rules"})
+
+ case "rules_toggle":
+ if err := s.db.SetRuleEnabled(req.RuleID, req.RuleEnabled); err != nil {
+ enc.Encode(Response{Type: "error", Error: err.Error()})
+ continue
+ }
+ enc.Encode(Response{Type: "rules"})
+
case "subscribe":
sub := s.hub.subscribe()
defer s.hub.unsubscribe(sub)