Compare commits

...

19 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
Julien Neuhart
b8926350b9 fix(libreoffice): wrong HTTP status codes if invalid PDF formats 2024-01-18 12:15:39 +01:00
Julien Neuhart
e5194b313d chore: update Go dependencies 2024-01-18 12:15:39 +01:00
Julien Neuhart
94228375ad chore: update Go dependencies 2024-01-10 11:55:05 +01:00
35 changed files with 1014 additions and 390 deletions

View File

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

View File

@@ -10,9 +10,9 @@ DOCKER_REPOSITORY=gotenberg
GOTENBERG_VERSION=snapshot
GOTENBERG_USER_GID=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.
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
build: ## Build the Gotenberg's Docker image
@@ -35,6 +35,7 @@ API_ROOT_PATH=/
API_TRACE_HEADER=Gotenberg-Trace
API_DISABLE_HEALTH_CHECK_LOGGING=false
CHROMIUM_RESTART_AFTER=0
CHROMIUM_MAX_QUEUE_SIZE=0
CHROMIUM_AUTO_START=false
CHROMIUM_START_TIMEOUT=20s
CHROMIUM_INCOGNITO=false
@@ -45,12 +46,13 @@ CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES=false
CHROMIUM_HOST_RESOLVER_RULES=
CHROMIUM_PROXY_SERVER=
CHROMIUM_ALLOW_LIST=
CHROMIUM_DENY_LIST="^file:///[^tmp].*"
CHROMIUM_DENY_LIST=^file:(?!//\/tmp/).*
CHROMIUM_CLEAR_CACHE=false
CHROMIUM_CLEAR_COOKIES=false
CHROMIUM_DISABLE_JAVASCRIPT=false
CHROMIUM_DISABLE_ROUTES=false
LIBREOFFICE_RESTART_AFTER=10
LIBREOFFICE_MAX_QUEUE_SIZE=0
LIBREOFFICE_AUTO_START=false
LIBREOFFICE_START_TIMEOUT=20s
LIBREOFFICE_DISABLE_ROUTES=false
@@ -89,6 +91,7 @@ run: ## Start a Gotenberg container
--api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \
--chromium-restart-after=$(CHROMIUM_RESTART_AFTER) \
--chromium-auto-start=$(CHROMIUM_AUTO_START) \
--chromium-max-queue-size=$(CHROMIUM_MAX_QUEUE_SIZE) \
--chromium-start-timeout=$(CHROMIUM_START_TIMEOUT) \
--chromium-incognito=$(CHROMIUM_INCOGNITO) \
--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-host-resolver-rules=$(CHROMIUM_HOST_RESOLVER_RULES) \
--chromium-proxy-server=$(CHROMIUM_PROXY_SERVER) \
--chromium-allow-list=$(CHROMIUM_ALLOW_LIST) \
--chromium-deny-list=$(CHROMIUM_DENY_LIST) \
--chromium-allow-list="$(CHROMIUM_ALLOW_LIST)" \
--chromium-deny-list="$(CHROMIUM_DENY_LIST)" \
--chromium-clear-cache=$(CHROMIUM_CLEAR_CACHE) \
--chromium-clear-cookies=$(CHROMIUM_CLEAR_COOKIES) \
--chromium-disable-javascript=$(CHROMIUM_DISABLE_JAVASCRIPT) \
--chromium-disable-routes=$(CHROMIUM_DISABLE_ROUTES) \
--libreoffice-restart-after=$(LIBREOFFICE_RESTART_AFTER) \
--libreoffice-max-queue-size=$(LIBREOFFICE_MAX_QUEUE_SIZE) \
--libreoffice-auto-start=$(LIBREOFFICE_AUTO_START) \
--libreoffice-start-timeout=$(LIBREOFFICE_START_TIMEOUT) \
--libreoffice-disable-routes=$(LIBREOFFICE_DISABLE_ROUTES) \
@@ -116,8 +120,8 @@ run: ## Start a Gotenberg container
--prometheus-collect-interval=$(PROMETHEUS_COLLECT_INTERVAL) \
--prometheus-disable-route-logging=$(PROMETHEUS_DISABLE_ROUTE_LOGGING) \
--prometheus-disable-collect=$(PROMETHEUS_DISABLE_COLLECT) \
--webhook-allow-list=$(WEBHOOK_ALLOW_LIST) \
--webhook-deny-list=$(WEBHOOK_DENY_LIST) \
--webhook-allow-list="$(WEBHOOK_ALLOW_LIST)" \
--webhook-deny-list="$(WEBHOOK_DENY_LIST)" \
--webhook-error-allow-list=$(WEBHOOK_ERROR_ALLOW_LIST) \
--webhook-error-deny-list=$(WEBHOOK_ERROR_DENY_LIST) \
--webhook-max-retry=$(WEBHOOK_MAX_RETRY) \

View File

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

35
go.mod
View File

@@ -4,14 +4,14 @@ go 1.21
require (
github.com/alexliesenfeld/health v0.8.0
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/chromedp/cdproto v0.0.0-20231205062650-00455a960d61
github.com/chromedp/chromedp v0.9.3
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/chromedp/cdproto v0.0.0-20240214232516-ad4608604e9e
github.com/chromedp/chromedp v0.9.5
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-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/labstack/echo/v4 v4.11.4
github.com/labstack/gommon v0.4.2
@@ -20,22 +20,24 @@ require (
github.com/microcosm-cc/bluemonday v1.0.26
github.com/nwaples/rardecode v1.1.3 // indirect
github.com/pdfcpu/pdfcpu v0.6.0
github.com/pierrec/lz4/v4 v4.1.19 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/prometheus/client_golang v1.18.0
github.com/russross/blackfriday/v2 v2.1.0
github.com/spf13/pflag v1.0.5
github.com/ulikunitz/xz v0.5.11 // indirect
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.26.0
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/image v0.14.0 // indirect
golang.org/x/net v0.19.0
golang.org/x/sync v0.5.0
golang.org/x/sys v0.15.0 // indirect
golang.org/x/term v0.15.0
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/image v0.15.0 // indirect
golang.org/x/net v0.21.0
golang.org/x/sync v0.6.0
golang.org/x/sys v0.17.0 // indirect
golang.org/x/term v0.17.0
golang.org/x/text v0.14.0
)
require github.com/dlclark/regexp2 v1.10.0
require (
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
@@ -44,7 +46,7 @@ require (
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.3.1 // indirect
github.com/gobwas/ws v1.3.2 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/hhrutter/lzw v1.0.0 // indirect
github.com/hhrutter/tiff v1.0.1 // indirect
@@ -52,12 +54,11 @@ require (
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/client_model v0.6.0 // indirect
github.com/prometheus/common v0.47.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/fasttemplate v1.2.2 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect

76
go.sum
View File

@@ -1,23 +1,25 @@
github.com/alexliesenfeld/health v0.8.0 h1:lCV0i+ZJPTbqP7LfKG7p3qZBl5VhelwUFCIVWl77fgk=
github.com/alexliesenfeld/health v0.8.0/go.mod h1:TfNP0f+9WQVWMQRzvMUjlws4ceXKEL3WR+6Hp95HUFc=
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
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/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
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-20231205062650-00455a960d61 h1:XD280QPATe9jaz20dylKe3vBsNcH1w3mkssGY0lidn8=
github.com/chromedp/cdproto v0.0.0-20231205062650-00455a960d61/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/chromedp v0.9.3 h1:Wq58e0dZOdHsxaj9Owmfcf+ibtpYN1N0FWVbaxa/esg=
github.com/chromedp/chromedp v0.9.3/go.mod h1:NipeUkUcuzIdFbBP8eNNvl9upcceOfWzoJn6cRe4ksA=
github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/cdproto v0.0.0-20240214232516-ad4608604e9e h1:kXEolCWQZzuEFcuaTzfqXToX+e29OcvK87BcBiBBJ1c=
github.com/chromedp/cdproto v0.0.0-20240214232516-ad4608604e9e/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/chromedp v0.9.5 h1:viASzruPJOiThk7c5bueOUY91jGLJVximoEMGoH93rg=
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/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
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/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/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
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/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
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.1 h1:Qi34dfLMWJbiKaNbDVzM9x27nZBjmkaW6i4+Ku+pGVU=
github.com/gobwas/ws v1.3.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q=
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.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
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.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
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/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
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/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.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
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/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
@@ -77,8 +78,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
@@ -91,23 +90,23 @@ github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzb
github.com/pdfcpu/pdfcpu v0.6.0 h1:z4kARP5bcWa39TTYMcN/kjBnm7MvhTWjXgeYmkdAGMI=
github.com/pdfcpu/pdfcpu v0.6.0/go.mod h1:kmpD0rk8YnZj0l3qSeGBlAB+XszHUgNv//ORH/E7EYo=
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.19 h1:tYLzDnjDXh9qIxSTKHwXwOYmm9d887Y7Y1ZkyXYHAN4=
github.com/pierrec/lz4/v4 v4.1.19/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
github.com/prometheus/common v0.47.0 h1:p5Cz0FNHo7SnWOmWmoRozVcjEp0bIVU8cV7OShpjL1k=
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/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
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.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
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/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
@@ -135,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/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
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/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
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/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
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/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

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

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].
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
// and provides methods for starting, stopping, and checking the health of the
// process.
@@ -74,6 +78,7 @@ type processSupervisor struct {
logger *zap.Logger
process Process
maxReqLimit int64
maxQueueSize int64
mutexChan chan struct{}
firstStart atomic.Bool
reqCounter atomic.Int64
@@ -83,12 +88,13 @@ type processSupervisor struct {
}
// 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{
logger: logger,
process: process,
mutexChan: make(chan struct{}, 1),
maxReqLimit: maxReqLimit,
logger: logger,
process: process,
mutexChan: make(chan struct{}, 1),
maxReqLimit: maxReqLimit,
maxQueueSize: maxQueueSize,
}
b.reqCounter.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 {
currentQueueSize := s.reqQueueSize.Load()
if s.maxQueueSize > 0 && currentQueueSize >= s.maxQueueSize {
return ErrMaximumQueueSizeExceeded
}
s.reqQueueSize.Add(1)
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 {
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()
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 {
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 {
ps.firstStart.Store(true)
}
@@ -249,6 +249,8 @@ func TestProcessSupervisor_Run(t *testing.T) {
expectedStartCalls int64
expectedHealthyCalls int64
expectedStopCalls int64
currentQueueSize int64
maxQueueSize int64
}{
{
scenario: "successfully run task on non-started process",
@@ -348,6 +350,34 @@ func TestProcessSupervisor_Run(t *testing.T) {
expectedHealthyCalls: 1,
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) {
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 {
ps.firstStart.Store(true)
}
if tc.isRestarting {
ps.isRestarting.Store(true)
}
if tc.currentQueueSize > 0 {
ps.reqQueueSize.Store(tc.currentQueueSize)
}
task := func() error {
return tc.taskError
@@ -451,13 +484,13 @@ func TestProcessSupervisor_runWithDeadline(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)
defer cancel()
if tc.ctxDone {
cancel()
} else {
defer cancel()
}
err := ps.runWithDeadline(ctx, func() error {
@@ -485,7 +518,7 @@ func TestProcessSupervisor_ReqQueueSize(t *testing.T) {
return true
},
}
ps := NewProcessSupervisor(logger, process, 0).(*processSupervisor)
ps := NewProcessSupervisor(logger, process, 0, 0).(*processSupervisor)
// Simulating a lock.
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)
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 _, fh := range files {
err = copyToDisk(fh)
if err != nil {
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
// does not create a file.
func (ctx *Context) GeneratePath(extension string) string {
return fmt.Sprintf("%s/%s%s", ctx.dirPath, uuid.New(), extension)
// GeneratePath generates a path within the context's working directory.
// It either generates a new UUID-based filename or uses the provided filename.
// It does not create a file.
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
@@ -246,7 +250,7 @@ func (ctx *Context) BuildOutputFile() (string, error) {
ImplicitTopLevelFolder: false,
}
archivePath := ctx.GeneratePath(".zip")
archivePath := ctx.GeneratePath("", ".zip")
err := z.Archive(ctx.outputPaths, archivePath)
if err != nil {

View File

@@ -190,11 +190,15 @@ func TestContext_GeneratePath(t *testing.T) {
dirPath: "/foo",
}
path := ctx.GeneratePath(".pdf")
path := ctx.GeneratePath("", ".pdf")
if !strings.HasPrefix(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) {

View File

@@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"os"
"regexp"
"strings"
"sync"
"sync/atomic"
@@ -15,6 +14,7 @@ import (
"github.com/chromedp/cdproto/network"
"github.com/chromedp/cdproto/runtime"
"github.com/chromedp/chromedp"
"github.com/dlclark/regexp2"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
@@ -39,8 +39,8 @@ type browserArguments struct {
wsUrlReadTimeout time.Duration
// Tasks specific.
allowList *regexp.Regexp
denyList *regexp.Regexp
allowList *regexp2.Regexp
denyList *regexp2.Regexp
clearCache bool
clearCookies bool
disableJavaScript bool
@@ -263,20 +263,17 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string
return errors.New("browser not started, cannot handle tasks")
}
// We validate the "main" URL against our allow / deny lists.
if !b.arguments.allowList.MatchString(url) {
return fmt.Errorf("'%s' does not match the expression from the allowed list: %w", url, ErrUrlNotAuthorized)
}
if b.arguments.denyList.String() != "" && b.arguments.denyList.MatchString(url) {
return fmt.Errorf("'%s' matches the expression from the denied list: %w", url, ErrUrlNotAuthorized)
}
deadline, ok := ctx.Deadline()
if !ok {
return errors.New("context has no deadline")
}
// We validate the "main" URL against our allow / deny lists.
err := gotenberg.FilterDeadline(b.arguments.allowList, b.arguments.denyList, url, deadline)
if err != nil {
return fmt.Errorf("filter URL: %w", err)
}
b.ctxMu.RLock()
defer b.ctxMu.RUnlock()
@@ -310,7 +307,7 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string
listenForEventExceptionThrown(taskCtx, logger, &consoleExceptions, &consoleExceptionsMu)
}
err := chromedp.Run(taskCtx, tasks...)
err = chromedp.Run(taskCtx, tasks...)
if err != nil {
errMessage := err.Error()

View File

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

View File

@@ -20,10 +20,6 @@ func init() {
}
var (
// ErrUrlNotAuthorized happens if a URL is not acceptable according to the
// allowed/denied lists.
ErrUrlNotAuthorized = errors.New("URL not authorized")
// ErrInvalidEmulatedMediaType happens if the emulated media type is not
// "screen" nor "print". Empty value are allowed though.
ErrInvalidEmulatedMediaType = errors.New("invalid emulated media type")
@@ -280,6 +276,7 @@ func (mod *Chromium) Descriptor() gotenberg.ModuleDescriptor {
FlagSet: func() *flag.FlagSet {
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-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.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")
@@ -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-proxy-server", "", "Set the outbound proxy server; this switch only affects HTTP and HTTPS requests")
fs.String("chromium-allow-list", "", "Set the allowed URLs for Chromium using a regular expression")
fs.String("chromium-deny-list", "^file:///[^tmp].*", "Set the denied URLs for Chromium using a regular expression")
fs.String("chromium-deny-list", `^file:(?!//\/tmp/).*`, "Set the denied URLs for Chromium using a regular expression")
fs.Bool("chromium-clear-cache", false, "Clear Chromium cache between each conversion")
fs.Bool("chromium-clear-cookies", false, "Clear Chromium cookies between each conversion")
fs.Bool("chromium-disable-javascript", false, "Disable JavaScript")
@@ -344,7 +341,7 @@ func (mod *Chromium) Provision(ctx *gotenberg.Context) error {
// Process.
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.
provider, err := ctx.Module(new(gotenberg.PdfEngineProvider))

View File

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

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)
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 {
outputPath := ctx.GeneratePath(".pdf")
outputPath := ctx.GeneratePath("", ".pdf")
err := chromium.Pdf(ctx, ctx.Log(), url, outputPath, options)
err = handleChromiumError(err, url, options.Options)
if err != nil {
if errors.Is(err, ErrOmitBackgroundWithoutPrintBackground) {
return api.WrapError(
err,
fmt.Errorf("convert to PDF: %w", err),
api.NewSentinelHttpError(
http.StatusBadRequest,
"omitBackground requires printBackground set to true",
@@ -549,11 +549,20 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
zeroValued := gotenberg.PdfFormats{}
if pdfFormats != zeroValued {
convertInputPath := outputPath
convertOutputPath := ctx.GeneratePath(".pdf")
convertOutputPath := ctx.GeneratePath("", ".pdf")
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
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) {
return api.WrapError(
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 {
ext := fmt.Sprintf(".%s", options.Format)
outputPath := ctx.GeneratePath(ext)
outputPath := ctx.GeneratePath("", ext)
err := chromium.Screenshot(ctx, ctx.Log(), url, outputPath, options)
err = handleChromiumError(err, url, options.Options)
@@ -602,7 +611,17 @@ func handleChromiumError(err error, url string, options Options) error {
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(
err,
api.NewSentinelHttpError(

View File

@@ -1231,10 +1231,22 @@ func TestConvertUrl(t *testing.T) {
expectOutputPathsCount int
}{
{
scenario: "ErrUrlNotAuthorized",
scenario: "ErrMaximumQueueSizeExceeded",
ctx: &api.ContextMock{Context: new(api.Context)},
api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
return ErrUrlNotAuthorized
return gotenberg.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(),
expectError: true,
@@ -1341,6 +1353,22 @@ func TestConvertUrl(t *testing.T) {
expectHttpError: false,
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",
ctx: &api.ContextMock{Context: new(api.Context)},
@@ -1463,10 +1491,22 @@ func TestScreenshotUrl(t *testing.T) {
expectOutputPathsCount int
}{
{
scenario: "ErrUrlNotAuthorized",
scenario: "ErrMaximumQueueSizeExceeded",
ctx: &api.ContextMock{Context: new(api.Context)},
api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
return ErrUrlNotAuthorized
return gotenberg.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(),
expectError: true,

View File

@@ -21,9 +21,9 @@ func init() {
}
var (
// ErrInvalidPdfFormat happens if the PDF format option cannot be handled
// ErrInvalidPdfFormats happens if the PDF formats option cannot be handled
// by LibreOffice.
ErrInvalidPdfFormat = errors.New("invalid PDF format")
ErrInvalidPdfFormats = errors.New("invalid PDF formats")
// ErrMalformedPageRanges happens if the page ranges option cannot be
// interpreted by LibreOffice.
@@ -80,6 +80,7 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
FlagSet: func() *flag.FlagSet {
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-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.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.
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
}

View File

@@ -282,7 +282,7 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputP
case gotenberg.PdfA3b:
args = append(args, "--export", "SelectPdfVersion=3")
default:
return ErrInvalidPdfFormat
return ErrInvalidPdfFormats
}
if options.PdfFormats.PdfUa {

View File

@@ -235,7 +235,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
expectError: true,
},
{
scenario: "ErrInvalidPdfFormat",
scenario: "ErrInvalidPdfFormats",
libreOffice: func() libreOffice {
p := new(libreOfficeProcess)
p.socketPort = 12345
@@ -247,7 +247,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
cancelledCtx: false,
start: false,
expectError: true,
expectedError: ErrInvalidPdfFormat,
expectedError: ErrInvalidPdfFormats,
},
{
scenario: "ErrMalformedPageRanges",

View File

@@ -64,7 +64,7 @@ func (engine *LibreOfficePdfEngine) Convert(ctx context.Context, logger *zap.Log
return nil
}
if errors.Is(err, api.ErrInvalidPdfFormat) {
if errors.Is(err, api.ErrInvalidPdfFormats) {
return fmt.Errorf("convert PDF to '%+v' with LibreOffice: %w", formats, gotenberg.ErrPdfFormatNotSupported)
}

View File

@@ -137,7 +137,7 @@ func TestLibreOfficePdfEngine_Convert(t *testing.T) {
scenario: "invalid PDF format",
api: &api.ApiMock{
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options api.Options) error {
return api.ErrInvalidPdfFormat
return api.ErrInvalidPdfFormats
},
},
expectError: true,

View File

@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net/http"
"path/filepath"
"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.
outputPaths := make([]string, len(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{
Landscape: landscape,
PageRanges: nativePageRanges,
@@ -67,6 +68,26 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
err = libreOffice.Pdf(ctx, ctx.Log(), inputPath, outputPaths[i], options)
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) {
return api.WrapError(
fmt.Errorf("convert to PDF: %w", err),
api.NewSentinelHttpError(
http.StatusBadRequest,
fmt.Sprintf("A PDF format in '%+v' is not supported", pdfFormats),
),
)
}
if errors.Is(err, libreofficeapi.ErrMalformedPageRanges) {
return api.WrapError(
fmt.Errorf("convert to PDF: %w", err),
@@ -82,22 +103,42 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
// win: if there is only one PDF, skip this step.
if len(outputPaths) > 1 && merge {
outputPath := ctx.GeneratePath(".pdf")
outputPath := ctx.GeneratePath("", ".pdf")
err = engine.Merge(ctx, ctx.Log(), outputPaths, outputPath)
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)
}
// Now, let's check if the client want to convert this result
// PDF to specific PDF formats.
// Now, let's check if the client want to convert this
// resulting PDF to specific PDF formats.
zeroValued := gotenberg.PdfFormats{}
if !nativePdfFormats && pdfFormats != zeroValued {
convertInputPath := outputPath
convertOutputPath := ctx.GeneratePath(".pdf")
convertOutputPath := ctx.GeneratePath("", ".pdf")
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
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) {
return api.WrapError(
fmt.Errorf("convert PDF: %w", err),
@@ -134,10 +175,21 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
for i, outputPath := range outputPaths {
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])
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) {
return api.WrapError(
fmt.Errorf("convert PDF: %w", err),

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"net/http"
"slices"
"testing"
"github.com/labstack/echo/v4"
@@ -25,6 +26,7 @@ func TestConvertRoute(t *testing.T) {
expectHttpError bool
expectHttpStatus int
expectOutputPathsCount int
expectOutputPaths []string
}{
{
scenario: "missing at least one mandatory file",
@@ -37,6 +39,50 @@ func TestConvertRoute(t *testing.T) {
expectHttpStatus: http.StatusBadRequest,
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)",
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 libreofficeapi.ErrInvalidPdfFormats
},
ExtensionsMock: func() []string {
return []string{".docx"}
},
},
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusBadRequest,
expectOutputPathsCount: 0,
},
{
scenario: "ErrMalformedPageRanges",
ctx: func() *api.ContextMock {
@@ -44,6 +90,11 @@ func TestConvertRoute(t *testing.T) {
ctx.SetFiles(map[string]string{
"document.docx": "/document.docx",
})
ctx.SetValues(map[string][]string{
"pdfa": {
"foo",
},
})
return ctx
}(),
libreOffice: &libreofficeapi.ApiMock{
@@ -80,6 +131,41 @@ func TestConvertRoute(t *testing.T) {
expectHttpError: false,
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)",
ctx: func() *api.ContextMock {
@@ -191,6 +277,7 @@ func TestConvertRoute(t *testing.T) {
expectError: false,
expectHttpError: false,
expectOutputPathsCount: 1,
expectOutputPaths: []string{"/document.docx.pdf"},
},
{
scenario: "success (many files)",
@@ -199,6 +286,7 @@ func TestConvertRoute(t *testing.T) {
ctx.SetFiles(map[string]string{
"document.docx": "/document.docx",
"document2.docx": "/document2.docx",
"document2.doc": "/document2.doc",
})
return ctx
}(),
@@ -207,19 +295,21 @@ func TestConvertRoute(t *testing.T) {
return nil
},
ExtensionsMock: func() []string {
return []string{".docx"}
return []string{".docx", ".doc"}
},
},
expectError: 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 := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"document.docx": "/document.docx",
"document.docx": "/document.docx",
"document2.docx": "/document2.docx",
})
ctx.SetValues(map[string][]string{
"pdfa": {
@@ -249,14 +339,16 @@ func TestConvertRoute(t *testing.T) {
},
expectError: 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 := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"document.docx": "/document.docx",
"document.docx": "/document.docx",
"document2.docx": "/document2.docx",
})
ctx.SetValues(map[string][]string{
"pdfa": {
@@ -283,7 +375,41 @@ func TestConvertRoute(t *testing.T) {
},
expectError: 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",
@@ -317,6 +443,48 @@ func TestConvertRoute(t *testing.T) {
expectHttpError: false,
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)",
ctx: func() *api.ContextMock {
@@ -590,6 +758,12 @@ func TestConvertRoute(t *testing.T) {
if 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"
"fmt"
"net/http"
"path/filepath"
"strings"
"github.com/labstack/echo/v4"
@@ -43,10 +45,20 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
// Alright, let's merge the PDFs.
outputPath := ctx.GeneratePath(".pdf")
outputPath := ctx.GeneratePath("", ".pdf")
err = engine.Merge(ctx, ctx.Log(), inputPaths, outputPath)
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)
}
@@ -56,11 +68,20 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
zeroValued := gotenberg.PdfFormats{}
if pdfFormats != zeroValued {
convertInputPath := outputPath
convertOutputPath := ctx.GeneratePath(".pdf")
convertOutputPath := ctx.GeneratePath("", ".pdf")
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
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) {
return api.WrapError(
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))
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])
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) {
return api.WrapError(
fmt.Errorf("convert PDF: %w", err),

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"net/http"
"slices"
"testing"
"github.com/labstack/echo/v4"
@@ -31,6 +32,27 @@ func TestMergeHandler(t *testing.T) {
expectHttpStatus: http.StatusBadRequest,
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",
ctx: func() *api.ContextMock {
@@ -90,7 +112,7 @@ func TestMergeHandler(t *testing.T) {
expectOutputPathsCount: 1,
},
{
scenario: "ErrPdfFormatNotSupported",
scenario: "ErrMaximumQueueSizeExceeded (convert)",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
@@ -104,6 +126,34 @@ func TestMergeHandler(t *testing.T) {
})
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{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
@@ -224,6 +274,7 @@ func TestConvertHandler(t *testing.T) {
expectHttpError bool
expectHttpStatus int
expectOutputPathsCount int
expectOutputPaths []string
}{
{
scenario: "missing at least one mandatory file",
@@ -248,7 +299,7 @@ func TestConvertHandler(t *testing.T) {
expectOutputPathsCount: 0,
},
{
scenario: "ErrPdfFormatNotSupported",
scenario: "ErrMaximumQueueSizeExceeded",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
@@ -261,6 +312,30 @@ func TestConvertHandler(t *testing.T) {
})
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{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return gotenberg.ErrPdfFormatNotSupported
@@ -370,6 +445,7 @@ func TestConvertHandler(t *testing.T) {
expectError: false,
expectHttpError: false,
expectOutputPathsCount: 2,
expectOutputPaths: []string{"/file.pdf", "/file2.pdf"},
},
} {
t.Run(tc.scenario, func(t *testing.T) {
@@ -408,6 +484,12 @@ func TestConvertHandler(t *testing.T) {
if 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"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/dlclark/regexp2"
"github.com/hashicorp/go-retryablehttp"
"github.com/labstack/echo/v4"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
)
@@ -44,38 +45,38 @@ func webhookMiddleware(w *Webhook) api.Middleware {
)
}
// Let's check if the webhook URLs are acceptable according to our
// allowed/denied lists.
filter := func(URL, header string, allowList, denyList *regexp.Regexp) error {
if !allowList.MatchString(URL) {
return api.WrapError(
fmt.Errorf("'%s' does not match the expression from the allowed list", URL),
api.NewSentinelHttpError(
http.StatusForbidden,
fmt.Sprintf("Invalid '%s' header value: '%s' does not match the authorized URLs", header, URL),
),
)
}
if denyList.String() != "" && denyList.MatchString(URL) {
return api.WrapError(
fmt.Errorf("'%s' matches the expression from the denied list", URL),
api.NewSentinelHttpError(
http.StatusForbidden,
fmt.Sprintf("Invalid '%s' header value: '%s' does not match the authorized URLs", header, URL),
),
)
}
return nil
deadline, ok := ctx.Deadline()
if !ok {
return errors.New("context has no deadline")
}
err := filter(webhookUrl, "Gotenberg-Webhook-Url", w.allowList, w.denyList)
// Let's check if the webhook URLs are acceptable according to our
// allowed/denied lists.
filter := func(url, header string, allowList, denyList *regexp2.Regexp, deadline time.Time) error {
err := gotenberg.FilterDeadline(allowList, denyList, url, deadline)
if err == nil {
return nil
}
if errors.Is(err, gotenberg.ErrFiltered) {
return api.WrapError(
err,
api.NewSentinelHttpError(
http.StatusForbidden,
fmt.Sprintf("Invalid '%s' header value: '%s' does not match the authorized URL", header, url),
),
)
}
return err
}
err := filter(webhookUrl, "Gotenberg-Webhook-Url", w.allowList, w.denyList, deadline)
if err != nil {
return fmt.Errorf("filter webhook URL: %w", err)
}
err = filter(webhookErrorUrl, "Gotenberg-Webhook-Error-Url", w.errorAllowList, w.errorDenyList)
err = filter(webhookErrorUrl, "Gotenberg-Webhook-Error-Url", w.errorAllowList, w.errorDenyList, deadline)
if err != nil {
return fmt.Errorf("filter webhook error URL: %w", err)
}

View File

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

View File

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

View File

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

View File

@@ -2,8 +2,8 @@
<head>
<style>
body {
font-size: 8rem;
margin: 4rem auto;
font-size: 12px;
margin: auto 20px;
}
</style>
</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>
</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">
var globalVar = 'notReady'

View File

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

View File

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