From ad152e62e5124b673099a9103eb6e7f933771794 Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Sun, 18 Feb 2024 17:18:40 +0100 Subject: [PATCH] fix(chromium): better default deny list regexp --- Makefile | 10 +- pkg/gotenberg/flags.go | 8 +- pkg/gotenberg/regexp.go | 53 +++++ pkg/gotenberg/regexp_test.go | 83 +++++++ pkg/gotenberg/supervisor_test.go | 4 +- pkg/modules/chromium/browser.go | 23 +- pkg/modules/chromium/browser_test.go | 312 ++++++++++++------------- pkg/modules/chromium/chromium.go | 6 +- pkg/modules/chromium/events.go | 23 +- pkg/modules/chromium/routes.go | 2 +- pkg/modules/chromium/routes_test.go | 8 +- pkg/modules/webhook/middleware.go | 55 ++--- pkg/modules/webhook/middleware_test.go | 85 ++++--- pkg/modules/webhook/webhook.go | 10 +- 14 files changed, 419 insertions(+), 263 deletions(-) create mode 100644 pkg/gotenberg/regexp.go create mode 100644 pkg/gotenberg/regexp_test.go diff --git a/Makefile b/Makefile index 21b316c7..64ee4267 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,7 @@ CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES=false CHROMIUM_HOST_RESOLVER_RULES= CHROMIUM_PROXY_SERVER= CHROMIUM_ALLOW_LIST= -CHROMIUM_DENY_LIST="^file:///[^tmp].*" +CHROMIUM_DENY_LIST=^file:(?!//\/tmp/).* CHROMIUM_CLEAR_CACHE=false CHROMIUM_CLEAR_COOKIES=false CHROMIUM_DISABLE_JAVASCRIPT=false @@ -100,8 +100,8 @@ run: ## Start a Gotenberg container --chromium-allow-file-access-from-files=$(CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES) \ --chromium-host-resolver-rules=$(CHROMIUM_HOST_RESOLVER_RULES) \ --chromium-proxy-server=$(CHROMIUM_PROXY_SERVER) \ - --chromium-allow-list=$(CHROMIUM_ALLOW_LIST) \ - --chromium-deny-list=$(CHROMIUM_DENY_LIST) \ + --chromium-allow-list="$(CHROMIUM_ALLOW_LIST)" \ + --chromium-deny-list="$(CHROMIUM_DENY_LIST)" \ --chromium-clear-cache=$(CHROMIUM_CLEAR_CACHE) \ --chromium-clear-cookies=$(CHROMIUM_CLEAR_COOKIES) \ --chromium-disable-javascript=$(CHROMIUM_DISABLE_JAVASCRIPT) \ @@ -120,8 +120,8 @@ run: ## Start a Gotenberg container --prometheus-collect-interval=$(PROMETHEUS_COLLECT_INTERVAL) \ --prometheus-disable-route-logging=$(PROMETHEUS_DISABLE_ROUTE_LOGGING) \ --prometheus-disable-collect=$(PROMETHEUS_DISABLE_COLLECT) \ - --webhook-allow-list=$(WEBHOOK_ALLOW_LIST) \ - --webhook-deny-list=$(WEBHOOK_DENY_LIST) \ + --webhook-allow-list="$(WEBHOOK_ALLOW_LIST)" \ + --webhook-deny-list="$(WEBHOOK_DENY_LIST)" \ --webhook-error-allow-list=$(WEBHOOK_ERROR_ALLOW_LIST) \ --webhook-error-deny-list=$(WEBHOOK_ERROR_DENY_LIST) \ --webhook-max-retry=$(WEBHOOK_MAX_RETRY) \ diff --git a/pkg/gotenberg/flags.go b/pkg/gotenberg/flags.go index 80d17049..3cc83669 100644 --- a/pkg/gotenberg/flags.go +++ b/pkg/gotenberg/flags.go @@ -1,9 +1,9 @@ package gotenberg import ( - "regexp" "time" + "github.com/dlclark/regexp2" "github.com/labstack/gommon/bytes" flag "github.com/spf13/pflag" ) @@ -199,19 +199,19 @@ func (f *ParsedFlags) MustDeprecatedHumanReadableBytesString(deprecated string, // MustRegexp returns the regular expression of a flag given by name. // It panics if an error occurs. -func (f *ParsedFlags) MustRegexp(name string) *regexp.Regexp { +func (f *ParsedFlags) MustRegexp(name string) *regexp2.Regexp { val, err := f.GetString(name) if err != nil { panic(err) } - return regexp.MustCompile(val) + return regexp2.MustCompile(val, 0) } // MustDeprecatedRegexp returns the regular expression of a deprecated flag if // it was explicitly set or the regular expression of the new flag. // It panics if an error occurs. -func (f *ParsedFlags) MustDeprecatedRegexp(deprecated string, newName string) *regexp.Regexp { +func (f *ParsedFlags) MustDeprecatedRegexp(deprecated string, newName string) *regexp2.Regexp { if f.Changed(deprecated) { return f.MustRegexp(deprecated) } diff --git a/pkg/gotenberg/regexp.go b/pkg/gotenberg/regexp.go new file mode 100644 index 00000000..c627c08c --- /dev/null +++ b/pkg/gotenberg/regexp.go @@ -0,0 +1,53 @@ +package gotenberg + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/dlclark/regexp2" +) + +// ErrFiltered happens if a value is filtered by the [FilterDeadline] function. +var ErrFiltered = errors.New("value filtered") + +// FilterDeadline checks if given value is allowed and not denied according to +// regex patterns. It returns a [context.DeadlineExceeded] if it takes too long +// to process. +func FilterDeadline(allowed, denied *regexp2.Regexp, s string, deadline time.Time) error { + // FIXME: not ideal to compile everytime, but is there another way to create a clone? + if allowed.String() != "" { + allow := regexp2.MustCompile(allowed.String(), 0) + allow.MatchTimeout = time.Until(deadline) + + ok, err := allow.MatchString(s) + if err != nil { + if time.Now().After(deadline) { + return context.DeadlineExceeded + } + return fmt.Errorf("'%s' cannot handle '%s': %w", allow.String(), s, err) + } + if !ok { + return fmt.Errorf("'%s' does not match the expression from the allowed list: %w", s, ErrFiltered) + } + } + + if denied.String() != "" { + deny := regexp2.MustCompile(denied.String(), 0) + deny.MatchTimeout = time.Until(deadline) + + ok, err := deny.MatchString(s) + if err != nil { + if time.Now().After(deadline) { + return context.DeadlineExceeded + } + return fmt.Errorf("'%s' cannot handle '%s': %w", deny.String(), s, err) + } + if ok { + return fmt.Errorf("'%s' matches the expression from the denied list: %w", s, ErrFiltered) + } + } + + return nil +} diff --git a/pkg/gotenberg/regexp_test.go b/pkg/gotenberg/regexp_test.go new file mode 100644 index 00000000..45c38ade --- /dev/null +++ b/pkg/gotenberg/regexp_test.go @@ -0,0 +1,83 @@ +package gotenberg + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/dlclark/regexp2" +) + +func TestFilterDeadline(t *testing.T) { + for _, tc := range []struct { + scenario string + allowed *regexp2.Regexp + denied *regexp2.Regexp + s string + deadline time.Time + expectError bool + expectedError error + }{ + { + scenario: "DeadlineExceeded (allowed)", + allowed: regexp2.MustCompile("foo", 0), + denied: regexp2.MustCompile("", 0), + s: "foo", + deadline: time.Now().Add(time.Duration(-1) * time.Hour), + expectError: true, + expectedError: context.DeadlineExceeded, + }, + { + scenario: "ErrFiltered (allowed)", + allowed: regexp2.MustCompile("foo", 0), + denied: regexp2.MustCompile("", 0), + s: "bar", + deadline: time.Now().Add(time.Duration(5) * time.Second), + expectError: true, + expectedError: ErrFiltered, + }, + { + scenario: "DeadlineExceeded (denied)", + allowed: regexp2.MustCompile("", 0), + denied: regexp2.MustCompile("foo", 0), + s: "foo", + deadline: time.Now().Add(time.Duration(-1) * time.Hour), + expectError: true, + expectedError: context.DeadlineExceeded, + }, + { + scenario: "ErrFiltered (denied)", + allowed: regexp2.MustCompile("", 0), + denied: regexp2.MustCompile("foo", 0), + s: "foo", + deadline: time.Now().Add(time.Duration(5) * time.Second), + expectError: true, + expectedError: ErrFiltered, + }, + { + scenario: "success", + allowed: regexp2.MustCompile("", 0), + denied: regexp2.MustCompile("", 0), + s: "foo", + deadline: time.Now().Add(time.Duration(5) * time.Second), + expectError: false, + }, + } { + t.Run(tc.scenario, func(t *testing.T) { + err := FilterDeadline(tc.allowed, tc.denied, tc.s, tc.deadline) + + if tc.expectError && err == nil { + t.Fatal("expected an error but got none") + } + + if !tc.expectError && err != nil { + t.Fatalf("expected no error but got: %v", err) + } + + if tc.expectedError != nil && !errors.Is(err, tc.expectedError) { + t.Fatalf("expected error %v but got: %v", tc.expectedError, err) + } + }) + } +} diff --git a/pkg/gotenberg/supervisor_test.go b/pkg/gotenberg/supervisor_test.go index ef7eb4b6..10f381bc 100644 --- a/pkg/gotenberg/supervisor_test.go +++ b/pkg/gotenberg/supervisor_test.go @@ -487,10 +487,10 @@ func TestProcessSupervisor_runWithDeadline(t *testing.T) { ps := NewProcessSupervisor(zap.NewNop(), new(ProcessMock), 0, 0).(*processSupervisor) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - defer cancel() - if tc.ctxDone { cancel() + } else { + defer cancel() } err := ps.runWithDeadline(ctx, func() error { diff --git a/pkg/modules/chromium/browser.go b/pkg/modules/chromium/browser.go index fd2dd13b..bb23ae6f 100644 --- a/pkg/modules/chromium/browser.go +++ b/pkg/modules/chromium/browser.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "os" - "regexp" "strings" "sync" "sync/atomic" @@ -15,6 +14,7 @@ import ( "github.com/chromedp/cdproto/network" "github.com/chromedp/cdproto/runtime" "github.com/chromedp/chromedp" + "github.com/dlclark/regexp2" "go.uber.org/zap" "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" @@ -39,8 +39,8 @@ type browserArguments struct { wsUrlReadTimeout time.Duration // Tasks specific. - allowList *regexp.Regexp - denyList *regexp.Regexp + allowList *regexp2.Regexp + denyList *regexp2.Regexp clearCache bool clearCookies bool disableJavaScript bool @@ -263,20 +263,17 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string return errors.New("browser not started, cannot handle tasks") } - // We validate the "main" URL against our allow / deny lists. - if !b.arguments.allowList.MatchString(url) { - return fmt.Errorf("'%s' does not match the expression from the allowed list: %w", url, ErrUrlNotAuthorized) - } - - if b.arguments.denyList.String() != "" && b.arguments.denyList.MatchString(url) { - return fmt.Errorf("'%s' matches the expression from the denied list: %w", url, ErrUrlNotAuthorized) - } - deadline, ok := ctx.Deadline() if !ok { return errors.New("context has no deadline") } + // We validate the "main" URL against our allow / deny lists. + err := gotenberg.FilterDeadline(b.arguments.allowList, b.arguments.denyList, url, deadline) + if err != nil { + return fmt.Errorf("filter URL: %w", err) + } + b.ctxMu.RLock() defer b.ctxMu.RUnlock() @@ -310,7 +307,7 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string listenForEventExceptionThrown(taskCtx, logger, &consoleExceptions, &consoleExceptionsMu) } - err := chromedp.Run(taskCtx, tasks...) + err = chromedp.Run(taskCtx, tasks...) if err != nil { errMessage := err.Error() diff --git a/pkg/modules/chromium/browser_test.go b/pkg/modules/chromium/browser_test.go index db2cd189..4a4bc540 100644 --- a/pkg/modules/chromium/browser_test.go +++ b/pkg/modules/chromium/browser_test.go @@ -5,11 +5,11 @@ import ( "errors" "fmt" "os" - "regexp" "strings" "testing" "time" + "github.com/dlclark/regexp2" "github.com/google/uuid" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -268,46 +268,9 @@ func TestChromiumBrowser_pdf(t *testing.T) { expectError: true, }, { - scenario: "ErrUrlNotAuthorized: main URL does not match the allowed list", + scenario: "context has no deadline", browser: func() browser { b := new(chromiumBrowser) - b.arguments = browserArguments{ - allowList: regexp.MustCompile("^file:///[^tmp].*"), - } - b.isStarted.Store(true) - return b - }(), - fs: gotenberg.NewFileSystem(), - noDeadline: false, - start: false, - expectError: true, - expectedError: ErrUrlNotAuthorized, - }, - { - scenario: "ErrUrlNotAuthorized: main URL does match the denied list", - browser: func() browser { - b := new(chromiumBrowser) - b.arguments = browserArguments{ - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile("^file:///tmp.*"), - } - b.isStarted.Store(true) - return b - }(), - fs: gotenberg.NewFileSystem(), - noDeadline: false, - start: false, - expectError: true, - expectedError: ErrUrlNotAuthorized, - }, - { - scenario: "ErrUrlNotAuthorized: main URL does match the denied list", - browser: func() browser { - b := new(chromiumBrowser) - b.arguments = browserArguments{ - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), - } b.isStarted.Store(true) return b }(), @@ -316,14 +279,48 @@ func TestChromiumBrowser_pdf(t *testing.T) { start: false, expectError: true, }, + { + scenario: "ErrFiltered: main URL does not match the allowed list", + browser: func() browser { + b := new(chromiumBrowser) + b.arguments = browserArguments{ + allowList: regexp2.MustCompile(`^file:(?!//\/tmp/).*`, 0), + denyList: regexp2.MustCompile("", 0), + } + b.isStarted.Store(true) + return b + }(), + fs: gotenberg.NewFileSystem(), + noDeadline: false, + start: false, + expectError: true, + expectedError: gotenberg.ErrFiltered, + }, + { + scenario: "ErrFiltered: main URL does match the denied list", + browser: func() browser { + b := new(chromiumBrowser) + b.arguments = browserArguments{ + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("^file:///tmp.*", 0), + } + b.isStarted.Store(true) + return b + }(), + fs: gotenberg.NewFileSystem(), + noDeadline: false, + start: false, + expectError: true, + expectedError: gotenberg.ErrFiltered, + }, { scenario: "a request does not match the allowed list", browser: newChromiumBrowser( browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile("^file:///tmp.*"), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("^file:///tmp.*", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -354,8 +351,8 @@ func TestChromiumBrowser_pdf(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile("^file:///[^tmp].*"), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile(`^file:(?!//\/tmp/).*`, 0), }, ), fs: func() *gotenberg.FileSystem { @@ -386,8 +383,8 @@ func TestChromiumBrowser_pdf(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -421,8 +418,8 @@ func TestChromiumBrowser_pdf(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -454,8 +451,8 @@ func TestChromiumBrowser_pdf(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -487,8 +484,8 @@ func TestChromiumBrowser_pdf(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), clearCache: true, }, ), @@ -520,8 +517,8 @@ func TestChromiumBrowser_pdf(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), clearCookies: true, }, ), @@ -553,8 +550,8 @@ func TestChromiumBrowser_pdf(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), disableJavaScript: true, }, ), @@ -588,8 +585,8 @@ func TestChromiumBrowser_pdf(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -625,8 +622,8 @@ func TestChromiumBrowser_pdf(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -658,8 +655,8 @@ func TestChromiumBrowser_pdf(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -694,8 +691,8 @@ func TestChromiumBrowser_pdf(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -727,8 +724,8 @@ func TestChromiumBrowser_pdf(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -762,8 +759,8 @@ func TestChromiumBrowser_pdf(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -797,8 +794,8 @@ func TestChromiumBrowser_pdf(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -832,8 +829,8 @@ func TestChromiumBrowser_pdf(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -878,8 +875,8 @@ func TestChromiumBrowser_pdf(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -913,8 +910,8 @@ func TestChromiumBrowser_pdf(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -959,8 +956,8 @@ func TestChromiumBrowser_pdf(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -995,8 +992,8 @@ func TestChromiumBrowser_pdf(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -1033,8 +1030,8 @@ func TestChromiumBrowser_pdf(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -1066,8 +1063,8 @@ func TestChromiumBrowser_pdf(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -1202,45 +1199,12 @@ func TestChromiumBrowser_screenshot(t *testing.T) { expectError: true, }, { - scenario: "ErrUrlNotAuthorized: main URL does not match the allowed list", + scenario: "context has not deadline", browser: func() browser { b := new(chromiumBrowser) b.arguments = browserArguments{ - allowList: regexp.MustCompile("^file:///[^tmp].*"), - } - b.isStarted.Store(true) - return b - }(), - fs: gotenberg.NewFileSystem(), - noDeadline: false, - start: false, - expectError: true, - expectedError: ErrUrlNotAuthorized, - }, - { - scenario: "ErrUrlNotAuthorized: main URL does match the denied list", - browser: func() browser { - b := new(chromiumBrowser) - b.arguments = browserArguments{ - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile("^file:///tmp.*"), - } - b.isStarted.Store(true) - return b - }(), - fs: gotenberg.NewFileSystem(), - noDeadline: false, - start: false, - expectError: true, - expectedError: ErrUrlNotAuthorized, - }, - { - scenario: "ErrUrlNotAuthorized: main URL does match the denied list", - browser: func() browser { - b := new(chromiumBrowser) - b.arguments = browserArguments{ - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), } b.isStarted.Store(true) return b @@ -1250,14 +1214,48 @@ func TestChromiumBrowser_screenshot(t *testing.T) { start: false, expectError: true, }, + { + scenario: "ErrFiltered: main URL does not match the allowed list", + browser: func() browser { + b := new(chromiumBrowser) + b.arguments = browserArguments{ + allowList: regexp2.MustCompile(`^file:(?!//\/tmp/).*`, 0), + denyList: regexp2.MustCompile("", 0), + } + b.isStarted.Store(true) + return b + }(), + fs: gotenberg.NewFileSystem(), + noDeadline: false, + start: false, + expectError: true, + expectedError: gotenberg.ErrFiltered, + }, + { + scenario: "ErrFiltered: main URL does match the denied list", + browser: func() browser { + b := new(chromiumBrowser) + b.arguments = browserArguments{ + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("^file:///tmp.*", 0), + } + b.isStarted.Store(true) + return b + }(), + fs: gotenberg.NewFileSystem(), + noDeadline: false, + start: false, + expectError: true, + expectedError: gotenberg.ErrFiltered, + }, { scenario: "a request does not match the allowed list", browser: newChromiumBrowser( browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile("^file:///tmp.*"), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("^file:///tmp.*", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -1288,8 +1286,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile("^file:///[^tmp].*"), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile(`^file:(?!//\/tmp/).*`, 0), }, ), fs: func() *gotenberg.FileSystem { @@ -1320,8 +1318,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -1355,8 +1353,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -1388,8 +1386,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -1421,8 +1419,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), clearCache: true, }, ), @@ -1454,8 +1452,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), clearCookies: true, }, ), @@ -1487,8 +1485,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), disableJavaScript: true, }, ), @@ -1522,8 +1520,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -1559,8 +1557,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -1594,8 +1592,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -1627,8 +1625,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -1662,8 +1660,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -1697,8 +1695,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -1732,8 +1730,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -1778,8 +1776,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -1813,8 +1811,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -1859,8 +1857,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { @@ -1900,8 +1898,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) { browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), }, ), fs: func() *gotenberg.FileSystem { diff --git a/pkg/modules/chromium/chromium.go b/pkg/modules/chromium/chromium.go index 4e73410b..ea5706e0 100644 --- a/pkg/modules/chromium/chromium.go +++ b/pkg/modules/chromium/chromium.go @@ -20,10 +20,6 @@ func init() { } var ( - // ErrUrlNotAuthorized happens if a URL is not acceptable according to the - // allowed/denied lists. - ErrUrlNotAuthorized = errors.New("URL not authorized") - // ErrInvalidEmulatedMediaType happens if the emulated media type is not // "screen" nor "print". Empty value are allowed though. ErrInvalidEmulatedMediaType = errors.New("invalid emulated media type") @@ -291,7 +287,7 @@ func (mod *Chromium) Descriptor() gotenberg.ModuleDescriptor { fs.String("chromium-host-resolver-rules", "", "Set custom mappings to the host resolver") fs.String("chromium-proxy-server", "", "Set the outbound proxy server; this switch only affects HTTP and HTTPS requests") fs.String("chromium-allow-list", "", "Set the allowed URLs for Chromium using a regular expression") - fs.String("chromium-deny-list", "^file:///[^tmp].*", "Set the denied URLs for Chromium using a regular expression") + fs.String("chromium-deny-list", `^file:(?!//\/tmp/).*`, "Set the denied URLs for Chromium using a regular expression") fs.Bool("chromium-clear-cache", false, "Clear Chromium cache between each conversion") fs.Bool("chromium-clear-cookies", false, "Clear Chromium cookies between each conversion") fs.Bool("chromium-disable-javascript", false, "Disable JavaScript") diff --git a/pkg/modules/chromium/events.go b/pkg/modules/chromium/events.go index e5128c34..a3297746 100644 --- a/pkg/modules/chromium/events.go +++ b/pkg/modules/chromium/events.go @@ -3,7 +3,6 @@ package chromium import ( "context" "fmt" - "regexp" "slices" "sync" @@ -13,14 +12,17 @@ import ( "github.com/chromedp/cdproto/page" "github.com/chromedp/cdproto/runtime" "github.com/chromedp/chromedp" + "github.com/dlclark/regexp2" "go.uber.org/multierr" "go.uber.org/zap" "golang.org/x/sync/errgroup" + + "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" ) // listenForEventRequestPaused listens for requests to check if they are // allowed or not. -func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowList *regexp.Regexp, denyList *regexp.Regexp) { +func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowList *regexp2.Regexp, denyList *regexp2.Regexp) { chromedp.ListenTarget(ctx, func(ev interface{}) { switch e := ev.(type) { case *fetch.EventRequestPaused: @@ -28,13 +30,15 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowL logger.Debug(fmt.Sprintf("event EventRequestPaused fired for '%s'", e.Request.URL)) allow := true - if !allowList.MatchString(e.Request.URL) { - logger.Warn(fmt.Sprintf("'%s' does not match the expression from the allowed list", e.Request.URL)) - allow = false + deadline, ok := ctx.Deadline() + if !ok { + logger.Error("context has no deadline, cannot filter URL") + return } - if denyList.String() != "" && denyList.MatchString(e.Request.URL) { - logger.Warn(fmt.Sprintf("'%s' matches the expression from the denied list", e.Request.URL)) + err := gotenberg.FilterDeadline(allowList, denyList, e.Request.URL, deadline) + if err != nil { + logger.Warn(err.Error()) allow = false } @@ -43,16 +47,15 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowL if allow { req := fetch.ContinueRequest(e.RequestID) - err := req.Do(executorCtx) + err = req.Do(executorCtx) if err != nil { logger.Error(fmt.Sprintf("continue request: %s", err)) } - return } req := fetch.FailRequest(e.RequestID, network.ErrorReasonAccessDenied) - err := req.Do(executorCtx) + err = req.Do(executorCtx) if err != nil { logger.Error(fmt.Sprintf("fail request: %s", err)) } diff --git a/pkg/modules/chromium/routes.go b/pkg/modules/chromium/routes.go index e9d09344..8859b9ab 100644 --- a/pkg/modules/chromium/routes.go +++ b/pkg/modules/chromium/routes.go @@ -621,7 +621,7 @@ func handleChromiumError(err error, url string, options Options) error { ) } - if errors.Is(err, ErrUrlNotAuthorized) { + if errors.Is(err, gotenberg.ErrFiltered) { return api.WrapError( err, api.NewSentinelHttpError( diff --git a/pkg/modules/chromium/routes_test.go b/pkg/modules/chromium/routes_test.go index bb5d7fe1..269944f0 100644 --- a/pkg/modules/chromium/routes_test.go +++ b/pkg/modules/chromium/routes_test.go @@ -1243,10 +1243,10 @@ func TestConvertUrl(t *testing.T) { expectOutputPathsCount: 0, }, { - scenario: "ErrUrlNotAuthorized", + scenario: "ErrFiltered", ctx: &api.ContextMock{Context: new(api.Context)}, api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return ErrUrlNotAuthorized + return gotenberg.ErrFiltered }}, options: DefaultPdfOptions(), expectError: true, @@ -1503,10 +1503,10 @@ func TestScreenshotUrl(t *testing.T) { expectOutputPathsCount: 0, }, { - scenario: "ErrUrlNotAuthorized", + scenario: "ErrFiltered", ctx: &api.ContextMock{Context: new(api.Context)}, api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { - return ErrUrlNotAuthorized + return gotenberg.ErrFiltered }}, options: DefaultScreenshotOptions(), expectError: true, diff --git a/pkg/modules/webhook/middleware.go b/pkg/modules/webhook/middleware.go index d0540bf3..29ffc78b 100644 --- a/pkg/modules/webhook/middleware.go +++ b/pkg/modules/webhook/middleware.go @@ -9,14 +9,15 @@ import ( "fmt" "net/http" "os" - "regexp" "strconv" "strings" "time" + "github.com/dlclark/regexp2" "github.com/hashicorp/go-retryablehttp" "github.com/labstack/echo/v4" + "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" "github.com/gotenberg/gotenberg/v8/pkg/modules/api" ) @@ -44,38 +45,38 @@ func webhookMiddleware(w *Webhook) api.Middleware { ) } - // Let's check if the webhook URLs are acceptable according to our - // allowed/denied lists. - filter := func(URL, header string, allowList, denyList *regexp.Regexp) error { - if !allowList.MatchString(URL) { - return api.WrapError( - fmt.Errorf("'%s' does not match the expression from the allowed list", URL), - api.NewSentinelHttpError( - http.StatusForbidden, - fmt.Sprintf("Invalid '%s' header value: '%s' does not match the authorized URLs", header, URL), - ), - ) - } - - if denyList.String() != "" && denyList.MatchString(URL) { - return api.WrapError( - fmt.Errorf("'%s' matches the expression from the denied list", URL), - api.NewSentinelHttpError( - http.StatusForbidden, - fmt.Sprintf("Invalid '%s' header value: '%s' does not match the authorized URLs", header, URL), - ), - ) - } - - return nil + deadline, ok := ctx.Deadline() + if !ok { + return errors.New("context has no deadline") } - err := filter(webhookUrl, "Gotenberg-Webhook-Url", w.allowList, w.denyList) + // Let's check if the webhook URLs are acceptable according to our + // allowed/denied lists. + filter := func(url, header string, allowList, denyList *regexp2.Regexp, deadline time.Time) error { + err := gotenberg.FilterDeadline(allowList, denyList, url, deadline) + if err == nil { + return nil + } + + if errors.Is(err, gotenberg.ErrFiltered) { + return api.WrapError( + err, + api.NewSentinelHttpError( + http.StatusForbidden, + fmt.Sprintf("Invalid '%s' header value: '%s' does not match the authorized URL", header, url), + ), + ) + } + + return err + } + + err := filter(webhookUrl, "Gotenberg-Webhook-Url", w.allowList, w.denyList, deadline) if err != nil { return fmt.Errorf("filter webhook URL: %w", err) } - err = filter(webhookErrorUrl, "Gotenberg-Webhook-Error-Url", w.errorAllowList, w.errorDenyList) + err = filter(webhookErrorUrl, "Gotenberg-Webhook-Error-Url", w.errorAllowList, w.errorDenyList, deadline) if err != nil { return fmt.Errorf("filter webhook error URL: %w", err) } diff --git a/pkg/modules/webhook/middleware_test.go b/pkg/modules/webhook/middleware_test.go index 664a4d31..89e13739 100644 --- a/pkg/modules/webhook/middleware_test.go +++ b/pkg/modules/webhook/middleware_test.go @@ -11,11 +11,11 @@ import ( "mime/multipart" "net/http" "net/http/httptest" - "regexp" "strings" "testing" "time" + "github.com/dlclark/regexp2" "github.com/labstack/echo/v4" "go.uber.org/zap" @@ -47,10 +47,10 @@ func TestWebhookMiddlewareGuards(t *testing.T) { buildWebhookModule := func() *Webhook { return &Webhook{ - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), - errorAllowList: regexp.MustCompile(""), - errorDenyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), + errorAllowList: regexp2.MustCompile("", 0), + errorDenyList: regexp2.MustCompile("", 0), maxRetry: 0, retryMinWait: 0, retryMaxWait: 0, @@ -63,6 +63,7 @@ func TestWebhookMiddlewareGuards(t *testing.T) { request *http.Request mod *Webhook next echo.HandlerFunc + noDeadline bool expectError bool expectHttpError bool expectHttpStatus int @@ -76,6 +77,7 @@ func TestWebhookMiddlewareGuards(t *testing.T) { return nil } }(), + noDeadline: false, expectError: false, expectHttpError: false, }, @@ -87,10 +89,23 @@ func TestWebhookMiddlewareGuards(t *testing.T) { return req }(), mod: buildWebhookModule(), + noDeadline: false, expectError: true, expectHttpError: true, expectHttpStatus: http.StatusBadRequest, }, + { + scenario: "context has no deadline", + request: func() *http.Request { + req := buildMultipartFormDataRequest() + req.Header.Set("Gotenberg-Webhook-Url", "foo") + req.Header.Set("Gotenberg-Webhook-Error-Url", "bar") + return req + }(), + mod: buildWebhookModule(), + noDeadline: true, + expectError: true, + }, { scenario: "webhook URL is not allowed", request: func() *http.Request { @@ -101,9 +116,10 @@ func TestWebhookMiddlewareGuards(t *testing.T) { }(), mod: func() *Webhook { mod := buildWebhookModule() - mod.allowList = regexp.MustCompile("bar") + mod.allowList = regexp2.MustCompile("bar", 0) return mod }(), + noDeadline: false, expectError: true, expectHttpError: true, expectHttpStatus: http.StatusForbidden, @@ -118,9 +134,10 @@ func TestWebhookMiddlewareGuards(t *testing.T) { }(), mod: func() *Webhook { mod := buildWebhookModule() - mod.denyList = regexp.MustCompile("foo") + mod.denyList = regexp2.MustCompile("foo", 0) return mod }(), + noDeadline: false, expectError: true, expectHttpError: true, expectHttpStatus: http.StatusForbidden, @@ -135,9 +152,10 @@ func TestWebhookMiddlewareGuards(t *testing.T) { }(), mod: func() *Webhook { mod := buildWebhookModule() - mod.errorAllowList = regexp.MustCompile("foo") + mod.errorAllowList = regexp2.MustCompile("foo", 0) return mod }(), + noDeadline: false, expectError: true, expectHttpError: true, expectHttpStatus: http.StatusForbidden, @@ -152,9 +170,10 @@ func TestWebhookMiddlewareGuards(t *testing.T) { }(), mod: func() *Webhook { mod := buildWebhookModule() - mod.errorDenyList = regexp.MustCompile("bar") + mod.errorDenyList = regexp2.MustCompile("bar", 0) return mod }(), + noDeadline: false, expectError: true, expectHttpError: true, expectHttpStatus: http.StatusForbidden, @@ -169,6 +188,7 @@ func TestWebhookMiddlewareGuards(t *testing.T) { return req }(), mod: buildWebhookModule(), + noDeadline: false, expectError: true, expectHttpError: true, expectHttpStatus: http.StatusBadRequest, @@ -183,6 +203,7 @@ func TestWebhookMiddlewareGuards(t *testing.T) { return req }(), mod: buildWebhookModule(), + noDeadline: false, expectError: true, expectHttpError: true, expectHttpStatus: http.StatusBadRequest, @@ -198,6 +219,7 @@ func TestWebhookMiddlewareGuards(t *testing.T) { return req }(), mod: buildWebhookModule(), + noDeadline: false, expectError: true, expectHttpError: true, expectHttpStatus: http.StatusBadRequest, @@ -213,6 +235,7 @@ func TestWebhookMiddlewareGuards(t *testing.T) { return req }(), mod: buildWebhookModule(), + noDeadline: false, expectError: true, expectHttpError: true, expectHttpStatus: http.StatusBadRequest, @@ -228,6 +251,7 @@ func TestWebhookMiddlewareGuards(t *testing.T) { return req }(), mod: buildWebhookModule(), + noDeadline: false, expectError: true, expectHttpError: true, expectHttpStatus: http.StatusBadRequest, @@ -242,6 +266,7 @@ func TestWebhookMiddlewareGuards(t *testing.T) { return req }(), mod: buildWebhookModule(), + noDeadline: false, expectError: true, expectHttpError: true, expectHttpStatus: http.StatusBadRequest, @@ -254,15 +279,20 @@ func TestWebhookMiddlewareGuards(t *testing.T) { c := srv.NewContext(tc.request, httptest.NewRecorder()) - ctx := &api.ContextMock{Context: &api.Context{}} - ctx.SetEchoContext(c) - - c.Set("context", ctx.Context) - c.Set("cancel", func() context.CancelFunc { - return func() { - return - } - }()) + if tc.noDeadline { + ctx := &api.ContextMock{Context: &api.Context{Context: context.Background()}} + ctx.SetEchoContext(c) + c.Set("context", ctx.Context) + c.Set("cancel", func() context.CancelFunc { + return nil + }()) + } else { + timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Second) + ctx := &api.ContextMock{Context: &api.Context{Context: timeoutCtx}} + ctx.SetEchoContext(c) + c.Set("context", ctx.Context) + c.Set("cancel", cancel) + } err := webhookMiddleware(tc.mod).Handler(tc.next)(c) @@ -320,10 +350,10 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) { buildWebhookModule := func() *Webhook { return &Webhook{ - allowList: regexp.MustCompile(""), - denyList: regexp.MustCompile(""), - errorAllowList: regexp.MustCompile(""), - errorDenyList: regexp.MustCompile(""), + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), + errorAllowList: regexp2.MustCompile("", 0), + errorDenyList: regexp2.MustCompile("", 0), maxRetry: 0, retryMinWait: 0, retryMaxWait: 0, @@ -426,22 +456,17 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) { c.Set("trace", "foo") c.Set("startTime", time.Now()) - ctx := &api.ContextMock{Context: &api.Context{}} + timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Second) + ctx := &api.ContextMock{Context: &api.Context{Context: timeoutCtx}} ctx.SetLogger(zap.NewNop()) ctx.SetEchoContext(c) c.Set("context", ctx.Context) - c.Set("cancel", func() context.CancelFunc { - return func() { - return - } - }()) + c.Set("cancel", cancel) webhook := echo.New() webhook.HideBanner = true webhook.HidePort = true - - rand.Seed(time.Now().UnixNano()) webhookPort := rand.Intn(65535-1025+1) + 1025 c.Request().Header.Set("Gotenberg-Webhook-Url", fmt.Sprintf("http://localhost:%d/", webhookPort)) diff --git a/pkg/modules/webhook/webhook.go b/pkg/modules/webhook/webhook.go index 7476bd5b..86612209 100644 --- a/pkg/modules/webhook/webhook.go +++ b/pkg/modules/webhook/webhook.go @@ -1,9 +1,9 @@ package webhook import ( - "regexp" "time" + "github.com/dlclark/regexp2" flag "github.com/spf13/pflag" "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" @@ -17,10 +17,10 @@ func init() { // Webhook is a module which provides a middleware for uploading output files // to any destinations in an asynchronous fashion. type Webhook struct { - allowList *regexp.Regexp - denyList *regexp.Regexp - errorAllowList *regexp.Regexp - errorDenyList *regexp.Regexp + allowList *regexp2.Regexp + denyList *regexp2.Regexp + errorAllowList *regexp2.Regexp + errorDenyList *regexp2.Regexp maxRetry int retryMinWait time.Duration retryMaxWait time.Duration