srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/ipc/ipc.go48
-rw-r--r--internal/ipc/server.go14
-rw-r--r--internal/proxy/intrude.go233
-rw-r--r--internal/proxy/intrude_test.go118
4 files changed, 326 insertions, 87 deletions
diff --git a/internal/ipc/ipc.go b/internal/ipc/ipc.go
index 89a8343..d581313 100644
--- a/internal/ipc/ipc.go
+++ b/internal/ipc/ipc.go
@@ -12,6 +12,7 @@ import (
"sync"
"time"
+ "mitmux/internal/proxy"
"mitmux/internal/rules"
"mitmux/internal/scope"
"mitmux/internal/store"
@@ -35,9 +36,13 @@ type Request struct {
Host string `json:"host,omitempty"`
Raw []byte `json:"raw,omitempty"`
- // For "intrude": the payload set, applied to each marked position in
- // turn (Sniper-style - see proxy.Intrude).
- Payloads []string `json:"payloads,omitempty"`
+ // For "intrude": the attack mode (proxy.Sniper and so on - empty
+ // defaults to Sniper) and its payload sets. Sniper and BatteringRam
+ // only ever use PayloadSets[0] (one shared set); Pitchfork and
+ // ClusterBomb require exactly one set per §marked§ position, in
+ // order - see proxy.Intrude.
+ Mode proxy.AttackMode `json:"mode,omitempty"`
+ PayloadSets [][]string `json:"payload_sets,omitempty"`
// For "intrude": optional Go regexps evaluated against each result's
// response bytes. GrepMatch flags whether it matched at all;
@@ -118,10 +123,14 @@ type StatusMsg struct {
HistoryCount int64 `json:"history_count"`
}
-// IntrudeResultMsg is one completed Intruder attack request.
+// IntrudeResultMsg is one completed Intruder attack request. Values holds
+// what was substituted into each §marked§ position for this request, in
+// position order - for Sniper, every entry but the one fuzzed position
+// equals that position's base value; for the other three modes every
+// entry is an actual payload.
type IntrudeResultMsg struct {
- Position int `json:"position"`
- Payload string `json:"payload"`
+ Iteration int `json:"iteration"`
+ Values []string `json:"values"`
EntryID int64 `json:"entry_id"`
StatusCode int `json:"status_code"`
RespSize int `json:"resp_size"`
@@ -468,23 +477,24 @@ func Subscribe(path string) (<-chan store.Summary, func() error, error) {
return ch, conn.Close, nil
}
-// Intrude starts a Sniper attack (see proxy.Intrude): template must
-// contain at least one §marked§ position, fuzzed in turn through
-// payloads. grepMatch/grepExtract are optional Go regexps evaluated
-// server-side against each result's response bytes (empty string
-// disables either check) - see IntrudeResultMsg. Unlike Subscribe's live
-// feed, no result is ever dropped for a slow consumer - each one is the
-// attack's actual data, not a notification with the real thing
-// recoverable elsewhere. A setup error (bad markers, empty payload set,
-// too many requests, an unparseable grep regexp) is returned directly
-// rather than through the channel. The returned channel closes when the
-// attack finishes or the connection is closed early.
-func Intrude(path, scheme, host string, template []byte, payloads []string, grepMatch, grepExtract string) (<-chan IntrudeResultMsg, func() error, error) {
+// Intrude starts an attack (see proxy.Intrude and proxy.AttackMode):
+// template must contain at least one §marked§ position. mode selects how
+// payloadSets combine across positions; "" defaults to Sniper.
+// grepMatch/grepExtract are optional Go regexps evaluated server-side
+// against each result's response bytes (empty string disables either
+// check) - see IntrudeResultMsg. Unlike Subscribe's live feed, no result
+// is ever dropped for a slow consumer - each one is the attack's actual
+// data, not a notification with the real thing recoverable elsewhere. A
+// setup error (bad markers, empty payload set, too many requests, an
+// unparseable grep regexp) is returned directly rather than through the
+// channel. The returned channel closes when the attack finishes or the
+// connection is closed early.
+func Intrude(path, scheme, host string, template []byte, mode proxy.AttackMode, payloadSets [][]string, grepMatch, grepExtract string) (<-chan IntrudeResultMsg, func() error, error) {
conn, err := net.Dial("unix", path)
if err != nil {
return nil, nil, fmt.Errorf("dial %s: %w", path, err)
}
- req := Request{Type: "intrude", Scheme: scheme, Host: host, Raw: template, Payloads: payloads, GrepMatch: grepMatch, GrepExtract: grepExtract}
+ req := Request{Type: "intrude", Scheme: scheme, Host: host, Raw: template, Mode: mode, PayloadSets: payloadSets, GrepMatch: grepMatch, GrepExtract: grepExtract}
if err := json.NewEncoder(conn).Encode(req); err != nil {
conn.Close()
return nil, nil, err
diff --git a/internal/ipc/server.go b/internal/ipc/server.go
index c83254e..c890a54 100644
--- a/internal/ipc/server.go
+++ b/internal/ipc/server.go
@@ -9,6 +9,7 @@ import (
"regexp"
"sync"
+ "mitmux/internal/proxy"
"mitmux/internal/rules"
"mitmux/internal/scope"
"mitmux/internal/store"
@@ -20,11 +21,12 @@ type Repeater interface {
Repeat(ctx context.Context, scheme, host string, raw []byte) (*store.Entry, error)
}
-// Intruder runs a Sniper attack over a §marked§ request template -
+// Intruder runs an attack (Sniper, Battering ram, Pitchfork, or Cluster
+// bomb - see proxy.AttackMode) over a §marked§ request template -
// implemented by *proxy.Server.
type Intruder interface {
- Intrude(ctx context.Context, scheme, host string, template []byte, payloads []string,
- onResult func(position int, payload string, entry *store.Entry, sendErr error) bool) error
+ Intrude(ctx context.Context, scheme, host string, template []byte, mode proxy.AttackMode, payloadSets [][]string,
+ onResult func(iteration int, values []string, entry *store.Entry, sendErr error) bool) error
}
// Hub fans out newly captured history entries to subscribed clients.
@@ -174,9 +176,9 @@ func (s *Server) handleConn(conn net.Conn) {
}
grepExtractRe = re
}
- err := s.intruder.Intrude(context.Background(), req.Scheme, req.Host, req.Raw, req.Payloads,
- func(position int, payload string, entry *store.Entry, sendErr error) bool {
- r := IntrudeResultMsg{Position: position, Payload: payload}
+ err := s.intruder.Intrude(context.Background(), req.Scheme, req.Host, req.Raw, req.Mode, req.PayloadSets,
+ func(iteration int, values []string, entry *store.Entry, sendErr error) bool {
+ r := IntrudeResultMsg{Iteration: iteration, Values: values}
if sendErr != nil {
r.Error = sendErr.Error()
}
diff --git a/internal/proxy/intrude.go b/internal/proxy/intrude.go
index 6595c75..3538f8d 100644
--- a/internal/proxy/intrude.go
+++ b/internal/proxy/intrude.go
@@ -1,10 +1,8 @@
// Intruder-equivalent: mark positions in a raw request template with §
// (Burp's own marker character, so anyone who's used Burp already knows
-// the syntax), and Sniper-attack them - one position fuzzed at a time
-// through a shared payload set, every other marked position holding its
-// base value. Battering ram / pitchfork / cluster bomb are not
-// implemented; Sniper covers the large majority of real Intruder usage
-// and this whole feature is explicitly optional in the build order.
+// the syntax) and fuzz them across four attack modes - Sniper, Battering
+// ram, Pitchfork, and Cluster bomb - matching Burp's own naming and
+// semantics for how positions and payload sets combine.
package proxy
import (
@@ -17,11 +15,33 @@ import (
const marker = "§"
-// maxIntrudeRequests caps positions × payloads for one attack - a safety
-// limit against an accidental huge wordlist times several positions
-// turning into an unbounded flood, not a tuned production value.
+// maxIntrudeRequests caps the number of requests one attack can send - a
+// safety limit against an accidental huge wordlist (or, for Cluster bomb,
+// a payload-set product) turning into an unbounded flood, not a tuned
+// production value.
const maxIntrudeRequests = 1000
+// AttackMode selects how payload sets combine across marked positions,
+// matching Burp's own four attack types.
+type AttackMode string
+
+const (
+ // Sniper fuzzes one position at a time through a single shared
+ // payload set; every other marked position holds its base value.
+ // Requests: positions × len(payloads).
+ Sniper AttackMode = "sniper"
+ // BatteringRam sends the same payload, from a single shared payload
+ // set, into every marked position at once. Requests: len(payloads).
+ BatteringRam AttackMode = "battering_ram"
+ // Pitchfork walks one payload set per position in lockstep - request
+ // i takes payload i from every set. Requests: the shortest set's
+ // length (Burp's own convention when sets are uneven).
+ Pitchfork AttackMode = "pitchfork"
+ // ClusterBomb tries every combination of one payload set per
+ // position. Requests: the product of every set's length.
+ ClusterBomb AttackMode = "cluster_bomb"
+)
+
// IntrudePosition is one marked, resolved insertion point.
type IntrudePosition struct {
Index int // 0-based, in order of appearance
@@ -52,11 +72,11 @@ func ParseMarkers(template []byte) (positions []IntrudePosition, stripped []byte
return positions, buf.Bytes(), nil
}
-// buildRequest re-inserts each position's base value into stripped
-// (computed relative to the ORIGINAL template's marker layout, so this
-// re-derives offsets rather than operating on the already-stripped
-// bytes) except for `active`, which gets payload instead.
-func buildRequest(template []byte, active int, payload string) ([]byte, error) {
+// buildRequestValues re-derives offsets from template's ORIGINAL marker
+// layout (rather than operating on already-stripped bytes) and substitutes
+// values[i] for the i-th marked position, in order. len(values) must equal
+// the number of marked positions in template.
+func buildRequestValues(template []byte, values []string) ([]byte, error) {
parts := bytes.Split(template, []byte(marker))
if len(parts)%2 != 1 {
return nil, fmt.Errorf("unterminated %s marker", marker)
@@ -65,11 +85,10 @@ func buildRequest(template []byte, active int, payload string) ([]byte, error) {
pos := 0
for i, part := range parts {
if i%2 == 1 {
- if pos == active {
- buf.WriteString(payload)
- } else {
- buf.Write(part)
+ if pos >= len(values) {
+ return nil, fmt.Errorf("position %d has no value", pos)
}
+ buf.WriteString(values[pos])
pos++
continue
}
@@ -78,48 +97,166 @@ func buildRequest(template []byte, active int, payload string) ([]byte, error) {
return buf.Bytes(), nil
}
-// Intrude runs a Sniper attack: template must contain at least one
-// §marked§ position. For each position, in order, every payload is sent
-// with that position replaced by the payload and all others at their
-// base value; onResult is called synchronously after each request
-// completes - with the position index, the payload used, the resulting
-// entry (nil if sendErr is set), and any send error - so a caller can
-// stream progress, and stops the attack early if it returns false.
-func (s *Server) Intrude(ctx context.Context, scheme, host string, template []byte, payloads []string,
- onResult func(position int, payload string, entry *store.Entry, sendErr error) bool) error {
- positions, _, err := ParseMarkers(template)
- if err != nil {
- return err
- }
+// intrudeValues computes, for one full attack, every position-substitution
+// set to send - one []string per request (indexed by position, in send
+// order) - according to mode. Pure and side-effect free, so the request
+// count can be validated against maxIntrudeRequests before anything is
+// dispatched, and so it's testable without a live target.
+//
+// payloadSets[0] is the shared payload set for Sniper and BatteringRam,
+// which only ever need one. Pitchfork and ClusterBomb are inherently
+// per-position - theirs is the whole point of the two modes - so they
+// require exactly len(positions) sets, one per marked position in order.
+func intrudeValues(mode AttackMode, positions []IntrudePosition, payloadSets [][]string) ([][]string, error) {
if len(positions) == 0 {
- return fmt.Errorf("no %s-marked positions in the request template", marker)
+ return nil, fmt.Errorf("no %s-marked positions in the request template", marker)
}
- if len(payloads) == 0 {
- return fmt.Errorf("no payloads")
+ if len(payloadSets) == 0 || len(payloadSets[0]) == 0 {
+ return nil, fmt.Errorf("no payloads")
}
- if total := len(positions) * len(payloads); total > maxIntrudeRequests {
- return fmt.Errorf("attack would send %d requests (%d positions × %d payloads), over the %d limit",
- total, len(positions), len(payloads), maxIntrudeRequests)
+
+ bases := make([]string, len(positions))
+ for i, p := range positions {
+ bases[i] = p.Base
}
- for _, pos := range positions {
+ var out [][]string
+ switch mode {
+ case "", Sniper:
+ payloads := payloadSets[0]
+ if total := len(positions) * len(payloads); total > maxIntrudeRequests {
+ return nil, fmt.Errorf("attack would send %d requests (%d positions × %d payloads), over the %d limit",
+ total, len(positions), len(payloads), maxIntrudeRequests)
+ }
+ for posIdx := range positions {
+ for _, payload := range payloads {
+ values := append([]string(nil), bases...)
+ values[posIdx] = payload
+ out = append(out, values)
+ }
+ }
+
+ case BatteringRam:
+ payloads := payloadSets[0]
+ if len(payloads) > maxIntrudeRequests {
+ return nil, fmt.Errorf("attack would send %d requests, over the %d limit", len(payloads), maxIntrudeRequests)
+ }
for _, payload := range payloads {
- raw, err := buildRequest(template, pos.Index, payload)
- if err != nil {
- return err
+ values := make([]string, len(positions))
+ for i := range values {
+ values[i] = payload
+ }
+ out = append(out, values)
+ }
+
+ case Pitchfork:
+ if len(payloadSets) != len(positions) {
+ return nil, fmt.Errorf("pitchfork needs one payload set per position (%d positions, %d payload sets given)",
+ len(positions), len(payloadSets))
+ }
+ n := len(payloadSets[0])
+ for _, set := range payloadSets {
+ if len(set) == 0 {
+ return nil, fmt.Errorf("no payloads")
}
- raw = fixContentLength(raw)
+ if len(set) < n {
+ n = len(set)
+ }
+ }
+ if n > maxIntrudeRequests {
+ return nil, fmt.Errorf("attack would send %d requests, over the %d limit", n, maxIntrudeRequests)
+ }
+ for i := 0; i < n; i++ {
+ values := make([]string, len(positions))
+ for p := range positions {
+ values[p] = payloadSets[p][i]
+ }
+ out = append(out, values)
+ }
- // sendRaw is already self-bounding (dialForRepeat's own dial
- // timeout, then conn.SetDeadline for the rest), so ctx here
- // only needs to carry cancellation - e.g. the IPC connection
- // driving this attack closing mid-run.
- e, sendErr := s.sendRaw(ctx, scheme, host, raw, "intruder")
+ case ClusterBomb:
+ if len(payloadSets) != len(positions) {
+ return nil, fmt.Errorf("cluster bomb needs one payload set per position (%d positions, %d payload sets given)",
+ len(positions), len(payloadSets))
+ }
+ // Checked incrementally, one set at a time, so a pathological
+ // product (e.g. three sets of 10000) bails out before ever
+ // trying to enumerate it, not after.
+ total := 1
+ for _, set := range payloadSets {
+ if len(set) == 0 {
+ return nil, fmt.Errorf("no payloads")
+ }
+ total *= len(set)
+ if total > maxIntrudeRequests {
+ return nil, fmt.Errorf("attack would send at least %d requests, over the %d limit", total, maxIntrudeRequests)
+ }
+ }
+ idx := make([]int, len(positions))
+ for {
+ values := make([]string, len(positions))
+ for p := range positions {
+ values[p] = payloadSets[p][idx[p]]
+ }
+ out = append(out, values)
- if !onResult(pos.Index, payload, e, sendErr) {
- return nil
+ // Odometer increment, rightmost (last) position fastest -
+ // matches Burp's own cluster-bomb iteration order.
+ p := len(positions) - 1
+ for p >= 0 {
+ idx[p]++
+ if idx[p] < len(payloadSets[p]) {
+ break
+ }
+ idx[p] = 0
+ p--
+ }
+ if p < 0 {
+ break
}
}
+
+ default:
+ return nil, fmt.Errorf("unknown attack mode %q", mode)
+ }
+ return out, nil
+}
+
+// Intrude runs one attack of the given mode over a §marked§ request
+// template. onResult is called synchronously after each request completes
+// - with a 0-based iteration index, the values substituted into each
+// marked position for that request (indexed by position, same order as
+// ParseMarkers), the resulting entry (nil if sendErr is set), and any send
+// error - so a caller can stream progress, and stops the attack early if
+// it returns false.
+func (s *Server) Intrude(ctx context.Context, scheme, host string, template []byte, mode AttackMode, payloadSets [][]string,
+ onResult func(iteration int, values []string, entry *store.Entry, sendErr error) bool) error {
+ positions, _, err := ParseMarkers(template)
+ if err != nil {
+ return err
+ }
+
+ requests, err := intrudeValues(mode, positions, payloadSets)
+ if err != nil {
+ return err
+ }
+
+ for i, values := range requests {
+ raw, err := buildRequestValues(template, values)
+ if err != nil {
+ return err
+ }
+ raw = fixContentLength(raw)
+
+ // sendRaw is already self-bounding (dialForRepeat's own dial
+ // timeout, then conn.SetDeadline for the rest), so ctx here
+ // only needs to carry cancellation - e.g. the IPC connection
+ // driving this attack closing mid-run.
+ e, sendErr := s.sendRaw(ctx, scheme, host, raw, "intruder")
+
+ if !onResult(i, values, e, sendErr) {
+ return nil
+ }
}
return nil
}
diff --git a/internal/proxy/intrude_test.go b/internal/proxy/intrude_test.go
index 6a0007f..59aa5a7 100644
--- a/internal/proxy/intrude_test.go
+++ b/internal/proxy/intrude_test.go
@@ -1,6 +1,7 @@
package proxy
import (
+ "fmt"
"reflect"
"testing"
)
@@ -66,36 +67,35 @@ func TestParseMarkers(t *testing.T) {
}
}
-func TestBuildRequest(t *testing.T) {
+func TestBuildRequestValues(t *testing.T) {
template := "GET /a/§1§/b/§2§/c/§3§ HTTP/1.1"
tests := []struct {
- active int
- payload string
- want string
+ values []string
+ want string
}{
- {0, "PAYLOAD", "GET /a/PAYLOAD/b/2/c/3 HTTP/1.1"},
- {1, "PAYLOAD", "GET /a/1/b/PAYLOAD/c/3 HTTP/1.1"},
- {2, "PAYLOAD", "GET /a/1/b/2/c/PAYLOAD HTTP/1.1"},
+ {[]string{"PAYLOAD", "2", "3"}, "GET /a/PAYLOAD/b/2/c/3 HTTP/1.1"},
+ {[]string{"1", "PAYLOAD", "3"}, "GET /a/1/b/PAYLOAD/c/3 HTTP/1.1"},
+ {[]string{"1", "2", "PAYLOAD"}, "GET /a/1/b/2/c/PAYLOAD HTTP/1.1"},
}
for _, tt := range tests {
- got, err := buildRequest([]byte(template), tt.active, tt.payload)
+ got, err := buildRequestValues([]byte(template), tt.values)
if err != nil {
- t.Fatalf("active=%d: unexpected error: %v", tt.active, err)
+ t.Fatalf("values=%v: unexpected error: %v", tt.values, err)
}
if string(got) != tt.want {
- t.Errorf("active=%d: got %q, want %q", tt.active, got, tt.want)
+ t.Errorf("values=%v: got %q, want %q", tt.values, got, tt.want)
}
}
}
-func TestBuildRequestPayloadContainingMarkerChar(t *testing.T) {
+func TestBuildRequestValuesPayloadContainingMarkerChar(t *testing.T) {
// A payload that itself contains the marker character must not be
- // reinterpreted as a marker on a later buildRequest call - each call
- // re-splits the ORIGINAL template, not the previously built request.
+ // reinterpreted as a marker - buildRequestValues splits the ORIGINAL
+ // template, never the already-substituted result.
template := "GET /§1§/§2§ HTTP/1.1"
- got, err := buildRequest([]byte(template), 0, "§injected§")
+ got, err := buildRequestValues([]byte(template), []string{"§injected§", "2"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -105,6 +105,96 @@ func TestBuildRequestPayloadContainingMarkerChar(t *testing.T) {
}
}
+func TestIntrudeValuesSniper(t *testing.T) {
+ positions := []IntrudePosition{{Index: 0, Base: "a"}, {Index: 1, Base: "b"}}
+ got, err := intrudeValues(Sniper, positions, [][]string{{"1", "2"}})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ want := [][]string{
+ {"1", "b"}, {"2", "b"}, // position 0 fuzzed, position 1 at base
+ {"a", "1"}, {"a", "2"}, // position 1 fuzzed, position 0 at base
+ }
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("got %v, want %v", got, want)
+ }
+}
+
+func TestIntrudeValuesBatteringRam(t *testing.T) {
+ positions := []IntrudePosition{{Index: 0, Base: "a"}, {Index: 1, Base: "b"}}
+ got, err := intrudeValues(BatteringRam, positions, [][]string{{"1", "2"}})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ // Same payload lands in every position at once, unlike Sniper.
+ want := [][]string{{"1", "1"}, {"2", "2"}}
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("got %v, want %v", got, want)
+ }
+}
+
+func TestIntrudeValuesPitchfork(t *testing.T) {
+ positions := []IntrudePosition{{Index: 0, Base: "a"}, {Index: 1, Base: "b"}}
+ got, err := intrudeValues(Pitchfork, positions, [][]string{{"1", "2", "3"}, {"x", "y"}})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ // Walks both sets in lockstep; stops at the shorter set's length (2),
+ // silently ignoring "3" from the longer one - Burp's own convention.
+ want := [][]string{{"1", "x"}, {"2", "y"}}
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("got %v, want %v", got, want)
+ }
+}
+
+func TestIntrudeValuesPitchforkRequiresOneSetPerPosition(t *testing.T) {
+ positions := []IntrudePosition{{Index: 0, Base: "a"}, {Index: 1, Base: "b"}}
+ _, err := intrudeValues(Pitchfork, positions, [][]string{{"1", "2"}})
+ if err == nil {
+ t.Fatal("expected error for one payload set across two positions")
+ }
+}
+
+func TestIntrudeValuesClusterBomb(t *testing.T) {
+ positions := []IntrudePosition{{Index: 0, Base: "a"}, {Index: 1, Base: "b"}}
+ got, err := intrudeValues(ClusterBomb, positions, [][]string{{"1", "2"}, {"x", "y"}})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ // Every combination - the rightmost (last) position cycles fastest.
+ want := [][]string{{"1", "x"}, {"1", "y"}, {"2", "x"}, {"2", "y"}}
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("got %v, want %v", got, want)
+ }
+}
+
+func TestIntrudeValuesClusterBombOverCapRejected(t *testing.T) {
+ positions := []IntrudePosition{{Index: 0, Base: "a"}, {Index: 1, Base: "b"}, {Index: 2, Base: "c"}}
+ big := make([]string, 20)
+ for i := range big {
+ big[i] = fmt.Sprintf("v%d", i)
+ }
+ // 20 * 20 * 20 = 8000, comfortably over the 1000 cap.
+ _, err := intrudeValues(ClusterBomb, positions, [][]string{big, big, big})
+ if err == nil {
+ t.Fatal("expected the request-count cap to reject this attack")
+ }
+}
+
+func TestIntrudeValuesOverCapRejected(t *testing.T) {
+ positions := []IntrudePosition{{Index: 0, Base: "a"}}
+ big := make([]string, maxIntrudeRequests+1)
+ for i := range big {
+ big[i] = fmt.Sprintf("v%d", i)
+ }
+ if _, err := intrudeValues(Sniper, positions, [][]string{big}); err == nil {
+ t.Error("Sniper: expected the request-count cap to reject this attack")
+ }
+ if _, err := intrudeValues(BatteringRam, positions, [][]string{big}); err == nil {
+ t.Error("BatteringRam: expected the request-count cap to reject this attack")
+ }
+}
+
func TestIntrudeRequestCount(t *testing.T) {
positions, _, err := ParseMarkers([]byte("GET /§a§/§b§ HTTP/1.1"))
if err != nil {