mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 00:22:14 +01:00
123 lines
3.2 KiB
Go
123 lines
3.2 KiB
Go
package chromium
|
|
|
|
import (
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/dlclark/regexp2"
|
|
)
|
|
|
|
func TestScopeMatchBudget(t *testing.T) {
|
|
t.Run("allows matching while credit remains", func(t *testing.T) {
|
|
b := newScopeMatchBudget(time.Second)
|
|
if !b.tryAcquire() {
|
|
t.Fatal("tryAcquire() = false on a fresh budget, want true")
|
|
}
|
|
})
|
|
|
|
t.Run("denies matching once exhausted", func(t *testing.T) {
|
|
b := newScopeMatchBudget(time.Second)
|
|
b.consume(time.Second)
|
|
if b.tryAcquire() {
|
|
t.Error("tryAcquire() = true after the budget was spent, want false")
|
|
}
|
|
})
|
|
|
|
t.Run("saturates at zero instead of wrapping into credit", func(t *testing.T) {
|
|
b := newScopeMatchBudget(time.Second)
|
|
b.consume(time.Hour)
|
|
if got := b.remaining.Load(); got != 0 {
|
|
t.Errorf("remaining = %d, want 0", got)
|
|
}
|
|
if b.tryAcquire() {
|
|
t.Error("tryAcquire() = true after an overlong match, want false")
|
|
}
|
|
})
|
|
|
|
t.Run("a spent budget stays spent", func(t *testing.T) {
|
|
b := newScopeMatchBudget(time.Second)
|
|
b.consume(time.Second)
|
|
b.consume(time.Millisecond)
|
|
if got := b.remaining.Load(); got != 0 {
|
|
t.Errorf("remaining = %d, want 0", got)
|
|
}
|
|
})
|
|
|
|
t.Run("is safe for concurrent use", func(t *testing.T) {
|
|
const goroutines = 64
|
|
// Each goroutine spends 1ms against a budget of half that many
|
|
// milliseconds, so the total spend overshoots it.
|
|
b := newScopeMatchBudget(time.Duration(goroutines/2) * time.Millisecond)
|
|
|
|
var wg sync.WaitGroup
|
|
for range goroutines {
|
|
wg.Go(func() {
|
|
b.tryAcquire()
|
|
b.consume(time.Millisecond)
|
|
})
|
|
}
|
|
wg.Wait()
|
|
|
|
if got := b.remaining.Load(); got != 0 {
|
|
t.Errorf("remaining = %d, want 0", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestScopeMatchBudget_BoundsCatastrophicBacktracking is the regression test for
|
|
// the amplification: many scoped headers matched against a hostile URL must cost
|
|
// the budget, not a multiple of it.
|
|
// See https://github.com/gotenberg/gotenberg/issues/1588.
|
|
func TestScopeMatchBudget_BoundsCatastrophicBacktracking(t *testing.T) {
|
|
const (
|
|
headers = 16
|
|
budget = 200 * time.Millisecond
|
|
)
|
|
|
|
// Nested quantifier with no possible match: classic catastrophic
|
|
// backtracking.
|
|
pattern := compileScopePattern(t, `(a+)+b`)
|
|
url := "http://example.com/" + strings.Repeat("a", 40)
|
|
|
|
b := newScopeMatchBudget(budget)
|
|
|
|
start := time.Now()
|
|
var matched int
|
|
for range headers {
|
|
if !b.tryAcquire() {
|
|
break
|
|
}
|
|
matchStart := time.Now()
|
|
_, _ = pattern.MatchString(url)
|
|
b.consume(time.Since(matchStart))
|
|
matched++
|
|
}
|
|
elapsed := time.Since(start)
|
|
|
|
if matched == headers {
|
|
t.Errorf("all %d headers were matched, want the budget to stop matching early", headers)
|
|
}
|
|
|
|
// Each match is separately capped at extraHttpHeaderScopeMatchTimeout, so
|
|
// the worst case is the budget plus one final match that started with the
|
|
// last of the credit. Generous slack keeps this stable on a loaded CI box.
|
|
ceiling := budget + extraHttpHeaderScopeMatchTimeout + time.Second
|
|
if elapsed > ceiling {
|
|
t.Errorf("matching took %s, want at most %s", elapsed, ceiling)
|
|
}
|
|
}
|
|
|
|
func compileScopePattern(t *testing.T, pattern string) *regexp2.Regexp {
|
|
t.Helper()
|
|
|
|
p, err := regexp2.Compile(pattern, regexp2.None)
|
|
if err != nil {
|
|
t.Fatalf("compile %q: %v", pattern, err)
|
|
}
|
|
p.MatchTimeout = extraHttpHeaderScopeMatchTimeout
|
|
|
|
return p
|
|
}
|