srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/internal/rules
diff options
context:
space:
mode:
Diffstat (limited to 'internal/rules')
-rw-r--r--internal/rules/rules.go97
1 files changed, 97 insertions, 0 deletions
diff --git a/internal/rules/rules.go b/internal/rules/rules.go
new file mode 100644
index 0000000..1f50ec4
--- /dev/null
+++ b/internal/rules/rules.go
@@ -0,0 +1,97 @@
+// Package rules implements match-and-replace: user-defined rules that
+// rewrite request/response headers as they pass through the proxy.
+// Deliberately headers-only for now - see ApplyHeaders for why bodies
+// are a separate, harder problem (noted as a follow-up in PLAN.md).
+package rules
+
+import (
+ "bufio"
+ "net/http"
+ "net/textproto"
+ "regexp"
+ "sort"
+ "strings"
+)
+
+// Rule is one match-and-replace rule.
+type Rule struct {
+ ID int64
+ Enabled bool
+ Name string
+ Scope string // "request" or "response"
+ Part string // "header" (only part supported so far)
+ Match string
+ Replace string
+ IsRegex bool
+ // Position orders rule application (ascending) when several rules
+ // could touch the same text.
+ Position int
+}
+
+// ApplyHeaders rewrites h in place by serializing it to a raw
+// "Name: value\r\n" block, running every enabled rule with Part=="header"
+// over that text (in Position order), and reparsing the result. Working
+// on the raw text rather than per-value substitution is what lets a rule
+// add or remove a header entirely, not just rewrite an existing value -
+// matching how Burp's header match/replace works. If a rule's output
+// doesn't parse back as valid headers, ApplyHeaders returns h unchanged
+// rather than risk sending something corrupted.
+func ApplyHeaders(h http.Header, rs []Rule) http.Header {
+ keys := make([]string, 0, len(h))
+ for k := range h {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+
+ var block strings.Builder
+ for _, k := range keys {
+ for _, v := range h[k] {
+ block.WriteString(k)
+ block.WriteString(": ")
+ block.WriteString(v)
+ block.WriteString("\r\n")
+ }
+ }
+ text := block.String()
+
+ changed := false
+ for _, r := range sortedByPosition(rs) {
+ if !r.Enabled || r.Part != "header" {
+ continue
+ }
+ if next, ok := apply(text, r); ok {
+ text, changed = next, true
+ }
+ }
+ if !changed {
+ return h
+ }
+
+ tp := textproto.NewReader(bufio.NewReader(strings.NewReader(text + "\r\n")))
+ mh, err := tp.ReadMIMEHeader()
+ if err != nil {
+ return h
+ }
+ return http.Header(mh)
+}
+
+func sortedByPosition(rs []Rule) []Rule {
+ out := make([]Rule, len(rs))
+ copy(out, rs)
+ sort.SliceStable(out, func(i, j int) bool { return out[i].Position < out[j].Position })
+ return out
+}
+
+func apply(text string, r Rule) (string, bool) {
+ if r.IsRegex {
+ re, err := regexp.Compile(r.Match)
+ if err != nil {
+ return text, false
+ }
+ return re.ReplaceAllString(text, r.Replace), true
+ }
+ if r.Match == "" {
+ return text, false
+ }
+ return strings.ReplaceAll(text, r.Match, r.Replace), true
+}