Compare commits

..

16 Commits

Author SHA1 Message Date
Julien Neuhart
25ce0b9aee fix(pdfengines): only preserve filenames in zip archive 2024-02-18 18:27:34 +01:00
Julien Neuhart
376a7db545 feat(pdfengines): preserve filenames in convert route 2024-02-18 18:17:12 +01:00
Julien Neuhart
f906ed741d fix(flags): typos 2024-02-18 17:52:51 +01:00
Julien Neuhart
ad152e62e5 fix(chromium): better default deny list regexp 2024-02-18 17:18:40 +01:00
Julien Neuhart
7f3a61ef43 chore: update dependencies 2024-02-18 17:18:08 +01:00
Julien Neuhart
a7ba8c8213 chore(testdata): add more data 2024-02-18 13:13:01 +01:00
Julien Neuhart
b1f3d6d4c0 feat: handling more ErrMaximumQueueSizeExceeded scenarios 2024-02-18 13:05:48 +01:00
Julien Neuhart
5707ae027f fix(libreoffice): expect output paths tests 2024-02-18 12:00:37 +01:00
timgrohmann
770208024d feat: enable max queue size for chromium and libreoffice
fix gotenberg/gotenberg#463
2024-02-18 11:35:49 +01:00
Julien Neuhart
975a9f5344 fix(libreoffice): wrong archive filename 2024-02-16 19:50:17 +01:00
Julien Neuhart
a45008411e chore: update dependencies 2024-02-16 19:50:17 +01:00
Julien Neuhart
9091ffd1b3 fix(libreoffice): keep original extension all the time for preserved filenames in zipped output 2024-02-16 19:50:17 +01:00
Chris Hughes
089f161d1e feat(libreoffice): preserve filenames for zipped files instead of using randomly generated UUID filenames (#793)
* feat(libreoffice): preserve filenames for zipped files instead of using generated UUID filenames

* chore: move github.com/google/uuid back to its original line

* fix: add libreoffice/routes.go to PR
2024-02-16 19:50:17 +01:00
dependabot[bot]
e6d7131701 chore(deps): bump golangci/golangci-lint-action from 3 to 4
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3 to 4.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-12 08:12:05 +01:00
Julien Neuhart
ed82170c2c chore(testdata): switch to px instead of rem in header.html & footer.html 2024-02-11 11:38:56 +01:00
Julien Neuhart
3472d42cdd chore: update dependencies 2024-02-07 10:44:30 +01:00
31 changed files with 954 additions and 364 deletions

View File

@@ -22,9 +22,9 @@ jobs:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Run linters - name: Run linters
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v4
with: with:
version: v1.54.2 version: v1.55.2
tests: tests:
needs: needs:

View File

@@ -10,9 +10,9 @@ DOCKER_REPOSITORY=gotenberg
GOTENBERG_VERSION=snapshot GOTENBERG_VERSION=snapshot
GOTENBERG_USER_GID=1001 GOTENBERG_USER_GID=1001
GOTENBERG_USER_UID=1001 GOTENBERG_USER_UID=1001
NOTO_COLOR_EMOJI_VERSION=v2.040 # See https://github.com/googlefonts/noto-emoji/releases. NOTO_COLOR_EMOJI_VERSION=v2.042 # See https://github.com/googlefonts/noto-emoji/releases.
PDFTK_VERSION=v3.3.3 # See https://gitlab.com/pdftk-java/pdftk/-/releases - Binary package. PDFTK_VERSION=v3.3.3 # See https://gitlab.com/pdftk-java/pdftk/-/releases - Binary package.
GOLANGCI_LINT_VERSION=v1.54.2 # See https://github.com/golangci/golangci-lint/releases. GOLANGCI_LINT_VERSION=v1.55.2 # See https://github.com/golangci/golangci-lint/releases.
.PHONY: build .PHONY: build
build: ## Build the Gotenberg's Docker image build: ## Build the Gotenberg's Docker image
@@ -35,6 +35,7 @@ API_ROOT_PATH=/
API_TRACE_HEADER=Gotenberg-Trace API_TRACE_HEADER=Gotenberg-Trace
API_DISABLE_HEALTH_CHECK_LOGGING=false API_DISABLE_HEALTH_CHECK_LOGGING=false
CHROMIUM_RESTART_AFTER=0 CHROMIUM_RESTART_AFTER=0
CHROMIUM_MAX_QUEUE_SIZE=0
CHROMIUM_AUTO_START=false CHROMIUM_AUTO_START=false
CHROMIUM_START_TIMEOUT=20s CHROMIUM_START_TIMEOUT=20s
CHROMIUM_INCOGNITO=false CHROMIUM_INCOGNITO=false
@@ -45,12 +46,13 @@ CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES=false
CHROMIUM_HOST_RESOLVER_RULES= CHROMIUM_HOST_RESOLVER_RULES=
CHROMIUM_PROXY_SERVER= CHROMIUM_PROXY_SERVER=
CHROMIUM_ALLOW_LIST= CHROMIUM_ALLOW_LIST=
CHROMIUM_DENY_LIST="^file:///[^tmp].*" CHROMIUM_DENY_LIST=^file:(?!//\/tmp/).*
CHROMIUM_CLEAR_CACHE=false CHROMIUM_CLEAR_CACHE=false
CHROMIUM_CLEAR_COOKIES=false CHROMIUM_CLEAR_COOKIES=false
CHROMIUM_DISABLE_JAVASCRIPT=false CHROMIUM_DISABLE_JAVASCRIPT=false
CHROMIUM_DISABLE_ROUTES=false CHROMIUM_DISABLE_ROUTES=false
LIBREOFFICE_RESTART_AFTER=10 LIBREOFFICE_RESTART_AFTER=10
LIBREOFFICE_MAX_QUEUE_SIZE=0
LIBREOFFICE_AUTO_START=false LIBREOFFICE_AUTO_START=false
LIBREOFFICE_START_TIMEOUT=20s LIBREOFFICE_START_TIMEOUT=20s
LIBREOFFICE_DISABLE_ROUTES=false LIBREOFFICE_DISABLE_ROUTES=false
@@ -89,6 +91,7 @@ run: ## Start a Gotenberg container
--api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \ --api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \
--chromium-restart-after=$(CHROMIUM_RESTART_AFTER) \ --chromium-restart-after=$(CHROMIUM_RESTART_AFTER) \
--chromium-auto-start=$(CHROMIUM_AUTO_START) \ --chromium-auto-start=$(CHROMIUM_AUTO_START) \
--chromium-max-queue-size=$(CHROMIUM_MAX_QUEUE_SIZE) \
--chromium-start-timeout=$(CHROMIUM_START_TIMEOUT) \ --chromium-start-timeout=$(CHROMIUM_START_TIMEOUT) \
--chromium-incognito=$(CHROMIUM_INCOGNITO) \ --chromium-incognito=$(CHROMIUM_INCOGNITO) \
--chromium-allow-insecure-localhost=$(CHROMIUM_ALLOW_INSECURE_LOCALHOST) \ --chromium-allow-insecure-localhost=$(CHROMIUM_ALLOW_INSECURE_LOCALHOST) \
@@ -97,13 +100,14 @@ run: ## Start a Gotenberg container
--chromium-allow-file-access-from-files=$(CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES) \ --chromium-allow-file-access-from-files=$(CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES) \
--chromium-host-resolver-rules=$(CHROMIUM_HOST_RESOLVER_RULES) \ --chromium-host-resolver-rules=$(CHROMIUM_HOST_RESOLVER_RULES) \
--chromium-proxy-server=$(CHROMIUM_PROXY_SERVER) \ --chromium-proxy-server=$(CHROMIUM_PROXY_SERVER) \
--chromium-allow-list=$(CHROMIUM_ALLOW_LIST) \ --chromium-allow-list="$(CHROMIUM_ALLOW_LIST)" \
--chromium-deny-list=$(CHROMIUM_DENY_LIST) \ --chromium-deny-list="$(CHROMIUM_DENY_LIST)" \
--chromium-clear-cache=$(CHROMIUM_CLEAR_CACHE) \ --chromium-clear-cache=$(CHROMIUM_CLEAR_CACHE) \
--chromium-clear-cookies=$(CHROMIUM_CLEAR_COOKIES) \ --chromium-clear-cookies=$(CHROMIUM_CLEAR_COOKIES) \
--chromium-disable-javascript=$(CHROMIUM_DISABLE_JAVASCRIPT) \ --chromium-disable-javascript=$(CHROMIUM_DISABLE_JAVASCRIPT) \
--chromium-disable-routes=$(CHROMIUM_DISABLE_ROUTES) \ --chromium-disable-routes=$(CHROMIUM_DISABLE_ROUTES) \
--libreoffice-restart-after=$(LIBREOFFICE_RESTART_AFTER) \ --libreoffice-restart-after=$(LIBREOFFICE_RESTART_AFTER) \
--libreoffice-max-queue-size=$(LIBREOFFICE_MAX_QUEUE_SIZE) \
--libreoffice-auto-start=$(LIBREOFFICE_AUTO_START) \ --libreoffice-auto-start=$(LIBREOFFICE_AUTO_START) \
--libreoffice-start-timeout=$(LIBREOFFICE_START_TIMEOUT) \ --libreoffice-start-timeout=$(LIBREOFFICE_START_TIMEOUT) \
--libreoffice-disable-routes=$(LIBREOFFICE_DISABLE_ROUTES) \ --libreoffice-disable-routes=$(LIBREOFFICE_DISABLE_ROUTES) \
@@ -116,8 +120,8 @@ run: ## Start a Gotenberg container
--prometheus-collect-interval=$(PROMETHEUS_COLLECT_INTERVAL) \ --prometheus-collect-interval=$(PROMETHEUS_COLLECT_INTERVAL) \
--prometheus-disable-route-logging=$(PROMETHEUS_DISABLE_ROUTE_LOGGING) \ --prometheus-disable-route-logging=$(PROMETHEUS_DISABLE_ROUTE_LOGGING) \
--prometheus-disable-collect=$(PROMETHEUS_DISABLE_COLLECT) \ --prometheus-disable-collect=$(PROMETHEUS_DISABLE_COLLECT) \
--webhook-allow-list=$(WEBHOOK_ALLOW_LIST) \ --webhook-allow-list="$(WEBHOOK_ALLOW_LIST)" \
--webhook-deny-list=$(WEBHOOK_DENY_LIST) \ --webhook-deny-list="$(WEBHOOK_DENY_LIST)" \
--webhook-error-allow-list=$(WEBHOOK_ERROR_ALLOW_LIST) \ --webhook-error-allow-list=$(WEBHOOK_ERROR_ALLOW_LIST) \
--webhook-error-deny-list=$(WEBHOOK_ERROR_DENY_LIST) \ --webhook-error-deny-list=$(WEBHOOK_ERROR_DENY_LIST) \
--webhook-max-retry=$(WEBHOOK_MAX_RETRY) \ --webhook-max-retry=$(WEBHOOK_MAX_RETRY) \

View File

@@ -75,7 +75,6 @@ func Run() {
go func(app gotenberg.App) { go func(app gotenberg.App) {
id := app.(gotenberg.Module).Descriptor().ID id := app.(gotenberg.Module).Descriptor().ID
err = app.Start() err = app.Start()
if err != nil { if err != nil {
fmt.Printf("[FATAL] starting %s: %s\n", id, err) fmt.Printf("[FATAL] starting %s: %s\n", id, err)
os.Exit(1) os.Exit(1)

24
go.mod
View File

@@ -5,13 +5,13 @@ go 1.21
require ( require (
github.com/alexliesenfeld/health v0.8.0 github.com/alexliesenfeld/health v0.8.0
github.com/andybalholm/brotli v1.1.0 // indirect github.com/andybalholm/brotli v1.1.0 // indirect
github.com/chromedp/cdproto v0.0.0-20240116100315-4a0ec5e4c400 github.com/chromedp/cdproto v0.0.0-20240214232516-ad4608604e9e
github.com/chromedp/chromedp v0.9.3 github.com/chromedp/chromedp v0.9.5
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.5.0 github.com/google/uuid v1.6.0
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.5 github.com/hashicorp/go-retryablehttp v0.7.5
github.com/klauspost/compress v1.17.4 // indirect github.com/klauspost/compress v1.17.6 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect github.com/klauspost/pgzip v1.2.6 // indirect
github.com/labstack/echo/v4 v4.11.4 github.com/labstack/echo/v4 v4.11.4
github.com/labstack/gommon v0.4.2 github.com/labstack/gommon v0.4.2
@@ -27,15 +27,17 @@ require (
github.com/ulikunitz/xz v0.5.11 // indirect github.com/ulikunitz/xz v0.5.11 // indirect
go.uber.org/multierr v1.11.0 go.uber.org/multierr v1.11.0
go.uber.org/zap v1.26.0 go.uber.org/zap v1.26.0
golang.org/x/crypto v0.18.0 // indirect golang.org/x/crypto v0.19.0 // indirect
golang.org/x/image v0.15.0 // indirect golang.org/x/image v0.15.0 // indirect
golang.org/x/net v0.20.0 golang.org/x/net v0.21.0
golang.org/x/sync v0.6.0 golang.org/x/sync v0.6.0
golang.org/x/sys v0.16.0 // indirect golang.org/x/sys v0.17.0 // indirect
golang.org/x/term v0.16.0 golang.org/x/term v0.17.0
golang.org/x/text v0.14.0 golang.org/x/text v0.14.0
) )
require github.com/dlclark/regexp2 v1.10.0
require ( require (
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
@@ -53,10 +55,10 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/client_model v0.6.0 // indirect
github.com/prometheus/common v0.46.0 // indirect github.com/prometheus/common v0.47.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect

52
go.sum
View File

@@ -9,15 +9,17 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chromedp/cdproto v0.0.0-20231011050154-1d073bb38998/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/cdproto v0.0.0-20240116100315-4a0ec5e4c400 h1:mHR3reslmE6J351eW8TgB/BPT+B9OzMxLe7dPa5WYSQ= github.com/chromedp/cdproto v0.0.0-20240214232516-ad4608604e9e h1:kXEolCWQZzuEFcuaTzfqXToX+e29OcvK87BcBiBBJ1c=
github.com/chromedp/cdproto v0.0.0-20240116100315-4a0ec5e4c400/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/cdproto v0.0.0-20240214232516-ad4608604e9e/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/chromedp v0.9.3 h1:Wq58e0dZOdHsxaj9Owmfcf+ibtpYN1N0FWVbaxa/esg= github.com/chromedp/chromedp v0.9.5 h1:viASzruPJOiThk7c5bueOUY91jGLJVximoEMGoH93rg=
github.com/chromedp/chromedp v0.9.3/go.mod h1:NipeUkUcuzIdFbBP8eNNvl9upcceOfWzoJn6cRe4ksA= github.com/chromedp/chromedp v0.9.5/go.mod h1:D4I2qONslauw/C7INoCir1BJkSwBYMyZgx8X276z3+Y=
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY= github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
@@ -25,17 +27,16 @@ github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q= github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q=
github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
@@ -52,8 +53,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
@@ -97,15 +98,15 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= github.com/prometheus/common v0.47.0 h1:p5Cz0FNHo7SnWOmWmoRozVcjEp0bIVU8cV7OShpjL1k=
github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ= github.com/prometheus/common v0.47.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
@@ -133,20 +134,21 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -1,9 +1,9 @@
package gotenberg package gotenberg
import ( import (
"regexp"
"time" "time"
"github.com/dlclark/regexp2"
"github.com/labstack/gommon/bytes" "github.com/labstack/gommon/bytes"
flag "github.com/spf13/pflag" 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. // MustRegexp returns the regular expression of a flag given by name.
// It panics if an error occurs. // 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) val, err := f.GetString(name)
if err != nil { if err != nil {
panic(err) panic(err)
} }
return regexp.MustCompile(val) return regexp2.MustCompile(val, 0)
} }
// MustDeprecatedRegexp returns the regular expression of a deprecated flag if // MustDeprecatedRegexp returns the regular expression of a deprecated flag if
// it was explicitly set or the regular expression of the new flag. // it was explicitly set or the regular expression of the new flag.
// It panics if an error occurs. // 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) { if f.Changed(deprecated) {
return f.MustRegexp(deprecated) return f.MustRegexp(deprecated)
} }

53
pkg/gotenberg/regexp.go Normal file
View File

@@ -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
}

View File

@@ -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)
}
})
}
}

View File

@@ -13,6 +13,10 @@ import (
// to restart an already restarting [Process]. // to restart an already restarting [Process].
var ErrProcessAlreadyRestarting = errors.New("process already restarting") var ErrProcessAlreadyRestarting = errors.New("process already restarting")
// ErrMaximumQueueSizeExceeded happens if Run() is called but the maximum queue
// size is already used.
var ErrMaximumQueueSizeExceeded = errors.New("maximum queue size exceeded")
// Process is an interface that represents an abstract process // Process is an interface that represents an abstract process
// and provides methods for starting, stopping, and checking the health of the // and provides methods for starting, stopping, and checking the health of the
// process. // process.
@@ -74,6 +78,7 @@ type processSupervisor struct {
logger *zap.Logger logger *zap.Logger
process Process process Process
maxReqLimit int64 maxReqLimit int64
maxQueueSize int64
mutexChan chan struct{} mutexChan chan struct{}
firstStart atomic.Bool firstStart atomic.Bool
reqCounter atomic.Int64 reqCounter atomic.Int64
@@ -83,12 +88,13 @@ type processSupervisor struct {
} }
// NewProcessSupervisor initializes a new [ProcessSupervisor]. // NewProcessSupervisor initializes a new [ProcessSupervisor].
func NewProcessSupervisor(logger *zap.Logger, process Process, maxReqLimit int64) ProcessSupervisor { func NewProcessSupervisor(logger *zap.Logger, process Process, maxReqLimit, maxQueueSize int64) ProcessSupervisor {
b := &processSupervisor{ b := &processSupervisor{
logger: logger, logger: logger,
process: process, process: process,
mutexChan: make(chan struct{}, 1), mutexChan: make(chan struct{}, 1),
maxReqLimit: maxReqLimit, maxReqLimit: maxReqLimit,
maxQueueSize: maxQueueSize,
} }
b.reqCounter.Store(0) b.reqCounter.Store(0)
b.reqQueueSize.Store(0) b.reqQueueSize.Store(0)
@@ -167,6 +173,11 @@ func (s *processSupervisor) Healthy() bool {
} }
func (s *processSupervisor) Run(ctx context.Context, logger *zap.Logger, task func() error) error { func (s *processSupervisor) Run(ctx context.Context, logger *zap.Logger, task func() error) error {
currentQueueSize := s.reqQueueSize.Load()
if s.maxQueueSize > 0 && currentQueueSize >= s.maxQueueSize {
return ErrMaximumQueueSizeExceeded
}
s.reqQueueSize.Add(1) s.reqQueueSize.Add(1)
for { for {

View File

@@ -46,7 +46,7 @@ func TestProcessSupervisor_Launch(t *testing.T) {
}, },
} }
ps := NewProcessSupervisor(logger, process, 5).(*processSupervisor) ps := NewProcessSupervisor(logger, process, 5, 0).(*processSupervisor)
if tc.firstStartSet { if tc.firstStartSet {
ps.firstStart.Store(true) ps.firstStart.Store(true)
} }
@@ -94,7 +94,7 @@ func TestProcessSupervisor_Shutdown(t *testing.T) {
}, },
} }
ps := NewProcessSupervisor(logger, process, 5) ps := NewProcessSupervisor(logger, process, 5, 0)
err := ps.Shutdown() err := ps.Shutdown()
if !tc.expectError && err != nil { if !tc.expectError && err != nil {
@@ -154,7 +154,7 @@ func TestProcessSupervisor_restart(t *testing.T) {
}, },
} }
ps := NewProcessSupervisor(logger, process, 5).(*processSupervisor) ps := NewProcessSupervisor(logger, process, 5, 0).(*processSupervisor)
if tc.initiallyRestarting { if tc.initiallyRestarting {
ps.isRestarting.Store(true) ps.isRestarting.Store(true)
} }
@@ -217,7 +217,7 @@ func TestProcessSupervisor_Healthy(t *testing.T) {
}, },
} }
ps := NewProcessSupervisor(logger, process, 5).(*processSupervisor) ps := NewProcessSupervisor(logger, process, 5, 0).(*processSupervisor)
if tc.initiallyStarted { if tc.initiallyStarted {
ps.firstStart.Store(true) ps.firstStart.Store(true)
} }
@@ -249,6 +249,8 @@ func TestProcessSupervisor_Run(t *testing.T) {
expectedStartCalls int64 expectedStartCalls int64
expectedHealthyCalls int64 expectedHealthyCalls int64
expectedStopCalls int64 expectedStopCalls int64
currentQueueSize int64
maxQueueSize int64
}{ }{
{ {
scenario: "successfully run task on non-started process", scenario: "successfully run task on non-started process",
@@ -348,6 +350,34 @@ func TestProcessSupervisor_Run(t *testing.T) {
expectedHealthyCalls: 1, expectedHealthyCalls: 1,
expectedStopCalls: 0, expectedStopCalls: 0,
}, },
{
scenario: "queue size exceeded",
initiallyStarted: false,
isRestarting: false,
processHealthy: true,
maxReqLimit: 2,
tasksToRun: 1,
expectError: true,
expectedStartCalls: 0,
expectedHealthyCalls: 0,
expectedStopCalls: 0,
currentQueueSize: 1,
maxQueueSize: 1,
},
{
scenario: "queue size not exceeded",
initiallyStarted: false,
isRestarting: false,
processHealthy: true,
maxReqLimit: 2,
tasksToRun: 1,
expectError: true,
expectedStartCalls: 1,
expectedHealthyCalls: 1,
expectedStopCalls: 0,
currentQueueSize: 1,
maxQueueSize: 2,
},
} { } {
t.Run(tc.scenario, func(t *testing.T) { t.Run(tc.scenario, func(t *testing.T) {
logger := zap.NewNop() logger := zap.NewNop()
@@ -372,13 +402,16 @@ func TestProcessSupervisor_Run(t *testing.T) {
}, },
} }
ps := NewProcessSupervisor(logger, process, tc.maxReqLimit).(*processSupervisor) ps := NewProcessSupervisor(logger, process, tc.maxReqLimit, tc.maxQueueSize).(*processSupervisor)
if tc.initiallyStarted { if tc.initiallyStarted {
ps.firstStart.Store(true) ps.firstStart.Store(true)
} }
if tc.isRestarting { if tc.isRestarting {
ps.isRestarting.Store(true) ps.isRestarting.Store(true)
} }
if tc.currentQueueSize > 0 {
ps.reqQueueSize.Store(tc.currentQueueSize)
}
task := func() error { task := func() error {
return tc.taskError return tc.taskError
@@ -451,13 +484,13 @@ func TestProcessSupervisor_runWithDeadline(t *testing.T) {
}, },
} { } {
t.Run(tc.scenario, func(t *testing.T) { t.Run(tc.scenario, func(t *testing.T) {
ps := NewProcessSupervisor(zap.NewNop(), new(ProcessMock), 0).(*processSupervisor) ps := NewProcessSupervisor(zap.NewNop(), new(ProcessMock), 0, 0).(*processSupervisor)
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
if tc.ctxDone { if tc.ctxDone {
cancel() cancel()
} else {
defer cancel()
} }
err := ps.runWithDeadline(ctx, func() error { err := ps.runWithDeadline(ctx, func() error {
@@ -485,7 +518,7 @@ func TestProcessSupervisor_ReqQueueSize(t *testing.T) {
return true return true
}, },
} }
ps := NewProcessSupervisor(logger, process, 0).(*processSupervisor) ps := NewProcessSupervisor(logger, process, 0, 0).(*processSupervisor)
// Simulating a lock. // Simulating a lock.
ps.mutexChan <- struct{}{} ps.mutexChan <- struct{}{}
@@ -586,7 +619,7 @@ func TestProcessSupervisor_RestartsCount(t *testing.T) {
}, },
} }
ps := NewProcessSupervisor(logger, process, 0).(*processSupervisor) ps := NewProcessSupervisor(logger, process, 0, 0).(*processSupervisor)
ps.restartsCounter.Store(tc.initialRestartsCount) ps.restartsCounter.Store(tc.initialRestartsCount)
for i := 0; i < tc.restartAttempts; i++ { for i := 0; i < tc.restartAttempts; i++ {

View File

@@ -164,7 +164,6 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
for _, files := range form.File { for _, files := range form.File {
for _, fh := range files { for _, fh := range files {
err = copyToDisk(fh) err = copyToDisk(fh)
if err != nil { if err != nil {
return ctx, cancel, fmt.Errorf("copy to disk: %w", err) return ctx, cancel, fmt.Errorf("copy to disk: %w", err)
} }
@@ -191,10 +190,15 @@ func (ctx *Context) FormData() *FormData {
} }
} }
// GeneratePath generates a path within the context's working directory. It // GeneratePath generates a path within the context's working directory.
// does not create a file. // It either generates a new UUID-based filename or uses the provided filename.
func (ctx *Context) GeneratePath(extension string) string { // It does not create a file.
return fmt.Sprintf("%s/%s%s", ctx.dirPath, uuid.New(), extension) func (ctx *Context) GeneratePath(filename, extension string) string {
if filename == "" {
// Generate a new UUID-based filename
filename = uuid.New().String()
}
return fmt.Sprintf("%s/%s%s", ctx.dirPath, filename, extension)
} }
// AddOutputPaths adds the given paths. Those paths will be used later to build // AddOutputPaths adds the given paths. Those paths will be used later to build
@@ -246,7 +250,7 @@ func (ctx *Context) BuildOutputFile() (string, error) {
ImplicitTopLevelFolder: false, ImplicitTopLevelFolder: false,
} }
archivePath := ctx.GeneratePath(".zip") archivePath := ctx.GeneratePath("", ".zip")
err := z.Archive(ctx.outputPaths, archivePath) err := z.Archive(ctx.outputPaths, archivePath)
if err != nil { if err != nil {

View File

@@ -190,11 +190,15 @@ func TestContext_GeneratePath(t *testing.T) {
dirPath: "/foo", dirPath: "/foo",
} }
path := ctx.GeneratePath(".pdf") path := ctx.GeneratePath("", ".pdf")
if !strings.HasPrefix(path, ctx.dirPath) { if !strings.HasPrefix(path, ctx.dirPath) {
t.Errorf("expected '%s' to start with '%s'", path, ctx.dirPath) t.Errorf("expected '%s' to start with '%s'", path, ctx.dirPath)
} }
path = ctx.GeneratePath("foo.txt", ".pdf")
if !strings.Contains(path, "foo.txt.pdf") {
t.Errorf("expected '%s' to start with '%s'", path, ctx.dirPath)
}
} }
func TestContext_AddOutputPaths(t *testing.T) { func TestContext_AddOutputPaths(t *testing.T) {

View File

@@ -5,7 +5,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"regexp"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -15,6 +14,7 @@ import (
"github.com/chromedp/cdproto/network" "github.com/chromedp/cdproto/network"
"github.com/chromedp/cdproto/runtime" "github.com/chromedp/cdproto/runtime"
"github.com/chromedp/chromedp" "github.com/chromedp/chromedp"
"github.com/dlclark/regexp2"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg" "github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
@@ -39,8 +39,8 @@ type browserArguments struct {
wsUrlReadTimeout time.Duration wsUrlReadTimeout time.Duration
// Tasks specific. // Tasks specific.
allowList *regexp.Regexp allowList *regexp2.Regexp
denyList *regexp.Regexp denyList *regexp2.Regexp
clearCache bool clearCache bool
clearCookies bool clearCookies bool
disableJavaScript 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") 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() deadline, ok := ctx.Deadline()
if !ok { if !ok {
return errors.New("context has no deadline") 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() b.ctxMu.RLock()
defer b.ctxMu.RUnlock() 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) listenForEventExceptionThrown(taskCtx, logger, &consoleExceptions, &consoleExceptionsMu)
} }
err := chromedp.Run(taskCtx, tasks...) err = chromedp.Run(taskCtx, tasks...)
if err != nil { if err != nil {
errMessage := err.Error() errMessage := err.Error()

View File

@@ -5,11 +5,11 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"regexp"
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/dlclark/regexp2"
"github.com/google/uuid" "github.com/google/uuid"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
@@ -268,46 +268,9 @@ func TestChromiumBrowser_pdf(t *testing.T) {
expectError: true, expectError: true,
}, },
{ {
scenario: "ErrUrlNotAuthorized: main URL does not match the allowed list", scenario: "context has no deadline",
browser: func() browser { browser: func() browser {
b := new(chromiumBrowser) 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) b.isStarted.Store(true)
return b return b
}(), }(),
@@ -316,14 +279,48 @@ func TestChromiumBrowser_pdf(t *testing.T) {
start: false, start: false,
expectError: true, 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", scenario: "a request does not match the allowed list",
browser: newChromiumBrowser( browser: newChromiumBrowser(
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile("^file:///tmp.*"), allowList: regexp2.MustCompile("^file:///tmp.*", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -354,8 +351,8 @@ func TestChromiumBrowser_pdf(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile("^file:///[^tmp].*"), denyList: regexp2.MustCompile(`^file:(?!//\/tmp/).*`, 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -386,8 +383,8 @@ func TestChromiumBrowser_pdf(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -421,8 +418,8 @@ func TestChromiumBrowser_pdf(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -454,8 +451,8 @@ func TestChromiumBrowser_pdf(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -487,8 +484,8 @@ func TestChromiumBrowser_pdf(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
clearCache: true, clearCache: true,
}, },
), ),
@@ -520,8 +517,8 @@ func TestChromiumBrowser_pdf(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
clearCookies: true, clearCookies: true,
}, },
), ),
@@ -553,8 +550,8 @@ func TestChromiumBrowser_pdf(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
disableJavaScript: true, disableJavaScript: true,
}, },
), ),
@@ -588,8 +585,8 @@ func TestChromiumBrowser_pdf(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -625,8 +622,8 @@ func TestChromiumBrowser_pdf(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -658,8 +655,8 @@ func TestChromiumBrowser_pdf(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -694,8 +691,8 @@ func TestChromiumBrowser_pdf(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -727,8 +724,8 @@ func TestChromiumBrowser_pdf(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -762,8 +759,8 @@ func TestChromiumBrowser_pdf(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -797,8 +794,8 @@ func TestChromiumBrowser_pdf(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -832,8 +829,8 @@ func TestChromiumBrowser_pdf(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -878,8 +875,8 @@ func TestChromiumBrowser_pdf(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -913,8 +910,8 @@ func TestChromiumBrowser_pdf(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -959,8 +956,8 @@ func TestChromiumBrowser_pdf(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -995,8 +992,8 @@ func TestChromiumBrowser_pdf(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -1033,8 +1030,8 @@ func TestChromiumBrowser_pdf(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -1066,8 +1063,8 @@ func TestChromiumBrowser_pdf(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -1202,45 +1199,12 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
expectError: true, expectError: true,
}, },
{ {
scenario: "ErrUrlNotAuthorized: main URL does not match the allowed list", scenario: "context has not deadline",
browser: func() browser { browser: func() browser {
b := new(chromiumBrowser) b := new(chromiumBrowser)
b.arguments = browserArguments{ b.arguments = browserArguments{
allowList: regexp.MustCompile("^file:///[^tmp].*"), allowList: regexp2.MustCompile("", 0),
} denyList: regexp2.MustCompile("", 0),
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) b.isStarted.Store(true)
return b return b
@@ -1250,14 +1214,48 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
start: false, start: false,
expectError: true, 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", scenario: "a request does not match the allowed list",
browser: newChromiumBrowser( browser: newChromiumBrowser(
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile("^file:///tmp.*"), allowList: regexp2.MustCompile("^file:///tmp.*", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -1288,8 +1286,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile("^file:///[^tmp].*"), denyList: regexp2.MustCompile(`^file:(?!//\/tmp/).*`, 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -1320,8 +1318,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -1355,8 +1353,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -1388,8 +1386,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -1421,8 +1419,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
clearCache: true, clearCache: true,
}, },
), ),
@@ -1454,8 +1452,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
clearCookies: true, clearCookies: true,
}, },
), ),
@@ -1487,8 +1485,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
disableJavaScript: true, disableJavaScript: true,
}, },
), ),
@@ -1522,8 +1520,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -1559,8 +1557,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -1594,8 +1592,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -1627,8 +1625,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -1662,8 +1660,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -1697,8 +1695,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -1732,8 +1730,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -1778,8 +1776,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -1813,8 +1811,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -1859,8 +1857,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
@@ -1900,8 +1898,8 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
browserArguments{ browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"), binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second, wsUrlReadTimeout: 5 * time.Second,
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {

View File

@@ -20,10 +20,6 @@ func init() {
} }
var ( 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 // ErrInvalidEmulatedMediaType happens if the emulated media type is not
// "screen" nor "print". Empty value are allowed though. // "screen" nor "print". Empty value are allowed though.
ErrInvalidEmulatedMediaType = errors.New("invalid emulated media type") ErrInvalidEmulatedMediaType = errors.New("invalid emulated media type")
@@ -280,6 +276,7 @@ func (mod *Chromium) Descriptor() gotenberg.ModuleDescriptor {
FlagSet: func() *flag.FlagSet { FlagSet: func() *flag.FlagSet {
fs := flag.NewFlagSet("chromium", flag.ExitOnError) fs := flag.NewFlagSet("chromium", flag.ExitOnError)
fs.Int64("chromium-restart-after", 0, "Number of conversions after which Chromium will automatically restart. Set to 0 to disable this feature") fs.Int64("chromium-restart-after", 0, "Number of conversions after which Chromium will automatically restart. Set to 0 to disable this feature")
fs.Int64("chromium-max-queue-size", 0, "Maximum request queue size for Chromium. Set to 0 to disable this feature")
fs.Bool("chromium-auto-start", false, "Automatically launch Chromium upon initialization if set to true; otherwise, Chromium will start at the time of the first conversion") fs.Bool("chromium-auto-start", false, "Automatically launch Chromium upon initialization if set to true; otherwise, Chromium will start at the time of the first conversion")
fs.Duration("chromium-start-timeout", time.Duration(20)*time.Second, "Maximum duration to wait for Chromium to start or restart") fs.Duration("chromium-start-timeout", time.Duration(20)*time.Second, "Maximum duration to wait for Chromium to start or restart")
fs.Bool("chromium-incognito", false, "Start Chromium with incognito mode") fs.Bool("chromium-incognito", false, "Start Chromium with incognito mode")
@@ -290,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-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-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-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-cache", false, "Clear Chromium cache between each conversion")
fs.Bool("chromium-clear-cookies", false, "Clear Chromium cookies between each conversion") fs.Bool("chromium-clear-cookies", false, "Clear Chromium cookies between each conversion")
fs.Bool("chromium-disable-javascript", false, "Disable JavaScript") fs.Bool("chromium-disable-javascript", false, "Disable JavaScript")
@@ -344,7 +341,7 @@ func (mod *Chromium) Provision(ctx *gotenberg.Context) error {
// Process. // Process.
mod.browser = newChromiumBrowser(mod.args) mod.browser = newChromiumBrowser(mod.args)
mod.supervisor = gotenberg.NewProcessSupervisor(mod.logger, mod.browser, flags.MustInt64("chromium-restart-after")) mod.supervisor = gotenberg.NewProcessSupervisor(mod.logger, mod.browser, flags.MustInt64("chromium-restart-after"), flags.MustInt64("chromium-max-queue-size"))
// PDF Engine. // PDF Engine.
provider, err := ctx.Module(new(gotenberg.PdfEngineProvider)) provider, err := ctx.Module(new(gotenberg.PdfEngineProvider))

View File

@@ -3,7 +3,6 @@ package chromium
import ( import (
"context" "context"
"fmt" "fmt"
"regexp"
"slices" "slices"
"sync" "sync"
@@ -13,14 +12,17 @@ import (
"github.com/chromedp/cdproto/page" "github.com/chromedp/cdproto/page"
"github.com/chromedp/cdproto/runtime" "github.com/chromedp/cdproto/runtime"
"github.com/chromedp/chromedp" "github.com/chromedp/chromedp"
"github.com/dlclark/regexp2"
"go.uber.org/multierr" "go.uber.org/multierr"
"go.uber.org/zap" "go.uber.org/zap"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
) )
// listenForEventRequestPaused listens for requests to check if they are // listenForEventRequestPaused listens for requests to check if they are
// allowed or not. // 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{}) { chromedp.ListenTarget(ctx, func(ev interface{}) {
switch e := ev.(type) { switch e := ev.(type) {
case *fetch.EventRequestPaused: 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)) logger.Debug(fmt.Sprintf("event EventRequestPaused fired for '%s'", e.Request.URL))
allow := true allow := true
if !allowList.MatchString(e.Request.URL) { deadline, ok := ctx.Deadline()
logger.Warn(fmt.Sprintf("'%s' does not match the expression from the allowed list", e.Request.URL)) if !ok {
allow = false logger.Error("context has no deadline, cannot filter URL")
return
} }
if denyList.String() != "" && denyList.MatchString(e.Request.URL) { err := gotenberg.FilterDeadline(allowList, denyList, e.Request.URL, deadline)
logger.Warn(fmt.Sprintf("'%s' matches the expression from the denied list", e.Request.URL)) if err != nil {
logger.Warn(err.Error())
allow = false allow = false
} }
@@ -43,16 +47,15 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowL
if allow { if allow {
req := fetch.ContinueRequest(e.RequestID) req := fetch.ContinueRequest(e.RequestID)
err := req.Do(executorCtx) err = req.Do(executorCtx)
if err != nil { if err != nil {
logger.Error(fmt.Sprintf("continue request: %s", err)) logger.Error(fmt.Sprintf("continue request: %s", err))
} }
return return
} }
req := fetch.FailRequest(e.RequestID, network.ErrorReasonAccessDenied) req := fetch.FailRequest(e.RequestID, network.ErrorReasonAccessDenied)
err := req.Do(executorCtx) err = req.Do(executorCtx)
if err != nil { if err != nil {
logger.Error(fmt.Sprintf("fail request: %s", err)) logger.Error(fmt.Sprintf("fail request: %s", err))
} }

View File

@@ -494,7 +494,7 @@ func markdownToHtml(ctx *api.Context, inputPath string, markdownPaths []string)
) )
} }
inputPath = ctx.GeneratePath(".html") inputPath = ctx.GeneratePath("", ".html")
err = os.WriteFile(inputPath, buffer.Bytes(), 0o600) err = os.WriteFile(inputPath, buffer.Bytes(), 0o600)
if err != nil { if err != nil {
@@ -505,14 +505,14 @@ func markdownToHtml(ctx *api.Context, inputPath string, markdownPaths []string)
} }
func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url string, pdfFormats gotenberg.PdfFormats, options PdfOptions) error { func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url string, pdfFormats gotenberg.PdfFormats, options PdfOptions) error {
outputPath := ctx.GeneratePath(".pdf") outputPath := ctx.GeneratePath("", ".pdf")
err := chromium.Pdf(ctx, ctx.Log(), url, outputPath, options) err := chromium.Pdf(ctx, ctx.Log(), url, outputPath, options)
err = handleChromiumError(err, url, options.Options) err = handleChromiumError(err, url, options.Options)
if err != nil { if err != nil {
if errors.Is(err, ErrOmitBackgroundWithoutPrintBackground) { if errors.Is(err, ErrOmitBackgroundWithoutPrintBackground) {
return api.WrapError( return api.WrapError(
err, fmt.Errorf("convert to PDF: %w", err),
api.NewSentinelHttpError( api.NewSentinelHttpError(
http.StatusBadRequest, http.StatusBadRequest,
"omitBackground requires printBackground set to true", "omitBackground requires printBackground set to true",
@@ -549,11 +549,20 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
zeroValued := gotenberg.PdfFormats{} zeroValued := gotenberg.PdfFormats{}
if pdfFormats != zeroValued { if pdfFormats != zeroValued {
convertInputPath := outputPath convertInputPath := outputPath
convertOutputPath := ctx.GeneratePath(".pdf") convertOutputPath := ctx.GeneratePath("", ".pdf")
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath) err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
if err != nil { if err != nil {
if errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded) {
return api.WrapError(
fmt.Errorf("convert PDF: %w", err),
api.NewSentinelHttpError(
http.StatusTooManyRequests,
"The maximum queue size has been reached",
),
)
}
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) { if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
return api.WrapError( return api.WrapError(
fmt.Errorf("convert PDF: %w", err), fmt.Errorf("convert PDF: %w", err),
@@ -581,7 +590,7 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
func screenshotUrl(ctx *api.Context, chromium Api, url string, options ScreenshotOptions) error { func screenshotUrl(ctx *api.Context, chromium Api, url string, options ScreenshotOptions) error {
ext := fmt.Sprintf(".%s", options.Format) ext := fmt.Sprintf(".%s", options.Format)
outputPath := ctx.GeneratePath(ext) outputPath := ctx.GeneratePath("", ext)
err := chromium.Screenshot(ctx, ctx.Log(), url, outputPath, options) err := chromium.Screenshot(ctx, ctx.Log(), url, outputPath, options)
err = handleChromiumError(err, url, options.Options) err = handleChromiumError(err, url, options.Options)
@@ -602,7 +611,17 @@ func handleChromiumError(err error, url string, options Options) error {
return nil return nil
} }
if errors.Is(err, ErrUrlNotAuthorized) { if errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded) {
return api.WrapError(
err,
api.NewSentinelHttpError(
http.StatusTooManyRequests,
"The maximum queue size has been reached",
),
)
}
if errors.Is(err, gotenberg.ErrFiltered) {
return api.WrapError( return api.WrapError(
err, err,
api.NewSentinelHttpError( api.NewSentinelHttpError(

View File

@@ -1231,10 +1231,22 @@ func TestConvertUrl(t *testing.T) {
expectOutputPathsCount int expectOutputPathsCount int
}{ }{
{ {
scenario: "ErrUrlNotAuthorized", scenario: "ErrMaximumQueueSizeExceeded",
ctx: &api.ContextMock{Context: new(api.Context)}, ctx: &api.ContextMock{Context: new(api.Context)},
api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
return ErrUrlNotAuthorized return gotenberg.ErrMaximumQueueSizeExceeded
}},
options: DefaultPdfOptions(),
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusTooManyRequests,
expectOutputPathsCount: 0,
},
{
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 gotenberg.ErrFiltered
}}, }},
options: DefaultPdfOptions(), options: DefaultPdfOptions(),
expectError: true, expectError: true,
@@ -1341,6 +1353,22 @@ func TestConvertUrl(t *testing.T) {
expectHttpError: false, expectHttpError: false,
expectOutputPathsCount: 0, expectOutputPathsCount: 0,
}, },
{
scenario: "ErrMaximumQueueSizeExceeded (PDF engine)",
ctx: &api.ContextMock{Context: new(api.Context)},
api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
return nil
}},
engine: &gotenberg.PdfEngineMock{ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return gotenberg.ErrMaximumQueueSizeExceeded
}},
pdfFormats: gotenberg.PdfFormats{PdfA: "foo"},
options: DefaultPdfOptions(),
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusTooManyRequests,
expectOutputPathsCount: 0,
},
{ {
scenario: "ErrPdfFormatNotSupported", scenario: "ErrPdfFormatNotSupported",
ctx: &api.ContextMock{Context: new(api.Context)}, ctx: &api.ContextMock{Context: new(api.Context)},
@@ -1463,10 +1491,22 @@ func TestScreenshotUrl(t *testing.T) {
expectOutputPathsCount int expectOutputPathsCount int
}{ }{
{ {
scenario: "ErrUrlNotAuthorized", scenario: "ErrMaximumQueueSizeExceeded",
ctx: &api.ContextMock{Context: new(api.Context)}, ctx: &api.ContextMock{Context: new(api.Context)},
api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
return ErrUrlNotAuthorized return gotenberg.ErrMaximumQueueSizeExceeded
}},
options: DefaultScreenshotOptions(),
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusTooManyRequests,
expectOutputPathsCount: 0,
},
{
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 gotenberg.ErrFiltered
}}, }},
options: DefaultScreenshotOptions(), options: DefaultScreenshotOptions(),
expectError: true, expectError: true,

View File

@@ -80,6 +80,7 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
FlagSet: func() *flag.FlagSet { FlagSet: func() *flag.FlagSet {
fs := flag.NewFlagSet("api", flag.ExitOnError) fs := flag.NewFlagSet("api", flag.ExitOnError)
fs.Int64("libreoffice-restart-after", 10, "Number of conversions after which LibreOffice will automatically restart. Set to 0 to disable this feature") fs.Int64("libreoffice-restart-after", 10, "Number of conversions after which LibreOffice will automatically restart. Set to 0 to disable this feature")
fs.Int64("libreoffice-max-queue-size", 0, "Maximum request queue size for LibreOffice. Set to 0 to disable this feature")
fs.Bool("libreoffice-auto-start", false, "Automatically launch LibreOffice upon initialization if set to true; otherwise, LibreOffice will start at the time of the first conversion") fs.Bool("libreoffice-auto-start", false, "Automatically launch LibreOffice upon initialization if set to true; otherwise, LibreOffice will start at the time of the first conversion")
fs.Duration("libreoffice-start-timeout", time.Duration(20)*time.Second, "Maximum duration to wait for LibreOffice to start or restart") fs.Duration("libreoffice-start-timeout", time.Duration(20)*time.Second, "Maximum duration to wait for LibreOffice to start or restart")
@@ -123,7 +124,7 @@ func (a *Api) Provision(ctx *gotenberg.Context) error {
// Process. // Process.
a.libreOffice = newLibreOfficeProcess(a.args) a.libreOffice = newLibreOfficeProcess(a.args)
a.supervisor = gotenberg.NewProcessSupervisor(a.logger, a.libreOffice, flags.MustInt64("libreoffice-restart-after")) a.supervisor = gotenberg.NewProcessSupervisor(a.logger, a.libreOffice, flags.MustInt64("libreoffice-restart-after"), flags.MustInt64("libreoffice-max-queue-size"))
return nil return nil
} }

View File

@@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"path/filepath"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
@@ -54,8 +55,8 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
// Alright, let's convert each document to PDF. // Alright, let's convert each document to PDF.
outputPaths := make([]string, len(inputPaths)) outputPaths := make([]string, len(inputPaths))
for i, inputPath := range inputPaths { for i, inputPath := range inputPaths {
outputPaths[i] = ctx.GeneratePath(".pdf") // document.docx -> document.docx.pdf.
outputPaths[i] = ctx.GeneratePath(filepath.Base(inputPath), ".pdf")
options := libreofficeapi.Options{ options := libreofficeapi.Options{
Landscape: landscape, Landscape: landscape,
PageRanges: nativePageRanges, PageRanges: nativePageRanges,
@@ -67,6 +68,16 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
err = libreOffice.Pdf(ctx, ctx.Log(), inputPath, outputPaths[i], options) err = libreOffice.Pdf(ctx, ctx.Log(), inputPath, outputPaths[i], options)
if err != nil { if err != nil {
if errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded) {
return api.WrapError(
fmt.Errorf("convert to PDF: %w", err),
api.NewSentinelHttpError(
http.StatusTooManyRequests,
"The maximum queue size has been reached",
),
)
}
if errors.Is(err, libreofficeapi.ErrInvalidPdfFormats) { if errors.Is(err, libreofficeapi.ErrInvalidPdfFormats) {
return api.WrapError( return api.WrapError(
fmt.Errorf("convert to PDF: %w", err), fmt.Errorf("convert to PDF: %w", err),
@@ -92,22 +103,42 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
// win: if there is only one PDF, skip this step. // win: if there is only one PDF, skip this step.
if len(outputPaths) > 1 && merge { if len(outputPaths) > 1 && merge {
outputPath := ctx.GeneratePath(".pdf") outputPath := ctx.GeneratePath("", ".pdf")
err = engine.Merge(ctx, ctx.Log(), outputPaths, outputPath) err = engine.Merge(ctx, ctx.Log(), outputPaths, outputPath)
if err != nil { if err != nil {
if errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded) {
return api.WrapError(
fmt.Errorf("merge PDFs: %w", err),
api.NewSentinelHttpError(
http.StatusTooManyRequests,
"The maximum queue size has been reached",
),
)
}
return fmt.Errorf("merge PDFs: %w", err) return fmt.Errorf("merge PDFs: %w", err)
} }
// Now, let's check if the client want to convert this result // Now, let's check if the client want to convert this
// PDF to specific PDF formats. // resulting PDF to specific PDF formats.
zeroValued := gotenberg.PdfFormats{} zeroValued := gotenberg.PdfFormats{}
if !nativePdfFormats && pdfFormats != zeroValued { if !nativePdfFormats && pdfFormats != zeroValued {
convertInputPath := outputPath convertInputPath := outputPath
convertOutputPath := ctx.GeneratePath(".pdf") convertOutputPath := ctx.GeneratePath("", ".pdf")
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath) err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
if err != nil { if err != nil {
if errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded) {
return api.WrapError(
fmt.Errorf("convert PDF: %w", err),
api.NewSentinelHttpError(
http.StatusTooManyRequests,
"The maximum queue size has been reached",
),
)
}
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) { if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
return api.WrapError( return api.WrapError(
fmt.Errorf("convert PDF: %w", err), fmt.Errorf("convert PDF: %w", err),
@@ -144,10 +175,21 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
for i, outputPath := range outputPaths { for i, outputPath := range outputPaths {
convertInputPath := outputPath convertInputPath := outputPath
convertOutputPaths[i] = ctx.GeneratePath(".pdf") // document.docx -> document.docx.pdf.
convertOutputPaths[i] = ctx.GeneratePath(filepath.Base(inputPaths[i]), ".pdf")
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPaths[i]) err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPaths[i])
if err != nil { if err != nil {
if errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded) {
return api.WrapError(
fmt.Errorf("convert PDF: %w", err),
api.NewSentinelHttpError(
http.StatusTooManyRequests,
"The maximum queue size has been reached",
),
)
}
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) { if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
return api.WrapError( return api.WrapError(
fmt.Errorf("convert PDF: %w", err), fmt.Errorf("convert PDF: %w", err),

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"net/http" "net/http"
"slices"
"testing" "testing"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
@@ -25,6 +26,7 @@ func TestConvertRoute(t *testing.T) {
expectHttpError bool expectHttpError bool
expectHttpStatus int expectHttpStatus int
expectOutputPathsCount int expectOutputPathsCount int
expectOutputPaths []string
}{ }{
{ {
scenario: "missing at least one mandatory file", scenario: "missing at least one mandatory file",
@@ -37,6 +39,28 @@ func TestConvertRoute(t *testing.T) {
expectHttpStatus: http.StatusBadRequest, expectHttpStatus: http.StatusBadRequest,
expectOutputPathsCount: 0, expectOutputPathsCount: 0,
}, },
{
scenario: "ErrMaximumQueueSizeExceeded",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"document.docx": "/document.docx",
})
return ctx
}(),
libreOffice: &libreofficeapi.ApiMock{
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
return gotenberg.ErrMaximumQueueSizeExceeded
},
ExtensionsMock: func() []string {
return []string{".docx"}
},
},
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusTooManyRequests,
expectOutputPathsCount: 0,
},
{ {
scenario: "ErrPdfFormatNotSupported (nativePdfFormats)", scenario: "ErrPdfFormatNotSupported (nativePdfFormats)",
ctx: func() *api.ContextMock { ctx: func() *api.ContextMock {
@@ -107,6 +131,41 @@ func TestConvertRoute(t *testing.T) {
expectHttpError: false, expectHttpError: false,
expectOutputPathsCount: 0, expectOutputPathsCount: 0,
}, },
{
scenario: "ErrMaximumQueueSizeExceeded (single file)",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"document.docx": "/document.docx",
})
ctx.SetValues(map[string][]string{
"pdfa": {
gotenberg.PdfA1b,
},
"nativePdfFormats": {
"false",
},
})
return ctx
}(),
libreOffice: &libreofficeapi.ApiMock{
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
return nil
},
ExtensionsMock: func() []string {
return []string{".docx"}
},
},
engine: &gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return gotenberg.ErrMaximumQueueSizeExceeded
},
},
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusTooManyRequests,
expectOutputPathsCount: 0,
},
{ {
scenario: "ErrPdfFormatNotSupported (single file)", scenario: "ErrPdfFormatNotSupported (single file)",
ctx: func() *api.ContextMock { ctx: func() *api.ContextMock {
@@ -218,6 +277,7 @@ func TestConvertRoute(t *testing.T) {
expectError: false, expectError: false,
expectHttpError: false, expectHttpError: false,
expectOutputPathsCount: 1, expectOutputPathsCount: 1,
expectOutputPaths: []string{"/document.docx.pdf"},
}, },
{ {
scenario: "success (many files)", scenario: "success (many files)",
@@ -226,6 +286,7 @@ func TestConvertRoute(t *testing.T) {
ctx.SetFiles(map[string]string{ ctx.SetFiles(map[string]string{
"document.docx": "/document.docx", "document.docx": "/document.docx",
"document2.docx": "/document2.docx", "document2.docx": "/document2.docx",
"document2.doc": "/document2.doc",
}) })
return ctx return ctx
}(), }(),
@@ -234,19 +295,21 @@ func TestConvertRoute(t *testing.T) {
return nil return nil
}, },
ExtensionsMock: func() []string { ExtensionsMock: func() []string {
return []string{".docx"} return []string{".docx", ".doc"}
}, },
}, },
expectError: false, expectError: false,
expectHttpError: false, expectHttpError: false,
expectOutputPathsCount: 2, expectOutputPathsCount: 3,
expectOutputPaths: []string{"/document.docx.pdf", "/document2.docx.pdf", "/document2.doc.pdf"},
}, },
{ {
scenario: "success with non-native PDF/A & PDF/UA (single file)", scenario: "success with non-native PDF/A & PDF/UA (many files)",
ctx: func() *api.ContextMock { ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)} ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{ ctx.SetFiles(map[string]string{
"document.docx": "/document.docx", "document.docx": "/document.docx",
"document2.docx": "/document2.docx",
}) })
ctx.SetValues(map[string][]string{ ctx.SetValues(map[string][]string{
"pdfa": { "pdfa": {
@@ -276,14 +339,16 @@ func TestConvertRoute(t *testing.T) {
}, },
expectError: false, expectError: false,
expectHttpError: false, expectHttpError: false,
expectOutputPathsCount: 1, expectOutputPathsCount: 2,
expectOutputPaths: []string{"/document.docx.pdf", "/document2.docx.pdf"},
}, },
{ {
scenario: "success with native PDF/A & PDF/UA (single file)", scenario: "success with native PDF/A & PDF/UA (many files)",
ctx: func() *api.ContextMock { ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)} ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{ ctx.SetFiles(map[string]string{
"document.docx": "/document.docx", "document.docx": "/document.docx",
"document2.docx": "/document2.docx",
}) })
ctx.SetValues(map[string][]string{ ctx.SetValues(map[string][]string{
"pdfa": { "pdfa": {
@@ -310,7 +375,41 @@ func TestConvertRoute(t *testing.T) {
}, },
expectError: false, expectError: false,
expectHttpError: false, expectHttpError: false,
expectOutputPathsCount: 1, expectOutputPathsCount: 2,
expectOutputPaths: []string{"/document.docx.pdf", "/document2.docx.pdf"},
},
{
scenario: "ErrMaximumQueueSizeExceeded (merge)",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"document.docx": "/document.docx",
"document2.docx": "/document2.docx",
})
ctx.SetValues(map[string][]string{
"merge": {
"true",
},
})
return ctx
}(),
libreOffice: &libreofficeapi.ApiMock{
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
return nil
},
ExtensionsMock: func() []string {
return []string{".docx"}
},
},
engine: &gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return gotenberg.ErrMaximumQueueSizeExceeded
},
},
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusTooManyRequests,
expectOutputPathsCount: 0,
}, },
{ {
scenario: "merge error", scenario: "merge error",
@@ -344,6 +443,48 @@ func TestConvertRoute(t *testing.T) {
expectHttpError: false, expectHttpError: false,
expectOutputPathsCount: 0, expectOutputPathsCount: 0,
}, },
{
scenario: "ErrMaximumQueueSizeExceeded (convert post-merge)",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"document.docx": "/document.docx",
"document2.docx": "/document2.docx",
})
ctx.SetValues(map[string][]string{
"merge": {
"true",
},
"pdfa": {
gotenberg.PdfA1b,
},
"nativePdfFormats": {
"false",
},
})
return ctx
}(),
libreOffice: &libreofficeapi.ApiMock{
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
return nil
},
ExtensionsMock: func() []string {
return []string{".docx"}
},
},
engine: &gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return gotenberg.ErrMaximumQueueSizeExceeded
},
},
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusTooManyRequests,
expectOutputPathsCount: 0,
},
{ {
scenario: "ErrPdfFormatNotSupported (merge)", scenario: "ErrPdfFormatNotSupported (merge)",
ctx: func() *api.ContextMock { ctx: func() *api.ContextMock {
@@ -617,6 +758,12 @@ func TestConvertRoute(t *testing.T) {
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) { if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) {
t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths())) t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
} }
for _, path := range tc.expectOutputPaths {
if !slices.Contains(tc.ctx.OutputPaths(), path) {
t.Errorf("expected '%s' in output paths %v", path, tc.ctx.OutputPaths())
}
}
}) })
} }
} }

View File

@@ -4,6 +4,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"path/filepath"
"strings"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
@@ -43,10 +45,20 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
// Alright, let's merge the PDFs. // Alright, let's merge the PDFs.
outputPath := ctx.GeneratePath(".pdf") outputPath := ctx.GeneratePath("", ".pdf")
err = engine.Merge(ctx, ctx.Log(), inputPaths, outputPath) err = engine.Merge(ctx, ctx.Log(), inputPaths, outputPath)
if err != nil { if err != nil {
if errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded) {
return api.WrapError(
fmt.Errorf("merge PDFs: %w", err),
api.NewSentinelHttpError(
http.StatusTooManyRequests,
"The maximum queue size has been reached",
),
)
}
return fmt.Errorf("merge PDFs: %w", err) return fmt.Errorf("merge PDFs: %w", err)
} }
@@ -56,11 +68,20 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
zeroValued := gotenberg.PdfFormats{} zeroValued := gotenberg.PdfFormats{}
if pdfFormats != zeroValued { if pdfFormats != zeroValued {
convertInputPath := outputPath convertInputPath := outputPath
convertOutputPath := ctx.GeneratePath(".pdf") convertOutputPath := ctx.GeneratePath("", ".pdf")
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath) err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
if err != nil { if err != nil {
if errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded) {
return api.WrapError(
fmt.Errorf("convert PDF: %w", err),
api.NewSentinelHttpError(
http.StatusTooManyRequests,
"The maximum queue size has been reached",
),
)
}
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) { if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
return api.WrapError( return api.WrapError(
fmt.Errorf("convert PDF: %w", err), fmt.Errorf("convert PDF: %w", err),
@@ -133,15 +154,29 @@ func convertRoute(engine gotenberg.PdfEngine) api.Route {
) )
} }
// Alright, let's convert the PDFs. // Alright, let's convert the PDFs.s
outputPaths := make([]string, len(inputPaths)) outputPaths := make([]string, len(inputPaths))
for i, inputPath := range inputPaths { for i, inputPath := range inputPaths {
outputPaths[i] = ctx.GeneratePath(".pdf") if len(outputPaths) > 1 {
// If .zip archive, keep the original filenames.
outputPaths[i] = ctx.GeneratePath(strings.TrimSuffix(filepath.Base(inputPath), filepath.Ext(inputPath)), ".pdf")
} else {
outputPaths[i] = ctx.GeneratePath("", ".pdf")
}
err = engine.Convert(ctx, ctx.Log(), pdfFormats, inputPath, outputPaths[i]) err = engine.Convert(ctx, ctx.Log(), pdfFormats, inputPath, outputPaths[i])
if err != nil { if err != nil {
if errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded) {
return api.WrapError(
fmt.Errorf("convert PDF: %w", err),
api.NewSentinelHttpError(
http.StatusTooManyRequests,
"The maximum queue size has been reached",
),
)
}
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) { if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
return api.WrapError( return api.WrapError(
fmt.Errorf("convert PDF: %w", err), fmt.Errorf("convert PDF: %w", err),

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"net/http" "net/http"
"slices"
"testing" "testing"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
@@ -31,6 +32,27 @@ func TestMergeHandler(t *testing.T) {
expectHttpStatus: http.StatusBadRequest, expectHttpStatus: http.StatusBadRequest,
expectOutputPathsCount: 0, expectOutputPathsCount: 0,
}, },
{
scenario: "ErrMaximumQueueSizeExceeded (merge)",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"file.pdf": "/file.pdf",
"file2.pdf": "/file2.pdf",
})
return ctx
}(),
engine: &gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return gotenberg.ErrMaximumQueueSizeExceeded
},
},
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusTooManyRequests,
expectOutputPathsCount: 0,
},
{ {
scenario: "error from PDF engine", scenario: "error from PDF engine",
ctx: func() *api.ContextMock { ctx: func() *api.ContextMock {
@@ -90,7 +112,7 @@ func TestMergeHandler(t *testing.T) {
expectOutputPathsCount: 1, expectOutputPathsCount: 1,
}, },
{ {
scenario: "ErrPdfFormatNotSupported", scenario: "ErrMaximumQueueSizeExceeded (convert)",
ctx: func() *api.ContextMock { ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)} ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{ ctx.SetFiles(map[string]string{
@@ -104,6 +126,34 @@ func TestMergeHandler(t *testing.T) {
}) })
return ctx return ctx
}(), }(),
engine: &gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return gotenberg.ErrMaximumQueueSizeExceeded
},
},
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusTooManyRequests,
expectOutputPathsCount: 0,
},
{
scenario: "ErrPdfFormatNotSupported",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"file.pdf": "/file.pdf",
"file2.pdf": "/file2.pdf",
})
ctx.SetValues(map[string][]string{
"pdfa": {
"foo",
},
})
return ctx
}(),
engine: &gotenberg.PdfEngineMock{ engine: &gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil return nil
@@ -224,6 +274,7 @@ func TestConvertHandler(t *testing.T) {
expectHttpError bool expectHttpError bool
expectHttpStatus int expectHttpStatus int
expectOutputPathsCount int expectOutputPathsCount int
expectOutputPaths []string
}{ }{
{ {
scenario: "missing at least one mandatory file", scenario: "missing at least one mandatory file",
@@ -248,7 +299,7 @@ func TestConvertHandler(t *testing.T) {
expectOutputPathsCount: 0, expectOutputPathsCount: 0,
}, },
{ {
scenario: "ErrPdfFormatNotSupported", scenario: "ErrMaximumQueueSizeExceeded",
ctx: func() *api.ContextMock { ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)} ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{ ctx.SetFiles(map[string]string{
@@ -261,6 +312,30 @@ func TestConvertHandler(t *testing.T) {
}) })
return ctx return ctx
}(), }(),
engine: &gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return gotenberg.ErrMaximumQueueSizeExceeded
},
},
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusTooManyRequests,
expectOutputPathsCount: 0,
},
{
scenario: "ErrPdfFormatNotSupported",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"file.pdf": "/file.pdf",
})
ctx.SetValues(map[string][]string{
"pdfa": {
"foo",
},
})
return ctx
}(),
engine: &gotenberg.PdfEngineMock{ engine: &gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return gotenberg.ErrPdfFormatNotSupported return gotenberg.ErrPdfFormatNotSupported
@@ -370,6 +445,7 @@ func TestConvertHandler(t *testing.T) {
expectError: false, expectError: false,
expectHttpError: false, expectHttpError: false,
expectOutputPathsCount: 2, expectOutputPathsCount: 2,
expectOutputPaths: []string{"/file.pdf", "/file2.pdf"},
}, },
} { } {
t.Run(tc.scenario, func(t *testing.T) { t.Run(tc.scenario, func(t *testing.T) {
@@ -408,6 +484,12 @@ func TestConvertHandler(t *testing.T) {
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) { if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) {
t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths())) t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
} }
for _, path := range tc.expectOutputPaths {
if !slices.Contains(tc.ctx.OutputPaths(), path) {
t.Errorf("expected '%s' in output paths %v", path, tc.ctx.OutputPaths())
}
}
}) })
} }
} }

View File

@@ -9,14 +9,15 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/dlclark/regexp2"
"github.com/hashicorp/go-retryablehttp" "github.com/hashicorp/go-retryablehttp"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v8/pkg/modules/api" "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 deadline, ok := ctx.Deadline()
// allowed/denied lists. if !ok {
filter := func(URL, header string, allowList, denyList *regexp.Regexp) error { return errors.New("context has no deadline")
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
} }
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 { if err != nil {
return fmt.Errorf("filter webhook URL: %w", err) 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 { if err != nil {
return fmt.Errorf("filter webhook error URL: %w", err) return fmt.Errorf("filter webhook error URL: %w", err)
} }

View File

@@ -11,11 +11,11 @@ import (
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"regexp"
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/dlclark/regexp2"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"go.uber.org/zap" "go.uber.org/zap"
@@ -47,10 +47,10 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
buildWebhookModule := func() *Webhook { buildWebhookModule := func() *Webhook {
return &Webhook{ return &Webhook{
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
errorAllowList: regexp.MustCompile(""), errorAllowList: regexp2.MustCompile("", 0),
errorDenyList: regexp.MustCompile(""), errorDenyList: regexp2.MustCompile("", 0),
maxRetry: 0, maxRetry: 0,
retryMinWait: 0, retryMinWait: 0,
retryMaxWait: 0, retryMaxWait: 0,
@@ -63,6 +63,7 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
request *http.Request request *http.Request
mod *Webhook mod *Webhook
next echo.HandlerFunc next echo.HandlerFunc
noDeadline bool
expectError bool expectError bool
expectHttpError bool expectHttpError bool
expectHttpStatus int expectHttpStatus int
@@ -76,6 +77,7 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
return nil return nil
} }
}(), }(),
noDeadline: false,
expectError: false, expectError: false,
expectHttpError: false, expectHttpError: false,
}, },
@@ -87,10 +89,23 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
return req return req
}(), }(),
mod: buildWebhookModule(), mod: buildWebhookModule(),
noDeadline: false,
expectError: true, expectError: true,
expectHttpError: true, expectHttpError: true,
expectHttpStatus: http.StatusBadRequest, 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", scenario: "webhook URL is not allowed",
request: func() *http.Request { request: func() *http.Request {
@@ -101,9 +116,10 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
}(), }(),
mod: func() *Webhook { mod: func() *Webhook {
mod := buildWebhookModule() mod := buildWebhookModule()
mod.allowList = regexp.MustCompile("bar") mod.allowList = regexp2.MustCompile("bar", 0)
return mod return mod
}(), }(),
noDeadline: false,
expectError: true, expectError: true,
expectHttpError: true, expectHttpError: true,
expectHttpStatus: http.StatusForbidden, expectHttpStatus: http.StatusForbidden,
@@ -118,9 +134,10 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
}(), }(),
mod: func() *Webhook { mod: func() *Webhook {
mod := buildWebhookModule() mod := buildWebhookModule()
mod.denyList = regexp.MustCompile("foo") mod.denyList = regexp2.MustCompile("foo", 0)
return mod return mod
}(), }(),
noDeadline: false,
expectError: true, expectError: true,
expectHttpError: true, expectHttpError: true,
expectHttpStatus: http.StatusForbidden, expectHttpStatus: http.StatusForbidden,
@@ -135,9 +152,10 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
}(), }(),
mod: func() *Webhook { mod: func() *Webhook {
mod := buildWebhookModule() mod := buildWebhookModule()
mod.errorAllowList = regexp.MustCompile("foo") mod.errorAllowList = regexp2.MustCompile("foo", 0)
return mod return mod
}(), }(),
noDeadline: false,
expectError: true, expectError: true,
expectHttpError: true, expectHttpError: true,
expectHttpStatus: http.StatusForbidden, expectHttpStatus: http.StatusForbidden,
@@ -152,9 +170,10 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
}(), }(),
mod: func() *Webhook { mod: func() *Webhook {
mod := buildWebhookModule() mod := buildWebhookModule()
mod.errorDenyList = regexp.MustCompile("bar") mod.errorDenyList = regexp2.MustCompile("bar", 0)
return mod return mod
}(), }(),
noDeadline: false,
expectError: true, expectError: true,
expectHttpError: true, expectHttpError: true,
expectHttpStatus: http.StatusForbidden, expectHttpStatus: http.StatusForbidden,
@@ -169,6 +188,7 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
return req return req
}(), }(),
mod: buildWebhookModule(), mod: buildWebhookModule(),
noDeadline: false,
expectError: true, expectError: true,
expectHttpError: true, expectHttpError: true,
expectHttpStatus: http.StatusBadRequest, expectHttpStatus: http.StatusBadRequest,
@@ -183,6 +203,7 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
return req return req
}(), }(),
mod: buildWebhookModule(), mod: buildWebhookModule(),
noDeadline: false,
expectError: true, expectError: true,
expectHttpError: true, expectHttpError: true,
expectHttpStatus: http.StatusBadRequest, expectHttpStatus: http.StatusBadRequest,
@@ -198,6 +219,7 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
return req return req
}(), }(),
mod: buildWebhookModule(), mod: buildWebhookModule(),
noDeadline: false,
expectError: true, expectError: true,
expectHttpError: true, expectHttpError: true,
expectHttpStatus: http.StatusBadRequest, expectHttpStatus: http.StatusBadRequest,
@@ -213,6 +235,7 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
return req return req
}(), }(),
mod: buildWebhookModule(), mod: buildWebhookModule(),
noDeadline: false,
expectError: true, expectError: true,
expectHttpError: true, expectHttpError: true,
expectHttpStatus: http.StatusBadRequest, expectHttpStatus: http.StatusBadRequest,
@@ -228,6 +251,7 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
return req return req
}(), }(),
mod: buildWebhookModule(), mod: buildWebhookModule(),
noDeadline: false,
expectError: true, expectError: true,
expectHttpError: true, expectHttpError: true,
expectHttpStatus: http.StatusBadRequest, expectHttpStatus: http.StatusBadRequest,
@@ -242,6 +266,7 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
return req return req
}(), }(),
mod: buildWebhookModule(), mod: buildWebhookModule(),
noDeadline: false,
expectError: true, expectError: true,
expectHttpError: true, expectHttpError: true,
expectHttpStatus: http.StatusBadRequest, expectHttpStatus: http.StatusBadRequest,
@@ -254,15 +279,20 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
c := srv.NewContext(tc.request, httptest.NewRecorder()) c := srv.NewContext(tc.request, httptest.NewRecorder())
ctx := &api.ContextMock{Context: &api.Context{}} if tc.noDeadline {
ctx.SetEchoContext(c) ctx := &api.ContextMock{Context: &api.Context{Context: context.Background()}}
ctx.SetEchoContext(c)
c.Set("context", ctx.Context) c.Set("context", ctx.Context)
c.Set("cancel", func() context.CancelFunc { c.Set("cancel", func() context.CancelFunc {
return func() { return nil
return }())
} } 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) err := webhookMiddleware(tc.mod).Handler(tc.next)(c)
@@ -320,10 +350,10 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) {
buildWebhookModule := func() *Webhook { buildWebhookModule := func() *Webhook {
return &Webhook{ return &Webhook{
allowList: regexp.MustCompile(""), allowList: regexp2.MustCompile("", 0),
denyList: regexp.MustCompile(""), denyList: regexp2.MustCompile("", 0),
errorAllowList: regexp.MustCompile(""), errorAllowList: regexp2.MustCompile("", 0),
errorDenyList: regexp.MustCompile(""), errorDenyList: regexp2.MustCompile("", 0),
maxRetry: 0, maxRetry: 0,
retryMinWait: 0, retryMinWait: 0,
retryMaxWait: 0, retryMaxWait: 0,
@@ -426,22 +456,17 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) {
c.Set("trace", "foo") c.Set("trace", "foo")
c.Set("startTime", time.Now()) 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.SetLogger(zap.NewNop())
ctx.SetEchoContext(c) ctx.SetEchoContext(c)
c.Set("context", ctx.Context) c.Set("context", ctx.Context)
c.Set("cancel", func() context.CancelFunc { c.Set("cancel", cancel)
return func() {
return
}
}())
webhook := echo.New() webhook := echo.New()
webhook.HideBanner = true webhook.HideBanner = true
webhook.HidePort = true webhook.HidePort = true
rand.Seed(time.Now().UnixNano())
webhookPort := rand.Intn(65535-1025+1) + 1025 webhookPort := rand.Intn(65535-1025+1) + 1025
c.Request().Header.Set("Gotenberg-Webhook-Url", fmt.Sprintf("http://localhost:%d/", webhookPort)) c.Request().Header.Set("Gotenberg-Webhook-Url", fmt.Sprintf("http://localhost:%d/", webhookPort))

View File

@@ -1,9 +1,9 @@
package webhook package webhook
import ( import (
"regexp"
"time" "time"
"github.com/dlclark/regexp2"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg" "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 // Webhook is a module which provides a middleware for uploading output files
// to any destinations in an asynchronous fashion. // to any destinations in an asynchronous fashion.
type Webhook struct { type Webhook struct {
allowList *regexp.Regexp allowList *regexp2.Regexp
denyList *regexp.Regexp denyList *regexp2.Regexp
errorAllowList *regexp.Regexp errorAllowList *regexp2.Regexp
errorDenyList *regexp.Regexp errorDenyList *regexp2.Regexp
maxRetry int maxRetry int
retryMinWait time.Duration retryMinWait time.Duration
retryMaxWait time.Duration retryMaxWait time.Duration

View File

@@ -2,8 +2,8 @@
<head> <head>
<style> <style>
body { body {
font-size: 30rem; font-size: 12px;
margin: 4rem auto; margin: auto 20px;
} }
</style> </style>
</head> </head>

View File

@@ -2,8 +2,8 @@
<head> <head>
<style> <style>
body { body {
font-size: 8rem; font-size: 12px;
margin: 4rem auto; margin: auto 20px;
} }
</style> </style>
</head> </head>

View File

@@ -59,6 +59,14 @@
<p id="javascript" style="display: none">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p> <p id="javascript" style="display: none">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</div> </div>
<div class="page-break-after">
<h2>/etc/passwd</h2>
<iframe src="/etc/passwd"></iframe>
<h2>\\localhost/etc/passwd</h2>
<iframe src="\\localhost/etc/passwd"></iframe>
</div>
<script type="application/javascript"> <script type="application/javascript">
var globalVar = 'notReady' var globalVar = 'notReady'

View File

@@ -2,8 +2,8 @@
<head> <head>
<style> <style>
body { body {
font-size: 8rem; font-size: 12px;
margin: 4rem auto; margin: auto 20px;
} }
</style> </style>
</head> </head>

View File

@@ -2,8 +2,8 @@
<head> <head>
<style> <style>
body { body {
font-size: 8rem; font-size: 12px;
margin: 4rem auto; margin: auto 20px;
} }
</style> </style>
</head> </head>