Compare commits

..

29 Commits

Author SHA1 Message Date
Julien Neuhart
697fa08d8b chore(godoc): add breaklines 2024-06-13 11:14:24 +02:00
garethjudson
669a35afef feat(libreoffice): add image options (#898)
* feat: libre office image options

* refactor: PR comment changes

* fix: change lossLessImageCompression default to false

---------

Co-authored-by: Gareth Judson <gareth.judson@delwp.vic.gov.au>
2024-06-13 10:28:36 +02:00
Julien Neuhart
b1d94a3845 feat(chromium): add user agent override 2024-06-12 20:51:10 +02:00
Julien Neuhart
06869cf491 chore(deps): update Golang dependencies 2024-06-12 20:51:10 +02:00
Julien Neuhart
ed67d8514c chore(deps): update golangci-lint 2024-06-04 17:02:05 +02:00
Julien Neuhart
07ec2f3d7b chore(deps): update Golang dependencies 2024-06-04 17:00:44 +02:00
Jonas Geiler
73d02e85c4 refactor(api): improved tests and test messages
Co-authored-by: Julien Neuhart <neuhart.julien@gmail.com>
2024-05-27 14:11:37 +02:00
Jonas Geiler
c1cf1eb37a chore: removed TODO.txt from .gitignore file 2024-05-27 14:11:37 +02:00
Jonas Geiler
7aecc94049 refactor(api): re-use error when starting the API server
Co-authored-by: Julien Neuhart <neuhart.julien@gmail.com>
2024-05-27 14:11:37 +02:00
Jonas Geiler
b55fbfeb8e refactor(api): updated tls validation error message
Co-authored-by: Julien Neuhart <neuhart.julien@gmail.com>
2024-05-27 14:11:37 +02:00
Jonas Geiler
20b8991fa4 feat(api): added tls/ssl support 2024-05-27 14:11:37 +02:00
Julien Neuhart
e50de4201d chore(deps): update Go dependencies 2024-05-22 09:44:19 +02:00
Julien Neuhart
ba2b876ee4 chore(deps): update golangci-lint 2024-05-22 09:43:41 +02:00
dependabot[bot]
9b1399c9d5 chore(deps): bump golangci/golangci-lint-action from 5 to 6
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 5 to 6.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v5...v6)

---
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-05-13 07:41:40 +02:00
Julien Neuhart
020a2f00c2 fix(pdfengine): PDFcpu -> pdfcpu 2024-05-02 10:04:25 +02:00
Julien Neuhart
8713946f35 fix(ci): codecov 2024-04-29 13:03:01 +02:00
IMATIASL\jaime.pineiro
bed8c25e40 feat(logging): Write log time in local timezone rather than UTC 2024-04-29 10:18:27 +02:00
dependabot[bot]
7c51a09068 chore(deps): bump golangci/golangci-lint-action from 4 to 5
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 4 to 5.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v4...v5)

---
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-04-29 09:48:09 +02:00
Julien Neuhart
0f0a6ce042 feat(chromium): add clip options to screenshot routes 2024-04-25 17:24:23 +02:00
Julien Neuhart
a890968ee4 chore(deps): update Go dependencies 2024-04-25 16:06:28 +02:00
Julien Neuhart
2267c5be00 feat(api): add version route 2024-04-17 09:55:38 +02:00
Julien Neuhart
e853cfb98a chore(deps): update Go dependencies 2024-04-17 09:08:43 +02:00
Julien Neuhart
f89a0db471 chore(deps): update Go dependencies 2024-04-11 09:57:55 +02:00
Julien Neuhart
1d66b43da1 feat(chromium): set cookies 2024-04-10 13:43:31 +02:00
Julien Neuhart
75c4a4f253 feat(api): add basic auth support 2024-04-05 17:20:34 +02:00
Julien Neuhart
6a9c95b3b1 chore(deps): update Go dependencies 2024-04-05 14:53:50 +02:00
Julien Neuhart
c11a2d0081 chore(golangci-lint): remove unused linters 2024-04-03 11:55:24 +02:00
Martin
43a13ff2c5 feat(libreoffice): add singlePageSheets option 2024-03-25 15:46:48 +01:00
Julien Neuhart
8454d163eb chore(golangci-lint): add missing EOF 2024-03-23 20:01:26 +01:00
32 changed files with 1144 additions and 128 deletions

View File

@@ -22,9 +22,9 @@ jobs:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Run linters - name: Run linters
uses: golangci/golangci-lint-action@v4 uses: golangci/golangci-lint-action@v6
with: with:
version: v1.56.2 version: v1.59.0
tests: tests:
needs: needs:
@@ -43,9 +43,12 @@ jobs:
- name: Build testing environment - name: Build testing environment
run: make build build-tests run: make build build-tests
- name: Run tests - name: Run tests
run: | run: make tests-once
make tests-once - name: Upload to Codecov
bash <(curl -s https://codecov.io/bash) uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
verbose: true
multiarch_build: multiarch_build:
if: github.event_name == 'push' && github.ref == 'refs/heads/main' if: github.event_name == 'push' && github.ref == 'refs/heads/main'

4
.gitignore vendored
View File

@@ -1,3 +1,5 @@
/coverage.html /coverage.html
/coverage.txt /coverage.txt
/TODO.txt .idea
.vscode
.DS_Store

View File

@@ -35,19 +35,16 @@ linters:
- misspell - misspell
- prealloc - prealloc
- promlinter - promlinter
- sloglint #- sloglint
- sqlclosecheck
- staticcheck - staticcheck
- tenv - tenv
- testableexamples - testableexamples
- testifylint
- tparallel - tparallel
- typecheck - typecheck
- unconvert - unconvert
- unused - unused
- wastedassign - wastedassign
- whitespace - whitespace
- zerologlint
run: run:
timeout: 5m timeout: 5m
@@ -56,4 +53,4 @@ run:
output: output:
print-issued-lines: true print-issued-lines: true
print-linter-name: true print-linter-name: true

View File

@@ -12,7 +12,7 @@ GOTENBERG_USER_GID=1001
GOTENBERG_USER_UID=1001 GOTENBERG_USER_UID=1001
NOTO_COLOR_EMOJI_VERSION=v2.042 # See https://github.com/googlefonts/noto-emoji/releases. NOTO_COLOR_EMOJI_VERSION=v2.042 # See https://github.com/googlefonts/noto-emoji/releases.
PDFTK_VERSION=v3.3.3 # See https://gitlab.com/pdftk-java/pdftk/-/releases - Binary package. PDFTK_VERSION=v3.3.3 # See https://gitlab.com/pdftk-java/pdftk/-/releases - Binary package.
GOLANGCI_LINT_VERSION=v1.56.2 # See https://github.com/golangci/golangci-lint/releases. GOLANGCI_LINT_VERSION=v1.59.0 # See https://github.com/golangci/golangci-lint/releases.
.PHONY: build .PHONY: build
build: ## Build the Gotenberg's Docker image build: ## Build the Gotenberg's Docker image
@@ -33,6 +33,9 @@ API_START_TIMEOUT=30s
API_TIMEOUT=30s API_TIMEOUT=30s
API_ROOT_PATH=/ API_ROOT_PATH=/
API_TRACE_HEADER=Gotenberg-Trace API_TRACE_HEADER=Gotenberg-Trace
API_ENABLE_BASIC_AUTH=false
GOTENBERG_API_BASIC_AUTH_USERNAME=
GOTENBERG_API_BASIC_AUTH_PASSWORD=
API_DISABLE_HEALTH_CHECK_LOGGING=false API_DISABLE_HEALTH_CHECK_LOGGING=false
CHROMIUM_RESTART_AFTER=0 CHROMIUM_RESTART_AFTER=0
CHROMIUM_MAX_QUEUE_SIZE=0 CHROMIUM_MAX_QUEUE_SIZE=0
@@ -79,6 +82,8 @@ WEBHOOK_DISABLE=false
run: ## Start a Gotenberg container run: ## Start a Gotenberg container
docker run --rm -it \ docker run --rm -it \
-p $(API_PORT):$(API_PORT) \ -p $(API_PORT):$(API_PORT) \
-e GOTENBERG_API_BASIC_AUTH_USERNAME=$(GOTENBERG_API_BASIC_AUTH_USERNAME) \
-e GOTENBERG_API_BASIC_AUTH_PASSWORD=$(GOTENBERG_API_BASIC_AUTH_PASSWORD) \
$(DOCKER_REPOSITORY)/gotenberg:$(GOTENBERG_VERSION) \ $(DOCKER_REPOSITORY)/gotenberg:$(GOTENBERG_VERSION) \
gotenberg \ gotenberg \
--gotenberg-graceful-shutdown-duration=$(GOTENBERG_GRACEFUL_SHUTDOWN_DURATION) \ --gotenberg-graceful-shutdown-duration=$(GOTENBERG_GRACEFUL_SHUTDOWN_DURATION) \
@@ -88,6 +93,7 @@ run: ## Start a Gotenberg container
--api-timeout=$(API_TIMEOUT) \ --api-timeout=$(API_TIMEOUT) \
--api-root-path=$(API_ROOT_PATH) \ --api-root-path=$(API_ROOT_PATH) \
--api-trace-header=$(API_TRACE_HEADER) \ --api-trace-header=$(API_TRACE_HEADER) \
--api-enable-basic-auth=$(API_ENABLE_BASIC_AUTH) \
--api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \ --api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \
--chromium-restart-after=$(CHROMIUM_RESTART_AFTER) \ --chromium-restart-after=$(CHROMIUM_RESTART_AFTER) \
--chromium-auto-start=$(CHROMIUM_AUTO_START) \ --chromium-auto-start=$(CHROMIUM_AUTO_START) \

View File

@@ -35,6 +35,7 @@ var Version = "snapshot"
// Run starts the Gotenberg application. Call this in the main of your program. // Run starts the Gotenberg application. Call this in the main of your program.
func Run() { func Run() {
fmt.Printf(banner, Version) fmt.Printf(banner, Version)
gotenberg.Version = Version
// Create the root FlagSet and adds the modules flags to it. // Create the root FlagSet and adds the modules flags to it.
fs := flag.NewFlagSet("gotenberg", flag.ExitOnError) fs := flag.NewFlagSet("gotenberg", flag.ExitOnError)

42
go.mod
View File

@@ -6,35 +6,35 @@ require (
github.com/alexliesenfeld/health v0.8.0 github.com/alexliesenfeld/health v0.8.0
github.com/andybalholm/brotli v1.1.0 // indirect github.com/andybalholm/brotli v1.1.0 // indirect
github.com/barasher/go-exiftool v1.10.0 github.com/barasher/go-exiftool v1.10.0
github.com/chromedp/cdproto v0.0.0-20240312231614-1e5096e63154 github.com/chromedp/cdproto v0.0.0-20240602235142-49d0e97b7881
github.com/chromedp/chromedp v0.9.5 github.com/chromedp/chromedp v0.9.5
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.5 github.com/hashicorp/go-retryablehttp v0.7.7
github.com/klauspost/compress v1.17.7 // indirect github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect github.com/klauspost/pgzip v1.2.6 // indirect
github.com/labstack/echo/v4 v4.11.4 github.com/labstack/echo/v4 v4.12.0
github.com/labstack/gommon v0.4.2 github.com/labstack/gommon v0.4.2
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mholt/archiver/v3 v3.5.1 github.com/mholt/archiver/v3 v3.5.1
github.com/microcosm-cc/bluemonday v1.0.26 github.com/microcosm-cc/bluemonday v1.0.26
github.com/nwaples/rardecode v1.1.3 // indirect github.com/nwaples/rardecode v1.1.3 // indirect
github.com/pdfcpu/pdfcpu v0.7.0 github.com/pdfcpu/pdfcpu v0.8.0
github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/prometheus/client_golang v1.19.0 github.com/prometheus/client_golang v1.19.1
github.com/russross/blackfriday/v2 v2.1.0 github.com/russross/blackfriday/v2 v2.1.0
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/ulikunitz/xz v0.5.11 // indirect github.com/ulikunitz/xz v0.5.12 // indirect
go.uber.org/multierr v1.11.0 go.uber.org/multierr v1.11.0
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
golang.org/x/crypto v0.21.0 // indirect golang.org/x/crypto v0.24.0 // indirect
golang.org/x/image v0.15.0 // indirect golang.org/x/image v0.17.0 // indirect
golang.org/x/net v0.22.0 golang.org/x/net v0.26.0
golang.org/x/sync v0.6.0 golang.org/x/sync v0.7.0
golang.org/x/sys v0.18.0 // indirect golang.org/x/sys v0.21.0 // indirect
golang.org/x/term v0.18.0 golang.org/x/term v0.21.0
golang.org/x/text v0.14.0 golang.org/x/text v0.16.0
) )
require github.com/dlclark/regexp2 v1.11.0 require github.com/dlclark/regexp2 v1.11.0
@@ -42,12 +42,13 @@ require github.com/dlclark/regexp2 v1.11.0
require ( require (
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chromedp/sysutil v1.0.0 // indirect github.com/chromedp/sysutil v1.0.0 // indirect
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.3.2 // indirect github.com/gobwas/ws v1.4.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/css v1.0.1 // indirect
github.com/hhrutter/lzw v1.0.0 // indirect github.com/hhrutter/lzw v1.0.0 // indirect
github.com/hhrutter/tiff v1.0.1 // indirect github.com/hhrutter/tiff v1.0.1 // indirect
@@ -56,13 +57,14 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.6.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.51.0 // indirect github.com/prometheus/common v0.54.0 // indirect
github.com/prometheus/procfs v0.13.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
google.golang.org/protobuf v1.33.0 // indirect golang.org/x/time v0.5.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
) )

90
go.sum
View File

@@ -9,11 +9,11 @@ github.com/barasher/go-exiftool v1.10.0 h1:f5JY5jc42M7tzR6tbL9508S2IXdIcG9QyieEX
github.com/barasher/go-exiftool v1.10.0/go.mod h1:F9s/a3uHSM8YniVfwF+sbQUtP8Gmh9nyzigNF+8vsWo= github.com/barasher/go-exiftool v1.10.0/go.mod h1:F9s/a3uHSM8YniVfwF+sbQUtP8Gmh9nyzigNF+8vsWo=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/cdproto v0.0.0-20240312231614-1e5096e63154 h1:jeAmkzyOAQBPRmZMhX+i/CJv0VViLkHk1nF0qx8s0Mk= github.com/chromedp/cdproto v0.0.0-20240602235142-49d0e97b7881 h1:RAUqkPvbEDGPgCYVc4GefBqAorWJAjKpVHgsRZyJmGE=
github.com/chromedp/cdproto v0.0.0-20240312231614-1e5096e63154/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/cdproto v0.0.0-20240602235142-49d0e97b7881/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/chromedp v0.9.5 h1:viASzruPJOiThk7c5bueOUY91jGLJVximoEMGoH93rg= 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/chromedp v0.9.5/go.mod h1:D4I2qONslauw/C7INoCir1BJkSwBYMyZgx8X276z3+Y=
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
@@ -26,12 +26,17 @@ github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cn
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY= github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q=
github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
@@ -44,10 +49,10 @@ github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0= github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0=
github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo= github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo=
github.com/hhrutter/tiff v1.0.1 h1:MIus8caHU5U6823gx7C6jrfoEvfSTGtEFRiM8/LOzC0= github.com/hhrutter/tiff v1.0.1 h1:MIus8caHU5U6823gx7C6jrfoEvfSTGtEFRiM8/LOzC0=
@@ -56,8 +61,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
@@ -66,8 +71,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
@@ -90,8 +95,8 @@ github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9l
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pdfcpu/pdfcpu v0.7.0 h1:cd7/z7hAyyDuzdciKfNZyQ3TYreJza2DsuPdIHYURcA= github.com/pdfcpu/pdfcpu v0.8.0 h1:SuEB4uVsPFz1nb802r38YpFpj9TtZh/oB0bGG34IRZw=
github.com/pdfcpu/pdfcpu v0.7.0/go.mod h1:kmpD0rk8YnZj0l3qSeGBlAB+XszHUgNv//ORH/E7EYo= github.com/pdfcpu/pdfcpu v0.8.0/go.mod h1:jj03y/KKrwigt5xCi8t7px2mATcKuOzkIOoCX62yMho=
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.2/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 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
@@ -99,14 +104,14 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.51.0 h1:vT5R9NAlW4V6k8Wruk7ikrHaHRsrPbduM/cKTOdQM/k= github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8=
github.com/prometheus/common v0.51.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ= github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ=
github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -119,14 +124,13 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
@@ -139,26 +143,28 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= golang.org/x/image v0.17.0 h1:nTRVVdajgB8zCMZVsViyzhnMKPwYeroEERRC64JuLco=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= golang.org/x/image v0.17.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

35
pkg/gotenberg/env.go Normal file
View File

@@ -0,0 +1,35 @@
package gotenberg
import (
"fmt"
"os"
"strconv"
)
// StringEnv retrieves the value of the environment variable named by the key.
// If the variable is present in the environment and not empty, the value is
// returned.
func StringEnv(key string) (string, error) {
val, ok := os.LookupEnv(key)
if !ok {
return "", fmt.Errorf("environment variable '%s' does not exist", key)
}
if val == "" {
return "", fmt.Errorf("environment variable '%s' is empty", key)
}
return val, nil
}
// IntEnv relies on [StringEnv] and converts the values if it exists and is not
// empty.
func IntEnv(key string) (int, error) {
val, err := StringEnv(key)
if err != nil {
return 0, err
}
intVal, err := strconv.Atoi(val)
if err != nil {
return 0, fmt.Errorf("get int value of environment variable '%s': %w", key, err)
}
return intVal, nil
}

134
pkg/gotenberg/env_test.go Normal file
View File

@@ -0,0 +1,134 @@
package gotenberg
import (
"os"
"testing"
)
func TestStringEnv(t *testing.T) {
for _, tc := range []struct {
scenario string
key string
setEnv func()
expectVal string
expectError bool
}{
{
scenario: "non-existing environment variable",
key: "NON_EXISTING",
expectVal: "",
expectError: true,
},
{
scenario: "empty environment variable",
key: "EMPTY_STRING",
setEnv: func() {
err := os.Setenv("EMPTY_STRING", "")
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
},
expectVal: "",
expectError: true,
},
{
scenario: "success",
key: "EXISTING_STRING_VALUE",
setEnv: func() {
err := os.Setenv("EXISTING_STRING_VALUE", "foo")
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
},
expectVal: "foo",
expectError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
if tc.setEnv != nil {
tc.setEnv()
}
val, err := StringEnv(tc.key)
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if tc.expectError && err == nil {
t.Fatal("expected error but got none")
}
if tc.expectVal != val {
t.Errorf("expected value '%s' but got '%s'", tc.expectVal, val)
}
})
}
}
func TestIntEnv(t *testing.T) {
for _, tc := range []struct {
scenario string
key string
setEnv func()
expectVal int
expectError bool
}{
{
scenario: "empty environment variable",
key: "EMPTY_INT",
setEnv: func() {
err := os.Setenv("EMPTY_INT", "")
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
},
expectVal: 0,
expectError: true,
},
{
scenario: "non-integer value",
key: "NON_INTEGER",
setEnv: func() {
err := os.Setenv("NON_INTEGER", "foo")
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
},
expectVal: 0,
expectError: true,
},
{
scenario: "success",
key: "EXISTING_INT_VALUE",
setEnv: func() {
err := os.Setenv("EXISTING_INT_VALUE", "123")
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
},
expectVal: 123,
expectError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
if tc.setEnv != nil {
tc.setEnv()
}
val, err := IntEnv(tc.key)
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if tc.expectError && err == nil {
t.Fatal("expected error but got none")
}
if tc.expectVal != val {
t.Errorf("expected value %d but got %d", tc.expectVal, val)
}
})
}
}

4
pkg/gotenberg/version.go Normal file
View File

@@ -0,0 +1,4 @@
package gotenberg
// Version is the... version of the Gotenberg application.
var Version = "snapshot"

View File

@@ -5,9 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"os"
"sort" "sort"
"strconv"
"strings" "strings"
"time" "time"
@@ -30,10 +28,14 @@ func init() {
// middlewares or health checks. // middlewares or health checks.
type Api struct { type Api struct {
port int port int
tlsCertFile string
tlsKeyFile string
startTimeout time.Duration startTimeout time.Duration
timeout time.Duration timeout time.Duration
rootPath string rootPath string
traceHeader string traceHeader string
basicAuthUsername string
basicAuthPassword string
disableHealthCheckLogging bool disableHealthCheckLogging bool
routes []Route routes []Route
@@ -159,12 +161,14 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
fs := flag.NewFlagSet("api", flag.ExitOnError) fs := flag.NewFlagSet("api", flag.ExitOnError)
fs.Int("api-port", 3000, "Set the port on which the API should listen") fs.Int("api-port", 3000, "Set the port on which the API should listen")
fs.String("api-port-from-env", "", "Set the environment variable with the port on which the API should listen - override the default port") fs.String("api-port-from-env", "", "Set the environment variable with the port on which the API should listen - override the default port")
fs.String("api-tls-cert-file", "", "Path to the TLS/SSL certificate file - for HTTPS support")
fs.String("api-tls-key-file", "", "Path to the TLS/SSL key file - for HTTPS support")
fs.Duration("api-start-timeout", time.Duration(30)*time.Second, "Set the time limit for the API to start") fs.Duration("api-start-timeout", time.Duration(30)*time.Second, "Set the time limit for the API to start")
fs.Duration("api-timeout", time.Duration(30)*time.Second, "Set the time limit for requests") fs.Duration("api-timeout", time.Duration(30)*time.Second, "Set the time limit for requests")
fs.String("api-root-path", "/", "Set the root path of the API - for service discovery via URL paths") fs.String("api-root-path", "/", "Set the root path of the API - for service discovery via URL paths")
fs.String("api-trace-header", "Gotenberg-Trace", "Set the header name to use for identifying requests") fs.String("api-trace-header", "Gotenberg-Trace", "Set the header name to use for identifying requests")
fs.Bool("api-enable-basic-auth", false, "Enable basic authentication - will look for the GOTENBERG_API_BASIC_AUTH_USERNAME and GOTENBERG_API_BASIC_AUTH_PASSWORD environment variables")
fs.Bool("api-disable-health-check-logging", false, "Disable health check logging") fs.Bool("api-disable-health-check-logging", false, "Disable health check logging")
return fs return fs
}(), }(),
New: func() gotenberg.Module { return new(Api) }, New: func() gotenberg.Module { return new(Api) },
@@ -175,6 +179,8 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
func (a *Api) Provision(ctx *gotenberg.Context) error { func (a *Api) Provision(ctx *gotenberg.Context) error {
flags := ctx.ParsedFlags() flags := ctx.ParsedFlags()
a.port = flags.MustInt("api-port") a.port = flags.MustInt("api-port")
a.tlsCertFile = flags.MustString("api-tls-cert-file")
a.tlsKeyFile = flags.MustString("api-tls-key-file")
a.startTimeout = flags.MustDuration("api-start-timeout") a.startTimeout = flags.MustDuration("api-start-timeout")
a.timeout = flags.MustDuration("api-timeout") a.timeout = flags.MustDuration("api-timeout")
a.rootPath = flags.MustString("api-root-path") a.rootPath = flags.MustString("api-root-path")
@@ -184,24 +190,28 @@ func (a *Api) Provision(ctx *gotenberg.Context) error {
// Port from env? // Port from env?
portEnvVar := flags.MustString("api-port-from-env") portEnvVar := flags.MustString("api-port-from-env")
if portEnvVar != "" { if portEnvVar != "" {
val, ok := os.LookupEnv(portEnvVar) port, err := gotenberg.IntEnv(portEnvVar)
if !ok {
return fmt.Errorf("environment variable '%s' does not exist", portEnvVar)
}
if val == "" {
return fmt.Errorf("environment variable '%s' is empty", portEnvVar)
}
port, err := strconv.Atoi(val)
if err != nil { if err != nil {
return fmt.Errorf("get int value of environment variable '%s': %w", portEnvVar, err) return fmt.Errorf("get API port from env: %w", err)
} }
a.port = port a.port = port
} }
// Enable basic auth?
enableBasicAuth := flags.MustBool("api-enable-basic-auth")
if enableBasicAuth {
basicAuthUsername, err := gotenberg.StringEnv("GOTENBERG_API_BASIC_AUTH_USERNAME")
if err != nil {
return fmt.Errorf("get basic auth username from env: %w", err)
}
basicAuthPassword, err := gotenberg.StringEnv("GOTENBERG_API_BASIC_AUTH_PASSWORD")
if err != nil {
return fmt.Errorf("get basic auth password from env: %w", err)
}
a.basicAuthUsername = basicAuthUsername
a.basicAuthPassword = basicAuthPassword
}
// Get routes from modules. // Get routes from modules.
mods, err := ctx.Modules(new(Router)) mods, err := ctx.Modules(new(Router))
if err != nil { if err != nil {
@@ -297,6 +307,12 @@ func (a *Api) Validate() error {
) )
} }
if (a.tlsCertFile != "" && a.tlsKeyFile == "") || (a.tlsCertFile == "" && a.tlsKeyFile != "") {
err = multierr.Append(err,
errors.New("both TLS certificate and key files must be set"),
)
}
if !strings.HasPrefix(a.rootPath, "/") { if !strings.HasPrefix(a.rootPath, "/") {
err = multierr.Append(err, err = multierr.Append(err,
errors.New("root path must start with /"), errors.New("root path must start with /"),
@@ -319,8 +335,9 @@ func (a *Api) Validate() error {
return err return err
} }
routesMap := make(map[string]string, len(a.routes)+1) routesMap := make(map[string]string, len(a.routes)+2)
routesMap["/health"] = "/health" routesMap["/health"] = "/health"
routesMap["/version"] = "/version"
for _, route := range a.routes { for _, route := range a.routes {
if route.Path == "" { if route.Path == "" {
@@ -394,6 +411,13 @@ func (a *Api) Start() error {
loggerMiddleware(a.logger, disableLoggingForPaths), loggerMiddleware(a.logger, disableLoggingForPaths),
) )
// Basic auth?
if a.basicAuthUsername != "" {
a.srv.Pre(
basicAuthMiddleware(a.basicAuthUsername, a.basicAuthPassword),
)
}
// Add the modules' middlewares in their respective stacks. // Add the modules' middlewares in their respective stacks.
var externalMultipartMiddlewares []Middleware var externalMultipartMiddlewares []Middleware
for _, externalMiddleware := range a.externalMiddlewares { for _, externalMiddleware := range a.externalMiddlewares {
@@ -431,7 +455,7 @@ func (a *Api) Start() error {
) )
} }
// Let's not forget the health check route. // Let's not forget the health check route...
a.srv.GET( a.srv.GET(
fmt.Sprintf("%s%s", a.rootPath, "health"), fmt.Sprintf("%s%s", a.rootPath, "health"),
func() echo.HandlerFunc { func() echo.HandlerFunc {
@@ -442,6 +466,14 @@ func (a *Api) Start() error {
hardTimeoutMiddleware(hardTimeout), hardTimeoutMiddleware(hardTimeout),
) )
// ...and the version route.
a.srv.GET(
fmt.Sprintf("%s%s", a.rootPath, "version"),
func(c echo.Context) error {
return c.String(http.StatusOK, gotenberg.Version)
},
)
// Wait for all modules to be ready. // Wait for all modules to be ready.
ctx, cancel := context.WithTimeout(context.Background(), a.startTimeout) ctx, cancel := context.WithTimeout(context.Background(), a.startTimeout)
defer cancel() defer cancel()
@@ -458,8 +490,15 @@ func (a *Api) Start() error {
// As the following code is blocking, run it in a goroutine. // As the following code is blocking, run it in a goroutine.
go func() { go func() {
server := &http2.Server{} var err error
err := a.srv.StartH2CServer(fmt.Sprintf(":%d", a.port), server) if a.tlsCertFile != "" && a.tlsKeyFile != "" {
// Start an HTTPS server (supports HTTP/2).
err = a.srv.StartTLS(fmt.Sprintf(":%d", a.port), a.tlsCertFile, a.tlsKeyFile)
} else {
// Start an HTTP/2 Cleartext (non-HTTPS) server.
server := &http2.Server{}
err = a.srv.StartH2CServer(fmt.Sprintf(":%d", a.port), server)
}
if !errors.Is(err, http.ErrServerClosed) { if !errors.Is(err, http.ErrServerClosed) {
a.logger.Fatal(err.Error()) a.logger.Fatal(err.Error())
} }

View File

@@ -58,10 +58,28 @@ func TestApi_Provision(t *testing.T) {
expectError: true, expectError: true,
}, },
{ {
scenario: "port from env: empty environment variable", scenario: "basic auth: non-existing GOTENBERG_API_BASIC_AUTH_USERNAME environment variable",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
fs := new(Api).Descriptor().FlagSet fs := new(Api).Descriptor().FlagSet
err := fs.Parse([]string{"--api-port-from-env=PORT"}) err := fs.Parse([]string{"--api-enable-basic-auth=true"})
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return gotenberg.NewContext(
gotenberg.ParsedFlags{
FlagSet: fs,
},
nil,
)
}(),
expectError: true,
},
{
scenario: "basic auth: non-existing GOTENBERG_API_BASIC_AUTH_PASSWORD environment variable",
ctx: func() *gotenberg.Context {
fs := new(Api).Descriptor().FlagSet
err := fs.Parse([]string{"--api-enable-basic-auth=true"})
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
@@ -74,7 +92,7 @@ func TestApi_Provision(t *testing.T) {
) )
}(), }(),
setEnv: func() { setEnv: func() {
err := os.Setenv("PORT", "") err := os.Setenv("GOTENBERG_API_BASIC_AUTH_USERNAME", "foo")
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
@@ -361,7 +379,7 @@ func TestApi_Provision(t *testing.T) {
} }
fs := new(Api).Descriptor().FlagSet fs := new(Api).Descriptor().FlagSet
err := fs.Parse([]string{"--api-port-from-env=PORT"}) err := fs.Parse([]string{"--api-port-from-env=PORT", "--api-enable-basic-auth=true"})
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
@@ -383,6 +401,14 @@ func TestApi_Provision(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
err = os.Setenv("GOTENBERG_API_BASIC_AUTH_USERNAME", "foo")
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
err = os.Setenv("GOTENBERG_API_BASIC_AUTH_PASSWORD", "bar")
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
}, },
expectPort: 1337, expectPort: 1337,
expectMiddlewares: []Middleware{ expectMiddlewares: []Middleware{
@@ -436,6 +462,8 @@ func TestApi_Validate(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
scenario string scenario string
port int port int
tlsCertFile string
tlsKeyFile string
rootPath string rootPath string
traceHeader string traceHeader string
routes []Route routes []Route
@@ -460,6 +488,26 @@ func TestApi_Validate(t *testing.T) {
middlewares: nil, middlewares: nil,
expectError: true, expectError: true,
}, },
{
scenario: "invalid TLS files: only cert file provided",
port: 10,
tlsCertFile: "cert.pem",
rootPath: "/foo/",
traceHeader: "foo",
routes: nil,
middlewares: nil,
expectError: true,
},
{
scenario: "invalid TLS files: only key file provided",
port: 10,
tlsKeyFile: "key.pem",
rootPath: "/foo/",
traceHeader: "foo",
routes: nil,
middlewares: nil,
expectError: true,
},
{ {
scenario: "invalid root path: missing / prefix", scenario: "invalid root path: missing / prefix",
port: 10, port: 10,
@@ -621,10 +669,33 @@ func TestApi_Validate(t *testing.T) {
}, },
}, },
}, },
{
scenario: "success with TLS",
port: 10,
tlsCertFile: "cert.pem",
tlsKeyFile: "key.pem",
rootPath: "/foo/",
traceHeader: "foo",
routes: []Route{
{
Method: http.MethodGet,
Path: "/foo",
Handler: func(_ echo.Context) error { return nil },
},
{
Method: http.MethodGet,
Path: "/forms/foo",
Handler: func(_ echo.Context) error { return nil },
IsMultipart: true,
},
},
},
} { } {
t.Run(tc.scenario, func(t *testing.T) { t.Run(tc.scenario, func(t *testing.T) {
mod := Api{ mod := Api{
port: tc.port, port: tc.port,
tlsCertFile: tc.tlsCertFile,
tlsKeyFile: tc.tlsKeyFile,
rootPath: tc.rootPath, rootPath: tc.rootPath,
traceHeader: tc.traceHeader, traceHeader: tc.traceHeader,
routes: tc.routes, routes: tc.routes,
@@ -647,6 +718,8 @@ func TestApi_Start(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
scenario string scenario string
readyFn []func() error readyFn []func() error
tlsCertFile string
tlsKeyFile string
expectError bool expectError bool
}{ }{
{ {
@@ -665,12 +738,26 @@ func TestApi_Start(t *testing.T) {
}, },
expectError: false, expectError: false,
}, },
{
scenario: "success with TLS",
readyFn: []func() error{
func() error { return nil },
func() error { return nil },
},
tlsCertFile: "/tests/test/testdata/api/cert.pem",
tlsKeyFile: "/tests/test/testdata/api/key.pem",
expectError: false,
},
} { } {
t.Run(tc.scenario, func(t *testing.T) { t.Run(tc.scenario, func(t *testing.T) {
mod := new(Api) mod := new(Api)
mod.port = 3000 mod.port = 3000
mod.tlsCertFile = tc.tlsCertFile
mod.tlsKeyFile = tc.tlsKeyFile
mod.startTimeout = time.Duration(30) * time.Second mod.startTimeout = time.Duration(30) * time.Second
mod.rootPath = "/" mod.rootPath = "/"
mod.basicAuthUsername = "foo"
mod.basicAuthPassword = "bar"
mod.disableHealthCheckLogging = true mod.disableHealthCheckLogging = true
mod.routes = []Route{ mod.routes = []Route{
{ {
@@ -755,12 +842,22 @@ func TestApi_Start(t *testing.T) {
// health request. // health request.
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
healthRequest := httptest.NewRequest(http.MethodGet, "/health", nil) healthRequest := httptest.NewRequest(http.MethodGet, "/health", nil)
healthRequest.SetBasicAuth(mod.basicAuthUsername, mod.basicAuthPassword)
mod.srv.ServeHTTP(recorder, healthRequest) mod.srv.ServeHTTP(recorder, healthRequest)
if recorder.Code != http.StatusOK { if recorder.Code != http.StatusOK {
t.Errorf("expected %d status code but got %d", http.StatusOK, recorder.Code) t.Errorf("expected %d status code but got %d", http.StatusOK, recorder.Code)
} }
// version request.
versionRequest := httptest.NewRequest(http.MethodGet, "/version", nil)
versionRequest.SetBasicAuth(mod.basicAuthUsername, mod.basicAuthPassword)
mod.srv.ServeHTTP(recorder, versionRequest)
if recorder.Code != http.StatusOK {
t.Errorf("expected %d status code but got %d", http.StatusOK, recorder.Code)
}
// "multipart/form-data" request. // "multipart/form-data" request.
multipartRequest := func(url string) *http.Request { multipartRequest := func(url string) *http.Request {
body := &bytes.Buffer{} body := &bytes.Buffer{}
@@ -791,6 +888,7 @@ func TestApi_Start(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, url, body) req := httptest.NewRequest(http.MethodPost, url, body)
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType()) req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
req.SetBasicAuth(mod.basicAuthUsername, mod.basicAuthPassword)
return req return req
} }

View File

@@ -2,6 +2,7 @@ package api
import ( import (
"context" "context"
"crypto/subtle"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@@ -10,6 +11,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg" "github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
@@ -112,7 +114,6 @@ func rootPathMiddleware(rootPath string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
c.Set("rootPath", rootPath) c.Set("rootPath", rootPath)
// Call the next middleware in the chain. // Call the next middleware in the chain.
return next(c) return next(c)
} }
@@ -217,6 +218,17 @@ func loggerMiddleware(logger *zap.Logger, disableLoggingForPaths []string) echo.
} }
} }
// basicAuthMiddleware manages basic authentication.
func basicAuthMiddleware(username, password string) echo.MiddlewareFunc {
return middleware.BasicAuth(func(u string, p string, e echo.Context) (bool, error) {
if subtle.ConstantTimeCompare([]byte(u), []byte(username)) == 1 &&
subtle.ConstantTimeCompare([]byte(p), []byte(password)) == 1 {
return true, nil
}
return false, nil
})
}
// contextMiddleware, a middleware for "multipart/form-data" requests, sets the // contextMiddleware, a middleware for "multipart/form-data" requests, sets the
// [Context] and related context.CancelFunc in the [echo.Context] under // [Context] and related context.CancelFunc in the [echo.Context] under
// "context" and "cancel". If the process is synchronous, it also handles the // "context" and "cancel". If the process is synchronous, it also handles the

View File

@@ -236,6 +236,56 @@ func TestTraceMiddleware(t *testing.T) {
} }
} }
func TestBasicAuthMiddleware(t *testing.T) {
for _, tc := range []struct {
scenario string
request *http.Request
username string
password string
expectError bool
}{
{
scenario: "invalid basic auth",
request: func() *http.Request {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.SetBasicAuth("invalid", "invalid")
return req
}(),
username: "foo",
password: "bar",
expectError: true,
},
{
scenario: "valid basic auth",
request: func() *http.Request {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.SetBasicAuth("foo", "bar")
return req
}(),
username: "foo",
password: "bar",
expectError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
recorder := httptest.NewRecorder()
srv := echo.New()
srv.HideBanner = true
srv.HidePort = true
c := srv.NewContext(tc.request, recorder)
err := basicAuthMiddleware(tc.username, tc.password)(func(c echo.Context) error {
return nil
})(c)
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if tc.expectError && err == nil {
t.Fatal("expected error but got none")
}
})
}
}
func TestLoggerMiddleware(t *testing.T) { func TestLoggerMiddleware(t *testing.T) {
for i, tc := range []struct { for i, tc := range []struct {
request *http.Request request *http.Request

View File

@@ -226,6 +226,8 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outp
clearCacheActionFunc(logger, b.arguments.clearCache), clearCacheActionFunc(logger, b.arguments.clearCache),
clearCookiesActionFunc(logger, b.arguments.clearCookies), clearCookiesActionFunc(logger, b.arguments.clearCookies),
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript), disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
setCookiesActionFunc(logger, options.Cookies),
userAgentOverride(logger, options.UserAgent),
extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders), extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent), navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, options.PrintBackground), hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, options.PrintBackground),
@@ -248,6 +250,8 @@ func (b *chromiumBrowser) screenshot(ctx context.Context, logger *zap.Logger, ur
clearCacheActionFunc(logger, b.arguments.clearCache), clearCacheActionFunc(logger, b.arguments.clearCache),
clearCookiesActionFunc(logger, b.arguments.clearCookies), clearCookiesActionFunc(logger, b.arguments.clearCookies),
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript), disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
setCookiesActionFunc(logger, options.Cookies),
userAgentOverride(logger, options.UserAgent),
extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders), extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent), navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, true), hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, true),
@@ -256,6 +260,7 @@ func (b *chromiumBrowser) screenshot(ctx context.Context, logger *zap.Logger, ur
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay), waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression), waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression),
// Screenshot specific. // Screenshot specific.
setDeviceMetricsOverride(logger, options.Width, options.Height),
captureScreenshotActionFunc(logger, outputPath, options), captureScreenshotActionFunc(logger, outputPath, options),
}) })
} }

View File

@@ -579,6 +579,76 @@ func TestChromiumBrowser_pdf(t *testing.T) {
"JavaScript disabled, skipping wait expression", "JavaScript disabled, skipping wait expression",
}, },
}, },
{
scenario: "set cookies",
browser: newChromiumBrowser(
browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second,
allowList: regexp2.MustCompile("", 0),
denyList: regexp2.MustCompile("", 0),
},
),
fs: func() *gotenberg.FileSystem {
fs := gotenberg.NewFileSystem()
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
if err != nil {
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
}
err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("<h1>Set cookies</h1>"), 0o755)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return fs
}(),
options: PdfOptions{
Options: Options{Cookies: []Cookie{{Name: "foo", Value: "bar", Domain: ".foo.bar"}}},
},
noDeadline: false,
start: true,
expectError: false,
expectedLogEntries: []string{
"set cookie",
},
},
{
scenario: "user agent override",
browser: newChromiumBrowser(
browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second,
allowList: regexp2.MustCompile("", 0),
denyList: regexp2.MustCompile("", 0),
},
),
fs: func() *gotenberg.FileSystem {
fs := gotenberg.NewFileSystem()
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
if err != nil {
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
}
err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("<h1>User-Agent override</h1>"), 0o755)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return fs
}(),
options: PdfOptions{
Options: Options{UserAgent: "foo"},
},
noDeadline: false,
start: true,
expectError: false,
expectedLogEntries: []string{
fmt.Sprintf("user agent override: foo"),
},
},
{ {
scenario: "extra HTTP headers", scenario: "extra HTTP headers",
browser: newChromiumBrowser( browser: newChromiumBrowser(
@@ -1125,6 +1195,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
"cache not cleared", "cache not cleared",
"cookies not cleared", "cookies not cleared",
"JavaScript not disabled", "JavaScript not disabled",
"no cookies to set",
"no extra HTTP headers", "no extra HTTP headers",
"navigate to", "navigate to",
"default white background not hidden", "default white background not hidden",
@@ -1549,6 +1620,41 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
"JavaScript disabled, skipping wait expression", "JavaScript disabled, skipping wait expression",
}, },
}, },
{
scenario: "set cookies",
browser: newChromiumBrowser(
browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second,
allowList: regexp2.MustCompile("", 0),
denyList: regexp2.MustCompile("", 0),
},
),
fs: func() *gotenberg.FileSystem {
fs := gotenberg.NewFileSystem()
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
if err != nil {
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
}
err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("<h1>Set cookies</h1>"), 0o755)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return fs
}(),
options: ScreenshotOptions{
Options: Options{Cookies: []Cookie{{Name: "fpp", Value: "bar", Domain: ".foo.bar"}}},
},
noDeadline: false,
start: true,
expectError: false,
expectedLogEntries: []string{
"set cookie",
},
},
{ {
scenario: "extra HTTP headers", scenario: "extra HTTP headers",
browser: newChromiumBrowser( browser: newChromiumBrowser(
@@ -1919,12 +2025,62 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
"cache not cleared", "cache not cleared",
"cookies not cleared", "cookies not cleared",
"JavaScript not disabled", "JavaScript not disabled",
"no user agent override",
"no extra HTTP headers", "no extra HTTP headers",
"navigate to", "navigate to",
"default white background not hidden", "default white background not hidden",
"no emulated media type", "no emulated media type",
"no wait delay", "no wait delay",
"no wait expression", "no wait expression",
"set device metrics override",
},
},
{
scenario: "success (clip)",
browser: newChromiumBrowser(
browserArguments{
binPath: os.Getenv("CHROMIUM_BIN_PATH"),
wsUrlReadTimeout: 5 * time.Second,
allowList: regexp2.MustCompile("", 0),
denyList: regexp2.MustCompile("", 0),
},
),
fs: func() *gotenberg.FileSystem {
fs := gotenberg.NewFileSystem()
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
if err != nil {
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
}
err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("<h1>Default options</h1>"), 0o755)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return fs
}(),
options: func() ScreenshotOptions {
options := DefaultScreenshotOptions()
options.Clip = true
return options
}(),
noDeadline: false,
start: true,
expectError: false,
expectedLogEntries: []string{
"cache not cleared",
"cookies not cleared",
"JavaScript not disabled",
"no cookies to set",
"no user agent override",
"no extra HTTP headers",
"navigate to",
"default white background not hidden",
"no emulated media type",
"no wait delay",
"no wait expression",
"set device metrics override",
}, },
}, },
{ {
@@ -1964,12 +2120,15 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
"cache not cleared", "cache not cleared",
"cookies not cleared", "cookies not cleared",
"JavaScript not disabled", "JavaScript not disabled",
"no cookies to set",
"no user agent override",
"no extra HTTP headers", "no extra HTTP headers",
"navigate to", "navigate to",
"default white background not hidden", "default white background not hidden",
"no emulated media type", "no emulated media type",
"no wait delay", "no wait delay",
"no wait expression", "no wait expression",
"set device metrics override",
}, },
}, },
} { } {

View File

@@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/alexliesenfeld/health" "github.com/alexliesenfeld/health"
"github.com/chromedp/cdproto/network"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
"go.uber.org/zap" "go.uber.org/zap"
@@ -103,8 +104,16 @@ type Options struct {
// Optional. // Optional.
WaitForExpression string WaitForExpression string
// ExtraHttpHeaders are the HTTP headers to send by Chromium while loading // Cookies are the cookies to put in the Chromium cookies' jar.
// the HTML document. // Optional
Cookies []Cookie
// UserAgent overrides the default 'User-Agent' HTTP header.
// Optional.
UserAgent string
// ExtraHttpHeaders are extra HTTP headers to send by Chromium while
// loading he HTML document.
// Optional. // Optional.
ExtraHttpHeaders map[string]string ExtraHttpHeaders map[string]string
@@ -128,6 +137,8 @@ func DefaultOptions() Options {
WaitDelay: 0, WaitDelay: 0,
WaitWindowStatus: "", WaitWindowStatus: "",
WaitForExpression: "", WaitForExpression: "",
Cookies: nil,
UserAgent: "",
ExtraHttpHeaders: nil, ExtraHttpHeaders: nil,
EmulatedMediaType: "", EmulatedMediaType: "",
OmitBackground: false, OmitBackground: false,
@@ -233,6 +244,19 @@ func DefaultPdfOptions() PdfOptions {
type ScreenshotOptions struct { type ScreenshotOptions struct {
Options Options
// Width is the device screen width in pixels.
// Optional.
Width int
// Height is the device screen height in pixels.
// Optional.
Height int
// Clip defines whether to clip the screenshot according to the device
// dimensions.
// Optional.
Clip bool
// Format is the image compression format, either "png" or "jpeg" or // Format is the image compression format, either "png" or "jpeg" or
// "webp". // "webp".
// Optional. // Optional.
@@ -252,12 +276,47 @@ type ScreenshotOptions struct {
func DefaultScreenshotOptions() ScreenshotOptions { func DefaultScreenshotOptions() ScreenshotOptions {
return ScreenshotOptions{ return ScreenshotOptions{
Options: DefaultOptions(), Options: DefaultOptions(),
Width: 800,
Height: 600,
Clip: false,
Format: "png", Format: "png",
Quality: 100, Quality: 100,
OptimizeForSpeed: false, OptimizeForSpeed: false,
} }
} }
// Cookie gathers the available entries for setting a cookie in the Chromium
// cookies' jar.
type Cookie struct {
// Name is the cookie name.
// Required.
Name string `json:"name"`
// Value is the cookie value.
// Required.
Value string `json:"value"`
// Domain is the cookie domain.
// Required.
Domain string `json:"domain"`
// Path is the cookie path.
// Optional.
Path string `json:"path,omitempty"`
// Secure sets the cookie secure if true.
// Optional.
Secure bool `json:"secure,omitempty"`
// HttpOnly sets the cookie as HTTP-only if true.
// Optional.
HttpOnly bool `json:"httpOnly,omitempty"`
// SameSite is cookie 'Same-Site' status.
// Optional.
SameSite network.CookieSameSite `json:"sameSite,omitempty"`
}
// Api helps to interact with Chromium for converting HTML documents to PDF. // Api helps to interact with Chromium for converting HTML documents to PDF.
type Api interface { type Api interface {
Pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error Pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error

View File

@@ -34,6 +34,8 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
waitDelay time.Duration waitDelay time.Duration
waitWindowStatus string waitWindowStatus string
waitForExpression string waitForExpression string
cookies []Cookie
userAgent string
extraHttpHeaders map[string]string extraHttpHeaders map[string]string
emulatedMediaType string emulatedMediaType string
omitBackground bool omitBackground bool
@@ -58,6 +60,26 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay). Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay).
String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus). String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus).
String("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression). String("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression).
Custom("cookies", func(value string) error {
if value == "" {
cookies = defaultOptions.Cookies
return nil
}
err := json.Unmarshal([]byte(value), &cookies)
if err != nil {
return fmt.Errorf("unmarshal cookies: %w", err)
}
for i, cookie := range cookies {
if strings.TrimSpace(cookie.Name) == "" || strings.TrimSpace(cookie.Value) == "" || strings.TrimSpace(cookie.Domain) == "" {
err = multierr.Append(err, fmt.Errorf("cookie %d must have its name, value and domain set", i))
}
}
return err
}).
String("userAgent", &userAgent, defaultOptions.UserAgent).
Custom("extraHttpHeaders", func(value string) error { Custom("extraHttpHeaders", func(value string) error {
if value == "" { if value == "" {
extraHttpHeaders = defaultOptions.ExtraHttpHeaders extraHttpHeaders = defaultOptions.ExtraHttpHeaders
@@ -94,6 +116,8 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
WaitDelay: waitDelay, WaitDelay: waitDelay,
WaitWindowStatus: waitWindowStatus, WaitWindowStatus: waitWindowStatus,
WaitForExpression: waitForExpression, WaitForExpression: waitForExpression,
Cookies: cookies,
UserAgent: userAgent,
ExtraHttpHeaders: extraHttpHeaders, ExtraHttpHeaders: extraHttpHeaders,
EmulatedMediaType: emulatedMediaType, EmulatedMediaType: emulatedMediaType,
OmitBackground: omitBackground, OmitBackground: omitBackground,
@@ -161,12 +185,17 @@ func FormDataChromiumScreenshotOptions(ctx *api.Context) (*api.FormData, Screens
defaultScreenshotOptions := DefaultScreenshotOptions() defaultScreenshotOptions := DefaultScreenshotOptions()
var ( var (
width, height int
clip bool
format string format string
quality int quality int
optimizeForSpeed bool optimizeForSpeed bool
) )
form. form.
Int("width", &width, defaultScreenshotOptions.Width).
Int("height", &height, defaultScreenshotOptions.Height).
Bool("clip", &clip, defaultScreenshotOptions.Clip).
Custom("format", func(value string) error { Custom("format", func(value string) error {
if value == "" { if value == "" {
format = defaultScreenshotOptions.Format format = defaultScreenshotOptions.Format
@@ -207,6 +236,9 @@ func FormDataChromiumScreenshotOptions(ctx *api.Context) (*api.FormData, Screens
screenshotOptions := ScreenshotOptions{ screenshotOptions := ScreenshotOptions{
Options: options, Options: options,
Width: width,
Height: height,
Clip: clip,
Format: format, Format: format,
Quality: quality, Quality: quality,
OptimizeForSpeed: optimizeForSpeed, OptimizeForSpeed: optimizeForSpeed,

View File

@@ -62,6 +62,59 @@ func TestFormDataChromiumOptions(t *testing.T) {
return options return options
}(), }(),
}, },
{
scenario: "invalid cookies form field",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetValues(map[string][]string{
"cookies": {
"foo",
},
})
return ctx
}(),
expectedOptions: DefaultOptions(),
},
{
scenario: "invalid cookies form field (missing required values)",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetValues(map[string][]string{
"cookies": {
"[{}]",
},
})
return ctx
}(),
expectedOptions: func() Options {
options := DefaultOptions()
// No validation in this method, so it still instantiates
// an empty item.
options.Cookies = []Cookie{{}}
return options
}(),
},
{
scenario: "valid cookies form field",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetValues(map[string][]string{
"cookies": {
`[{"name":"foo","value":"bar","domain":".foo.bar"}]`,
},
})
return ctx
}(),
expectedOptions: func() Options {
options := DefaultOptions()
options.Cookies = []Cookie{{
Name: "foo",
Value: "bar",
Domain: ".foo.bar",
}}
return options
}(),
},
{ {
scenario: "invalid extraHttpHeaders form field", scenario: "invalid extraHttpHeaders form field",
ctx: func() *api.ContextMock { ctx: func() *api.ContextMock {
@@ -332,6 +385,15 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
ctx: func() *api.ContextMock { ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)} ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetValues(map[string][]string{ ctx.SetValues(map[string][]string{
"width": {
"1280",
},
"height": {
"800",
},
"clip": {
"true",
},
"optimizeForSpeed": { "optimizeForSpeed": {
"true", "true",
}, },
@@ -343,6 +405,9 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
}(), }(),
expectedOptions: func() ScreenshotOptions { expectedOptions: func() ScreenshotOptions {
options := DefaultScreenshotOptions() options := DefaultScreenshotOptions()
options.Width = 1280
options.Height = 800
options.Clip = true
options.OptimizeForSpeed = true options.OptimizeForSpeed = true
options.EmulatedMediaType = "screen" options.EmulatedMediaType = "screen"
return options return options

View File

@@ -3,6 +3,7 @@ package chromium
import ( import (
"bufio" "bufio"
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"time" "time"
@@ -120,6 +121,14 @@ func captureScreenshotActionFunc(logger *zap.Logger, outputPath string, options
WithOptimizeForSpeed(options.OptimizeForSpeed). WithOptimizeForSpeed(options.OptimizeForSpeed).
WithFormat(page.CaptureScreenshotFormat(options.Format)) WithFormat(page.CaptureScreenshotFormat(options.Format))
if options.Clip {
captureScreenshot = captureScreenshot.WithClip(&page.Viewport{
Width: float64(options.Width),
Height: float64(options.Height),
Scale: 1,
})
}
if options.Format == "jpeg" { if options.Format == "jpeg" {
captureScreenshot = captureScreenshot. captureScreenshot = captureScreenshot.
WithQuality(int64(options.Quality)) WithQuality(int64(options.Quality))
@@ -153,6 +162,19 @@ func captureScreenshotActionFunc(logger *zap.Logger, outputPath string, options
} }
} }
func setDeviceMetricsOverride(logger *zap.Logger, width, height int) chromedp.ActionFunc {
return func(ctx context.Context) error {
logger.Debug("set device metrics override")
err := emulation.SetDeviceMetricsOverride(int64(width), int64(height), 1.0, false).Do(ctx)
if err == nil {
return nil
}
return fmt.Errorf("set device metrics override: %w", err)
}
}
func clearCacheActionFunc(logger *zap.Logger, clear bool) chromedp.ActionFunc { func clearCacheActionFunc(logger *zap.Logger, clear bool) chromedp.ActionFunc {
return func(ctx context.Context) error { return func(ctx context.Context) error {
// See https://github.com/gotenberg/gotenberg/issues/753. // See https://github.com/gotenberg/gotenberg/issues/753.
@@ -210,6 +232,72 @@ func disableJavaScriptActionFunc(logger *zap.Logger, disable bool) chromedp.Acti
} }
} }
func setCookiesActionFunc(logger *zap.Logger, cookies []Cookie) chromedp.ActionFunc {
return func(ctx context.Context) error {
if len(cookies) == 0 {
logger.Debug("no cookies to set")
return nil
}
deadline, ok := ctx.Deadline()
if !ok {
return errors.New("context has no deadline, cannot set cookies")
}
epochTime := cdp.TimeSinceEpoch(deadline)
cookiePretty := func(c *network.SetCookieParams) string {
return fmt.Sprintf(
"Name: '%s', Value: '%s', Domain: '%s', Path: '%s', Secure: %t, HTTPOnly: %t, SameSite: '%s', Expires: %s",
c.Name,
c.Value,
c.Domain,
c.Path,
c.Secure,
c.HTTPOnly,
c.SameSite.String(),
c.Expires.Time().String(),
)
}
for _, cookie := range cookies {
cookieParams := network.
SetCookie(cookie.Name, cookie.Value).
WithDomain(cookie.Domain).
WithPath(cookie.Path).
WithSecure(cookie.Secure).
WithHTTPOnly(cookie.HttpOnly).
WithSameSite(cookie.SameSite).
WithExpires(&epochTime)
err := cookieParams.Do(ctx)
if err != nil {
return fmt.Errorf("set cookie %s: %w", cookiePretty(cookieParams), err)
}
logger.Debug(fmt.Sprintf("set cookie %s", cookiePretty(cookieParams)))
}
return nil
}
}
func userAgentOverride(logger *zap.Logger, userAgent string) chromedp.ActionFunc {
return func(ctx context.Context) error {
if len(userAgent) == 0 {
logger.Debug("no user agent override")
return nil
}
logger.Debug(fmt.Sprintf("user agent override: %s", userAgent))
err := emulation.SetUserAgentOverride(userAgent).Do(ctx)
if err == nil {
return nil
}
return fmt.Errorf("set user agent override: %w", err)
}
}
func extraHttpHeadersActionFunc(logger *zap.Logger, extraHttpHeaders map[string]string) chromedp.ActionFunc { func extraHttpHeadersActionFunc(logger *zap.Logger, extraHttpHeaders map[string]string) chromedp.ActionFunc {
return func(ctx context.Context) error { return func(ctx context.Context) error {
if len(extraHttpHeaders) == 0 { if len(extraHttpHeaders) == 0 {

View File

@@ -54,6 +54,21 @@ type Options struct {
// Optional. // Optional.
ExportFormFields bool ExportFormFields bool
// SinglePageSheets allows to output each sheet as a single page in the
// resulting PDF.
// Optional
SinglePageSheets bool
// LosslessImageCompression allows turning lossless compression on or off
// to tweak image conversion performance.
// Optional
LosslessImageCompression bool
// ReduceImageResolution allows turning on or off image resolution
// reduction to tweak image conversion performance.
// Optional
ReduceImageResolution bool
// PdfFormats allows to convert the resulting PDF to PDF/A-1b, PDF/A-2b, // PdfFormats allows to convert the resulting PDF to PDF/A-1b, PDF/A-2b,
// PDF/A-3b and PDF/UA. // PDF/A-3b and PDF/UA.
// Optional. // Optional.

View File

@@ -277,6 +277,18 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputP
args = append(args, "--export", "ExportFormFields=false") args = append(args, "--export", "ExportFormFields=false")
} }
if options.SinglePageSheets {
args = append(args, "--export", "SinglePageSheets=true")
}
if options.LosslessImageCompression {
args = append(args, "--export", "UseLosslessCompression=true")
}
if !options.ReduceImageResolution {
args = append(args, "--export", "ReduceImageResolution=false")
}
switch options.PdfFormats.PdfA { switch options.PdfFormats.PdfA {
case "": case "":
case gotenberg.PdfA1b: case gotenberg.PdfA1b:

View File

@@ -393,6 +393,35 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
start: true, start: true,
expectError: false, expectError: false,
}, },
{
scenario: "success (single page sheets)",
libreOffice: newLibreOfficeProcess(
libreOfficeArguments{
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
startTimeout: 5 * time.Second,
},
),
fs: func() *gotenberg.FileSystem {
fs := gotenberg.NewFileSystem()
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
if err != nil {
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
}
err = os.WriteFile(fmt.Sprintf("%s/document.txt", fs.WorkingDirPath()), []byte("SinglePageSheets"), 0o755)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return fs
}(),
options: Options{SinglePageSheets: true},
cancelledCtx: false,
start: true,
expectError: false,
},
{ {
scenario: "success (page ranges)", scenario: "success (page ranges)",
libreOffice: newLibreOfficeProcess( libreOffice: newLibreOfficeProcess(
@@ -422,6 +451,64 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
start: true, start: true,
expectError: false, expectError: false,
}, },
{
scenario: "success LosslessImageCompression",
libreOffice: newLibreOfficeProcess(
libreOfficeArguments{
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
startTimeout: 5 * time.Second,
},
),
fs: func() *gotenberg.FileSystem {
fs := gotenberg.NewFileSystem()
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
if err != nil {
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
}
err = os.WriteFile(fmt.Sprintf("%s/document.txt", fs.WorkingDirPath()), []byte("LosslessImageCompression"), 0o755)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return fs
}(),
options: Options{LosslessImageCompression: true},
cancelledCtx: false,
start: true,
expectError: false,
},
{
scenario: "success ReduceImageResolution",
libreOffice: newLibreOfficeProcess(
libreOfficeArguments{
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
startTimeout: 5 * time.Second,
},
),
fs: func() *gotenberg.FileSystem {
fs := gotenberg.NewFileSystem()
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
if err != nil {
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
}
err = os.WriteFile(fmt.Sprintf("%s/document.txt", fs.WorkingDirPath()), []byte("ReduceImageResolution"), 0o755)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return fs
}(),
options: Options{ReduceImageResolution: false},
cancelledCtx: false,
start: true,
expectError: false,
},
{ {
scenario: "success (PDF/A-1b)", scenario: "success (PDF/A-1b)",
libreOffice: newLibreOfficeProcess( libreOffice: newLibreOfficeProcess(

View File

@@ -25,15 +25,18 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
// Let's get the data from the form and validate them. // Let's get the data from the form and validate them.
var ( var (
inputPaths []string inputPaths []string
landscape bool landscape bool
nativePageRanges string nativePageRanges string
exportFormFields bool exportFormFields bool
pdfa string singlePageSheets bool
pdfua bool losslessImageCompression bool
nativePdfFormats bool reduceImageResolution bool
merge bool pdfa string
metadata map[string]interface{} pdfua bool
nativePdfFormats bool
merge bool
metadata map[string]interface{}
) )
err := ctx.FormData(). err := ctx.FormData().
@@ -41,6 +44,9 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
Bool("landscape", &landscape, false). Bool("landscape", &landscape, false).
String("nativePageRanges", &nativePageRanges, ""). String("nativePageRanges", &nativePageRanges, "").
Bool("exportFormFields", &exportFormFields, true). Bool("exportFormFields", &exportFormFields, true).
Bool("singlePageSheets", &singlePageSheets, false).
Bool("losslessImageCompression", &losslessImageCompression, false).
Bool("reduceImageResolution", &reduceImageResolution, true).
String("pdfa", &pdfa, ""). String("pdfa", &pdfa, "").
Bool("pdfua", &pdfua, false). Bool("pdfua", &pdfua, false).
Bool("nativePdfFormats", &nativePdfFormats, true). Bool("nativePdfFormats", &nativePdfFormats, true).
@@ -69,9 +75,12 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
for i, inputPath := range inputPaths { for i, inputPath := range inputPaths {
outputPaths[i] = ctx.GeneratePath(".pdf") outputPaths[i] = ctx.GeneratePath(".pdf")
options := libreofficeapi.Options{ options := libreofficeapi.Options{
Landscape: landscape, Landscape: landscape,
PageRanges: nativePageRanges, PageRanges: nativePageRanges,
ExportFormFields: exportFormFields, ExportFormFields: exportFormFields,
SinglePageSheets: singlePageSheets,
LosslessImageCompression: losslessImageCompression,
ReduceImageResolution: reduceImageResolution,
} }
if nativePdfFormats { if nativePdfFormats {

View File

@@ -174,7 +174,7 @@ func newLogEncoder(format string) (zapcore.Encoder, error) {
// If interactive terminal, make output more human-readable by default. // If interactive terminal, make output more human-readable by default.
// Credits: https://github.com/caddyserver/caddy/blob/v2.1.1/logging.go#L671. // Credits: https://github.com/caddyserver/caddy/blob/v2.1.1/logging.go#L671.
encCfg.EncodeTime = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) { encCfg.EncodeTime = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString(ts.UTC().Format("2006/01/02 15:04:05.000")) encoder.AppendString(ts.Local().Format("2006/01/02 15:04:05.000"))
} }
if format == textLoggingFormat || format == autoLoggingFormat { if format == textLoggingFormat || format == autoLoggingFormat {

View File

@@ -46,22 +46,22 @@ func (engine *PdfCpu) Merge(ctx context.Context, logger *zap.Logger, inputPaths
return nil return nil
} }
return fmt.Errorf("merge PDFs with PDFcpu: %w", err) return fmt.Errorf("merge PDFs with pdfcpu: %w", err)
} }
// Convert is not available in this implementation. // Convert is not available in this implementation.
func (engine *PdfCpu) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { func (engine *PdfCpu) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return fmt.Errorf("convert PDF to '%+v' with PDFcpu: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported) return fmt.Errorf("convert PDF to '%+v' with pdfcpu: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
} }
// ReadMetadata is not available in this implementation. // ReadMetadata is not available in this implementation.
func (engine *PdfCpu) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) { func (engine *PdfCpu) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
return nil, fmt.Errorf("read PDF metadata with PDFcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported) return nil, fmt.Errorf("read PDF metadata with pdfcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported)
} }
// WriteMetadata is not available in this implementation. // WriteMetadata is not available in this implementation.
func (engine *PdfCpu) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error { func (engine *PdfCpu) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
return fmt.Errorf("write PDF metadata with PDFcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported) return fmt.Errorf("write PDF metadata with pdfcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported)
} }
// Interface guards. // Interface guards.

View File

@@ -159,7 +159,7 @@ func webhookMiddleware(w *Webhook) api.Middleware {
} }
headers := map[string]string{ headers := map[string]string{
echo.HeaderContentType: echo.MIMEApplicationJSONCharsetUTF8, echo.HeaderContentType: echo.MIMEApplicationJSON,
c.Get("traceHeader").(string): c.Get("trace").(string), c.Get("traceHeader").(string): c.Get("trace").(string),
} }

View File

@@ -376,7 +376,7 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) {
return errors.New("foo") return errors.New("foo")
} }
}(), }(),
expectWebhookContentType: echo.MIMEApplicationJSONCharsetUTF8, expectWebhookContentType: echo.MIMEApplicationJSON,
expectWebhookMethod: http.MethodPost, expectWebhookMethod: http.MethodPost,
expectWebhookErrorStatus: http.StatusInternalServerError, expectWebhookErrorStatus: http.StatusInternalServerError,
expectWebhookErrorMessage: http.StatusText(http.StatusInternalServerError), expectWebhookErrorMessage: http.StatusText(http.StatusInternalServerError),
@@ -390,7 +390,7 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) {
return api.NewSentinelHttpError(http.StatusBadRequest, http.StatusText(http.StatusBadRequest)) return api.NewSentinelHttpError(http.StatusBadRequest, http.StatusText(http.StatusBadRequest))
} }
}(), }(),
expectWebhookContentType: echo.MIMEApplicationJSONCharsetUTF8, expectWebhookContentType: echo.MIMEApplicationJSON,
expectWebhookMethod: http.MethodPost, expectWebhookMethod: http.MethodPost,
expectWebhookErrorStatus: http.StatusBadRequest, expectWebhookErrorStatus: http.StatusBadRequest,
expectWebhookErrorMessage: http.StatusText(http.StatusBadRequest), expectWebhookErrorMessage: http.StatusText(http.StatusBadRequest),
@@ -430,7 +430,7 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) {
} }
}(), }(),
returnedError: echo.ErrInternalServerError, returnedError: echo.ErrInternalServerError,
expectWebhookContentType: echo.MIMEApplicationJSONCharsetUTF8, expectWebhookContentType: echo.MIMEApplicationJSON,
expectWebhookMethod: http.MethodPost, expectWebhookMethod: http.MethodPost,
expectWebhookErrorStatus: http.StatusInternalServerError, expectWebhookErrorStatus: http.StatusInternalServerError,
expectWebhookErrorMessage: http.StatusText(http.StatusInternalServerError), expectWebhookErrorMessage: http.StatusText(http.StatusInternalServerError),
@@ -493,7 +493,7 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) {
} }
} }
if contentType == echo.MIMEApplicationJSONCharsetUTF8 { if contentType == echo.MIMEApplicationJSON {
body, err := io.ReadAll(c.Request().Body) body, err := io.ReadAll(c.Request().Body)
if err != nil { if err != nil {
errChan <- err errChan <- err

View File

@@ -5,19 +5,19 @@
# Credits: https://github.com/thecodingmachine/docker-images-php. # Credits: https://github.com/thecodingmachine/docker-images-php.
set +e set +e
mkdir testing_file_system_rights.foo mkdir -p testing_file_system_rights.foo
chmod 700 testing_file_system_rights.foo chmod 700 testing_file_system_rights.foo
su gotenberg -c "touch testing_file_system_rights.foo/foo > /dev/null 2>&1" su gotenberg -c "touch testing_file_system_rights.foo/foo > /dev/null 2>&1"
HAS_CONSISTENT_RIGHTS=$? HAS_CONSISTENT_RIGHTS=$?
if [[ "$HAS_CONSISTENT_RIGHTS" != "0" ]]; then if [[ "$HAS_CONSISTENT_RIGHTS" != "0" ]]; then
# If not specified, the DOCKER_USER is the owner of the current working directory (heuristic!). # If not specified, the DOCKER_USER is the owner of the current working directory (heuristic!).
DOCKER_USER=$(stat -c '%U' "$(pwd)") DOCKER_USER=$(stat -c '%u' "$(pwd)")
else else
# macOs or Windows. # macOs or Windows.
# Note: in most cases, we don't care about the rights (they are not respected). # Note: in most cases, we don't care about the rights (they are not respected).
FILE_OWNER=$(stat -c '%U' "testing_file_system_rights.foo/foo") FILE_OWNER=$(stat -c '%u' "testing_file_system_rights.foo/foo")
if [[ "$FILE_OWNER" == "root" ]]; then if [[ "$FILE_OWNER" == "0" ]]; then
# If root, we are likely on a Windows host. # If root, we are likely on a Windows host.
# All files will belong to root, but it does not matter as everybody can write/delete # All files will belong to root, but it does not matter as everybody can write/delete
# those (0777 access rights). # those (0777 access rights).
@@ -56,4 +56,4 @@ go mod tidy
set +x set +x
# Run the command with the correct user. # Run the command with the correct user.
exec "sudo" "-E" "-H" "-u" "#$DOCKER_USER_ID" "$@" exec "sudo" "-E" "-H" "-u" "#$DOCKER_USER_ID" "$@"

14
test/testdata/api/README.md vendored Normal file
View File

@@ -0,0 +1,14 @@
To generate a valid certificate and private key use the following command:
```bash
# In OpenSSL ≥ 1.1.1
openssl req -x509 -newkey rsa:4096 -sha256 -days 9999 -nodes \
-keyout key.pem -out cert.pem -subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1,IP:::1"
```
To check a certificate use the following command:
```bash
openssl x509 -in cert.pem -text
```

30
test/testdata/api/cert.pem vendored Normal file
View File

@@ -0,0 +1,30 @@
-----BEGIN CERTIFICATE-----
MIIFOjCCAyKgAwIBAgIUZaht2QoZwWDK8wEuwm+wmip9sqwwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI0MDUyNjA1MDExNFoYDzIwNTEx
MDExMDUwMTE0WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEB
AQUAA4ICDwAwggIKAoICAQCm3TNckD55ae6pxXS03iV2P3X/edZT4vRa8D15elsi
CSRf6mdU2TVAQyg22GEcMXgsbba7sotJVg7559LkWAgb3Dj9rbPputdS29ebncat
x1nzo1QHMCS/v7nWoPe9fQVlDEf3ebM198f+5vhccSYFxFa+YvsUmSw9yMF8z1nA
6POZTpm6ErByAY9EGvC8EcNOQd9xLOfBZc0e+mY/rMRxmL+0N+sSRE/ACU+2zxHN
KcCiFX5WQdLDYW1ZjX5xAVatCPNlxSQpj97/ktGsnqwGnJWSTeMUOPv7L0LrQasf
k1VXBvfTW2/ZqhI9WA8qfdC5XOuzVwfU14gv+6vv9AGZnUFmOjeo9XG4xWBxlIKj
AQAzeNwAGFltPkp5a3yPczxMjKY4xLsMGcDfpaNtv1B24+7fV77Nyty/gLLT9pUa
FtXLuRfIPSaUK3kFhAuYzGUL/sO9O9jJkNlxxygxQ+VyIgmdw0zWrUi3q+Lfmdya
FFjHdM7LnLrLCjSv3dEoMINngR3+IejsY+98Ehd25O3oW+ZnGIQX0VwykV8sOQWq
rpnnZ+2iHMtF6Vsj40z/Y9y7yVE18AIJFC+Yr/dD/nQyg6SE70wdNcOx0Ay1xHbP
WuV3fqKOfhONR7NEBSO2UbHi6GZAiYMMwwul+4yKAQ4+lHOEXG4JQcKvtZasxq9v
GwIDAQABo4GBMH8wHQYDVR0OBBYEFPPMaH3C0rAkD/E0gtBZwM0x8N9MMB8GA1Ud
IwQYMBaAFPPMaH3C0rAkD/E0gtBZwM0x8N9MMA8GA1UdEwEB/wQFMAMBAf8wLAYD
VR0RBCUwI4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqG
SIb3DQEBCwUAA4ICAQB0DXz59JfQgfJvRJfraa+Cv/3nG4NNmHYtNGhrid5sagH3
LM8jkbA+JKRAn34SmsqUZb1f2XFI64TwrScgMfAvfi+5ibRqBjM/YL06bTlG61Ua
t2IQchV2kcX4zQnaMCIYG0dUj6JoXIspiFULMHyGDs7G9BKVcZxlaLrEvPXBqQjd
nwF80KHS/rCConmICU9uWHPgHxS1VmhQiwYGbrakETilau1ppRS0bpH2k/stVyjc
plttIr1DUD6xaRV4mZ0NZVlIXl2nDf78iQvreJeMrGl6AcNHzV+/rGHpY/gOrziP
PQkB1VtSiHtBe/VMKXcVB69SEzsoGEknqs4GPXuVYm47POo3/U5HmGU+FadSATNP
qtorRECUMbVn0IfDzJX2jBLUjZlhPI9gqO9DhiAT8FWHRV2yPsZKuiBBg5/hUvyb
Qsswtmi/YV5e6AnXEuvylwE3wYzlqDzxwlZM08bWFyZYnSf38d3hAjVwUHAugiaX
4sgE6GPAiONW5FkTZ4Z2oXYiN213QDIIcQeFAl04qSEFkGBV8x2hM0uXgFH1S++j
JePdKBbV/CjxegyyYsmnln+eO1kC8pCalB6k6pEtYMqNYOnmimC8UKrJATrH9EVq
eAehGBiIe514Oz06Ws2Vm4PTiY2x0KVbqphwkuz85ohSF9UXOHUcMryusr1zlg==
-----END CERTIFICATE-----

52
test/testdata/api/key.pem vendored Normal file
View File

@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCm3TNckD55ae6p
xXS03iV2P3X/edZT4vRa8D15elsiCSRf6mdU2TVAQyg22GEcMXgsbba7sotJVg75
59LkWAgb3Dj9rbPputdS29ebncatx1nzo1QHMCS/v7nWoPe9fQVlDEf3ebM198f+
5vhccSYFxFa+YvsUmSw9yMF8z1nA6POZTpm6ErByAY9EGvC8EcNOQd9xLOfBZc0e
+mY/rMRxmL+0N+sSRE/ACU+2zxHNKcCiFX5WQdLDYW1ZjX5xAVatCPNlxSQpj97/
ktGsnqwGnJWSTeMUOPv7L0LrQasfk1VXBvfTW2/ZqhI9WA8qfdC5XOuzVwfU14gv
+6vv9AGZnUFmOjeo9XG4xWBxlIKjAQAzeNwAGFltPkp5a3yPczxMjKY4xLsMGcDf
paNtv1B24+7fV77Nyty/gLLT9pUaFtXLuRfIPSaUK3kFhAuYzGUL/sO9O9jJkNlx
xygxQ+VyIgmdw0zWrUi3q+LfmdyaFFjHdM7LnLrLCjSv3dEoMINngR3+IejsY+98
Ehd25O3oW+ZnGIQX0VwykV8sOQWqrpnnZ+2iHMtF6Vsj40z/Y9y7yVE18AIJFC+Y
r/dD/nQyg6SE70wdNcOx0Ay1xHbPWuV3fqKOfhONR7NEBSO2UbHi6GZAiYMMwwul
+4yKAQ4+lHOEXG4JQcKvtZasxq9vGwIDAQABAoICAA3s7URkT/0XTMZOUZcRPanI
p12oGq2Vhd8GUvBjDbKhdgUAgu8fJsUzgD+o1Ic4eaxQ26x+Z1wPb+CF6vZeLQ8Q
1JDB1lSYiy/BuRSmQ4EX30yTQjKiCe8Ws7j9u1nM/Tju9XKUbX2rgr1QeURTDQvz
fr8UyO9xzly5oawmLHoKTDTBzS0s3Md8Pys9NFSoXCxDwxSm3WXTy8fcuHvH51Je
SmFik/OfpSgu9BVwN9lvX+wm7wwzzMBmzmwS1Y0zbLsJbDFUv9fYHJF8/JKeC1fh
6Xme67yG+kB+3o7TmXxhHKt5TBAPfsjOMLIojzDJ9IlY/91PqLpSjRhetdKorRlf
gvTUGLPY8lHXwz6UBpdHmMhNHTBBvgHcOVAMD46Y81SdqMhH6TAYSh7XQVuA06Ql
MTWzGqzKriaQDWmJ6Nx8eZCRDZGXDq4rKiJFvRFUrGYYr818qU8yC+qv5uLrOF1w
YZV0WnZ7DtcaFliBrJVUfeTmrSJBa3yoIKbeaTDuh3TMHqgAbTx0lBdGoI6noRxx
v7fCGho/aSOJ2rtFOjRoLQ5K8onZ226arJEyfkTh16wztQgh9DC430OkL4cQkfA0
BxN8tGh4P0lqwcBnCOyzi4NV1NkwaaVv+pt757Cz+Cy6e2DQOtDkrFEh4p56NqDr
KcJz88LYbbChoFKlb/kBAoIBAQDhcuI0KWTErojWu/dB2Okth8OFaTdvAPEvo+qO
ItJgxyYL8JIdWUzS9vKro6vMctwIXvNa3K2AAwdZ1kHvaxDZaikQbnjnSjOqRlZa
4rVnI6SSKrYj6xmKcsxxTi9YP8b2msjdLuGhPfO8Dw4EQgOt6xg0rq4msnv6bYki
G7vsk/InnKLm8SEhR1XsmtVrpC7KbXniLQEehgs1Kp0nMLptSsOTnA1kGoZDVlXc
u+kBBai6MM0MMDpsJgM1IFkUousQl3ItBcTXD2ZCMcg5psQJbdEhu7oXiGkHdKlL
1UEIdSGNlzHiVM62uNxhNfwh7I6SPesDksJbE0Yd//jz/7FRAoIBAQC9ee+pDsSs
yqunQjMWImTpD5h9fSRTzWT8h0dXsJyYi7s9Dj4mQIuoarscMI6L7njeTqHE8ptc
b9DzIEWL+233nTOS59grCTD84Kyrn+Hw6EebgKYmtpZnvMw5cccoZ3qUsxVGurhA
lzVaKpHtPKWT1fduNmnqt6RkZ7aRBLZre/i9UntZ7VSZYRKMOBvJLpLdqmiSPkvM
w8ra+cedHwDegqLkoX0sxdI1yNYO1GSkJ+w1I1m1rzuCd9X5QXvyQhPezVB9wUwr
anNGb6/YgvthzvMQAXYqv4pmb769przieMJ6geQL3O8QEtvpDAV4SvrwnA+H/wfs
BvwI4MTHNJ6rAoIBADEfnzpOaq6QeTCQ9GdcpDJPisLfEj0Vr3f593nRDJYZzqh7
WtsaQisVs/rCRwTdYiRQzCXYP9XM0yU4ElXgtmMpRplV+PIssOVBPj+/dldq5MkU
lWtuJrqMGQ/3QUmxW+EQCQRo2rEPlrQ7c4pp9/NFbpjMFxZHfcwrYd4UvPXnprQK
5VP85oMh0A8mDarOs4NYJ16o/71u57JF/sgzTevShpr7Pc+6F9dEUKEwMK0QVpt7
VrJ5L8Gw5rIEwmREu4N5/F7jCujxag6yWjZ1p/GGBDOdfAb0qi39tYXYibsWCqGX
eD8gy4n07dAguzeJG+expiu5JrBBmt2ekNhZtoECggEAQ90VoraAaOcaPgju5UGi
ZPtKAcA9r6pEPlJxkg3WuKhrgr3EMs0xeDvqEnmtxJ7AOdduB21hieUesMXhQvbl
9ycd3sMdYKSlIB2Ums/kC/YWF7MnT3xcOIGdoAGJdIZDmFjgCPo9nhzKZiYTKCZt
o5qiSZ5Bp2jo/3FKKXzHnY3ElIKjXZ0sjNcA3qoGAbOltLEhTSf03D1QPcfy3+rH
0+p0T3ErtUqRNORfDuaxuT33Uzz7F6DQrrUfNWxn6WgTV9yvtKr0DiTNbu/3ypN3
WBvYYOY1HbCG8JObfK/ovTDFbz1woXMbSOWIUFsW5nPslxs+UNZeTwJdaQygTj0h
iwKCAQBuXOPtjWGZINgfjo/n67Q8FuzUXxDK/IuXzuDXhi+B3/O3/PcVEDHFByhI
KHOh2mTD25vgFWh+8dNg1R+m5uIeQ5zbZ5U0205XzmrZgBXXCAHKNruTwqZfQ6er
np+8wHhcCbBuy0Z0ponzBGlrDYmbxO9anXGxoe/rE3+pWbzMneZ+cHgNjXCtfZCY
XGfLeJWW3+B8y5Lff39ADawmjm8VWVEdd8DP5o2zulbXhgMMnAsSVsR/XgwBqpbf
82k1bqvN2v08ID5nmTRdzD+D51Hjw80Uoqhm5DJL4IwaoK/YstCu2TCbmLGiyqSk
lrSOZMPTLX1tN/cWa+Ii+/WALnkI
-----END PRIVATE KEY-----