Compare commits

...

15 Commits

Author SHA1 Message Date
Johannes Klein
e4ee0f29d6 feat(libreoffice): add generateTaggedPdf form field
Tags the PDF without declaring PDF/UA conformance. Today tagging is only
available through pdfua, which also writes pdfuaid:part=1 into the XMP
metadata. Mirrors the Chromium routes, where generateTaggedPdf and pdfua
are separate fields. Defaults to false, so existing requests are
unchanged.

Closes #1676
2026-10-15 16:56:07 +02:00
Julien Neuhart
db150b7733 fix(chromium): report unreachable resources behind the pinning proxy as loading failures 2026-10-13 19:45:06 +02:00
Julien Neuhart
e1d714e5e0 fix(chromium): route loopback WebSockets through the pinning proxy when an outbound policy is set 2026-10-13 19:39:50 +02:00
Julien Neuhart
7b604da4f1 fix(chromium)!: isolate each conversion in its own browser context 2026-10-13 18:53:41 +02:00
Julien Neuhart
887727b40e test(chromium): wait for network idle in resource status code scenarios 2026-10-13 18:31:35 +02:00
Julien Neuhart
dd76e79443 fix(api): enforce api-body-limit before parsing multipart requests 2026-10-13 18:15:00 +02:00
dependabot[bot]
0a337e97b8 chore(deps): bump github.com/moby/moby/client from 0.6.0 to 0.6.2
Bumps [github.com/moby/moby/client](https://github.com/moby/moby) from 0.6.0 to 0.6.2.
- [Release notes](https://github.com/moby/moby/releases)
- [Changelog](https://github.com/moby/moby/blob/v0.6.2/CHANGELOG.md)
- [Commits](https://github.com/moby/moby/compare/v0.6.0...v0.6.2)

---
updated-dependencies:
- dependency-name: github.com/moby/moby/client
  dependency-version: 0.6.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-13 18:03:43 +02:00
Julien Neuhart
5af64d2124 chore(deps): bump OpenTelemetry to v1.47.0 and contrib to v0.72.0
The Logs API is now stable (go.opentelemetry.io/otel/log v1.47.0) and
go.opentelemetry.io/otel/log/global is deprecated. Register the logger
provider via otel.SetLoggerProvider instead.

Supersedes #1680, #1681 and #1683.
2026-10-13 17:51:58 +02:00
dependabot[bot]
2f895d9313 chore(deps): bump github.com/moby/moby/api from 1.56.0 to 1.56.1
Bumps [github.com/moby/moby/api](https://github.com/moby/moby) from 1.56.0 to 1.56.1.
- [Release notes](https://github.com/moby/moby/releases)
- [Commits](https://github.com/moby/moby/compare/api/v1.56.0...api/v1.56.1)

---
updated-dependencies:
- dependency-name: github.com/moby/moby/api
  dependency-version: 1.56.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-13 17:47:38 +02:00
Antonio Pauletich
c83dbd7baf test(chromium): cover header and footer image deduplication 2026-10-05 13:32:59 +02:00
dependabot[bot]
cc9cd5d82d chore(deps): bump github.com/shirou/gopsutil/v4 from 4.26.8 to 4.26.9
Bumps [github.com/shirou/gopsutil/v4](https://github.com/shirou/gopsutil) from 4.26.8 to 4.26.9.
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v4.26.8...v4.26.9)

---
updated-dependencies:
- dependency-name: github.com/shirou/gopsutil/v4
  dependency-version: 4.26.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-02 18:59:45 +02:00
dependabot[bot]
768f87bb1c chore(deps): bump github.com/labstack/echo/v5 from 5.3.1 to 5.4.0
Bumps [github.com/labstack/echo/v5](https://github.com/labstack/echo) from 5.3.1 to 5.4.0.
- [Release notes](https://github.com/labstack/echo/releases)
- [Changelog](https://github.com/labstack/echo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/labstack/echo/compare/v5.3.1...v5.4.0)

---
updated-dependencies:
- dependency-name: github.com/labstack/echo/v5
  dependency-version: 5.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-02 18:59:37 +02:00
dependabot[bot]
a5bc10b9d8 chore(deps-dev): bump prettier-plugin-sh from 0.19.0 to 0.20.2
Bumps [prettier-plugin-sh](https://github.com/un-ts/prettier) from 0.19.0 to 0.20.2.
- [Release notes](https://github.com/un-ts/prettier/releases)
- [Changelog](https://github.com/un-ts/prettier/blob/master/CHANGELOG.md)
- [Commits](https://github.com/un-ts/prettier/compare/prettier-plugin-sh@0.19.0...prettier-plugin-sh@0.20.2)

---
updated-dependencies:
- dependency-name: prettier-plugin-sh
  dependency-version: 0.20.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-02 18:59:20 +02:00
dependabot[bot]
ffc3cb98af chore(deps-dev): bump prettier from 3.9.8 to 3.9.9
Bumps [prettier](https://github.com/prettier/prettier) from 3.9.8 to 3.9.9.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.9.8...3.9.9)

---
updated-dependencies:
- dependency-name: prettier
  dependency-version: 3.9.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-02 18:59:08 +02:00
dependabot[bot]
8b46090eac chore(deps-dev): bump prettier from 3.9.6 to 3.9.8
Bumps [prettier](https://github.com/prettier/prettier) from 3.9.6 to 3.9.8.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.9.6...3.9.8)

---
updated-dependencies:
- dependency-name: prettier
  dependency-version: 3.9.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-25 06:46:33 +02:00
33 changed files with 987 additions and 183 deletions

View File

@@ -31,6 +31,7 @@ body:multipart-form {
~skipEmptyPages: false ~skipEmptyPages: false
~addOriginalDocumentAsStream: false ~addOriginalDocumentAsStream: false
~singlePageSheets: false ~singlePageSheets: false
~generateTaggedPdf: false
~initialView: 0 ~initialView: 0
~initialPage: 1 ~initialPage: 1
~magnification: 0 ~magnification: 0

View File

@@ -66,6 +66,7 @@ CHROMIUM_ALLOW_LIST=
CHROMIUM_DENY_LIST=^file:(?!//\/tmp/).* CHROMIUM_DENY_LIST=^file:(?!//\/tmp/).*
CHROMIUM_DENY_PRIVATE_IPS=false CHROMIUM_DENY_PRIVATE_IPS=false
CHROMIUM_DENY_PUBLIC_IPS=false CHROMIUM_DENY_PUBLIC_IPS=false
CHROMIUM_SHARED_BROWSER_CONTEXT=false
CHROMIUM_CLEAR_CACHE=false CHROMIUM_CLEAR_CACHE=false
CHROMIUM_CLEAR_COOKIES=false CHROMIUM_CLEAR_COOKIES=false
CHROMIUM_DISABLE_JAVASCRIPT=false CHROMIUM_DISABLE_JAVASCRIPT=false
@@ -154,6 +155,7 @@ NO_CONCURRENCY=false
# chromium # chromium
# chromium-concurrent # chromium-concurrent
# chromium-convert-html # chromium-convert-html
# chromium-image-deduplication
# chromium-convert-markdown # chromium-convert-markdown
# chromium-convert-url # chromium-convert-url
# chromium-screenshot-html # chromium-screenshot-html

View File

@@ -60,6 +60,7 @@ services:
- "--chromium-deny-list=${CHROMIUM_DENY_LIST}" - "--chromium-deny-list=${CHROMIUM_DENY_LIST}"
- "--chromium-deny-private-ips=${CHROMIUM_DENY_PRIVATE_IPS}" - "--chromium-deny-private-ips=${CHROMIUM_DENY_PRIVATE_IPS}"
- "--chromium-deny-public-ips=${CHROMIUM_DENY_PUBLIC_IPS}" - "--chromium-deny-public-ips=${CHROMIUM_DENY_PUBLIC_IPS}"
- "--chromium-shared-browser-context=${CHROMIUM_SHARED_BROWSER_CONTEXT}"
- "--chromium-clear-cache=${CHROMIUM_CLEAR_CACHE}" - "--chromium-clear-cache=${CHROMIUM_CLEAR_CACHE}"
- "--chromium-clear-cookies=${CHROMIUM_CLEAR_COOKIES}" - "--chromium-clear-cookies=${CHROMIUM_CLEAR_COOKIES}"
- "--chromium-disable-javascript=${CHROMIUM_DISABLE_JAVASCRIPT}" - "--chromium-disable-javascript=${CHROMIUM_DISABLE_JAVASCRIPT}"

72
go.mod
View File

@@ -12,27 +12,26 @@ require (
github.com/gomarkdown/markdown v0.0.0-20260824154242-13c5cf49db8d github.com/gomarkdown/markdown v0.0.0-20260824154242-13c5cf49db8d
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/hashicorp/go-retryablehttp v0.7.8 github.com/hashicorp/go-retryablehttp v0.7.8
github.com/labstack/echo/v5 v5.3.1 github.com/labstack/echo/v5 v5.4.0
github.com/labstack/gommon v0.5.0 github.com/labstack/gommon v0.5.0
github.com/mholt/archives v0.1.5 github.com/mholt/archives v0.1.5
github.com/microcosm-cc/bluemonday v1.0.27 github.com/microcosm-cc/bluemonday v1.0.27
github.com/moby/moby/api v1.56.0 github.com/moby/moby/api v1.56.1
github.com/moby/moby/client v0.6.0 github.com/moby/moby/client v0.6.2
github.com/prometheus/client_golang v1.24.1 github.com/prometheus/client_golang v1.24.1
github.com/shirou/gopsutil/v4 v4.26.8 github.com/shirou/gopsutil/v4 v4.26.9
github.com/spf13/pflag v1.0.10 github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.12.1 github.com/stretchr/testify v1.12.1
github.com/testcontainers/testcontainers-go v0.44.0 github.com/testcontainers/testcontainers-go v0.44.0
go.opentelemetry.io/contrib/bridges/otelslog v0.20.1 go.opentelemetry.io/contrib/bridges/otelslog v0.21.0
go.opentelemetry.io/contrib/exporters/autoexport v0.71.0 go.opentelemetry.io/contrib/exporters/autoexport v0.72.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0
go.opentelemetry.io/otel v1.46.0 go.opentelemetry.io/otel v1.47.0
go.opentelemetry.io/otel/log v0.22.0 go.opentelemetry.io/otel/metric v1.47.0
go.opentelemetry.io/otel/metric v1.46.0 go.opentelemetry.io/otel/sdk v1.47.0
go.opentelemetry.io/otel/sdk v1.46.0 go.opentelemetry.io/otel/sdk/log v1.47.0
go.opentelemetry.io/otel/sdk/log v0.22.0 go.opentelemetry.io/otel/sdk/metric v1.47.0
go.opentelemetry.io/otel/sdk/metric v1.46.0 go.opentelemetry.io/otel/trace v1.47.0
go.opentelemetry.io/otel/trace v1.46.0
golang.org/x/net v0.59.0 golang.org/x/net v0.59.0
golang.org/x/sync v0.23.0 golang.org/x/sync v0.23.0
golang.org/x/term v0.46.0 golang.org/x/term v0.46.0
@@ -41,7 +40,7 @@ require (
require ( require (
dario.cat/mergo v1.0.2 // indirect dario.cat/mergo v1.0.2 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/STARRY-S/zip v0.2.3 // indirect github.com/STARRY-S/zip v0.2.3 // indirect
github.com/andybalholm/brotli v1.2.3 // indirect github.com/andybalholm/brotli v1.2.3 // indirect
@@ -62,10 +61,10 @@ require (
github.com/cucumber/gherkin/go/v42 v42.0.1 // indirect github.com/cucumber/gherkin/go/v42 v42.0.1 // indirect
github.com/cucumber/messages/go/v34 v34.2.1 // indirect github.com/cucumber/messages/go/v34 v34.2.1 // indirect
github.com/distribution/reference v0.6.0 // indirect github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.8.1 // indirect github.com/docker/go-connections v0.8.2 // indirect
github.com/docker/go-units v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/ebitengine/purego v0.11.0 // indirect github.com/ebitengine/purego v0.11.1 // indirect
github.com/felixge/httpsnoop v1.1.0 // indirect github.com/felixge/httpsnoop v1.1.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.5 // indirect github.com/go-jose/go-jose/v4 v4.1.5 // indirect
github.com/go-json-experiment/json v0.0.0-20260820222146-c27c302e5fc3 // indirect github.com/go-json-experiment/json v0.0.0-20260820222146-c27c302e5fc3 // indirect
@@ -76,7 +75,7 @@ require (
github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect github.com/gobwas/ws v1.4.0 // indirect
github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/css v1.0.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-memdb v1.3.5 // indirect github.com/hashicorp/go-memdb v1.3.5 // indirect
@@ -104,7 +103,7 @@ require (
github.com/pierrec/lz4/v4 v4.1.29 // indirect github.com/pierrec/lz4/v4 v4.1.29 // indirect
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 // indirect github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 // indirect
github.com/prometheus/client_model v0.6.3 // indirect github.com/prometheus/client_model v0.6.3 // indirect
github.com/prometheus/common v0.71.0 // indirect github.com/prometheus/common v0.72.0 // indirect
github.com/prometheus/otlptranslator v1.0.0 // indirect github.com/prometheus/otlptranslator v1.0.0 // indirect
github.com/prometheus/procfs v0.22.0 // indirect github.com/prometheus/procfs v0.22.0 // indirect
github.com/sirupsen/logrus v1.10.2 // indirect github.com/sirupsen/logrus v1.10.2 // indirect
@@ -116,27 +115,28 @@ require (
github.com/ulikunitz/xz v0.5.16 // indirect github.com/ulikunitz/xz v0.5.16 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/bridges/prometheus v0.71.0 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.72.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.22.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.23.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.22.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.23.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.47.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.46.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.46.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.68.0 // indirect go.opentelemetry.io/otel/exporters/prometheus v0.69.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.22.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.23.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.46.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.47.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.46.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.47.0 // indirect
go.opentelemetry.io/proto/otlp v1.11.0 // indirect go.opentelemetry.io/otel/log v1.47.0 // indirect
go.opentelemetry.io/proto/otlp v1.11.1 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // indirect go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/crypto v0.57.0 // indirect golang.org/x/crypto v0.57.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/oauth2 v0.37.0 // indirect
golang.org/x/sys v0.48.0 // indirect golang.org/x/sys v0.48.0 // indirect
golang.org/x/time v0.15.0 // indirect golang.org/x/time v0.15.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260831171406-18b4a7587f8a // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260831171406-18b4a7587f8a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc // indirect
google.golang.org/grpc v1.83.2 // indirect google.golang.org/grpc v1.84.0 // indirect
google.golang.org/protobuf v1.36.12 // indirect google.golang.org/protobuf v1.36.12 // indirect
) )

153
go.sum
View File

@@ -2,8 +2,8 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 h1:m2xcjRABtG288vnlV3U/PKh/HnPURxZp50RKNUwBxM8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4/go.mod h1:MTUE7ZuZO89FAtAwozGg6AWD83pcdefnEq5gF82MB8s=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4= github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
@@ -60,15 +60,15 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2/v2 v2.8.0 h1:CekDhPLGfm+GAJmPVFIG+5dqMIQPkyHJll7BbdneDfw= github.com/dlclark/regexp2/v2 v2.8.0 h1:CekDhPLGfm+GAJmPVFIG+5dqMIQPkyHJll7BbdneDfw=
github.com/dlclark/regexp2/v2 v2.8.0/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU= github.com/dlclark/regexp2/v2 v2.8.0/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M= github.com/docker/go-connections v0.8.2 h1:9d9DEokx5W7qlr0shkFXIxEA7+TTD+llwzcykSlxMNc=
github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-connections v0.8.2/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/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/ebitengine/purego v0.11.0 h1:jhp/D+Nyv7UUW8HAcmcjt2N2rYrYi9m3SL21k0Ua/NI= github.com/ebitengine/purego v0.11.1 h1:2zpWRSQNVKN4eKsKO9eM1ILDgWfYMY9GwqRmK6XeQ/0=
github.com/ebitengine/purego v0.11.0/go.mod h1:DCHPP08djqhNSoTfImcnHYQRZmd0qhakvrozqaEYhGQ= github.com/ebitengine/purego v0.11.1/go.mod h1:DCHPP08djqhNSoTfImcnHYQRZmd0qhakvrozqaEYhGQ=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 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/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
@@ -102,8 +102,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 h1:/Tnpcb2E0Pz/tN9s3bfEY2Q8ePCEX9iuS+cneUwncnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0 h1:Bd7KaOxzULLxtZ/K5s1aLbWhR0+5RToO65TXHsf3bqQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0/go.mod h1:zOBXOsUaBSjKgmH4OGzV1esUpR3oUSCPYVd2cUBjKYY= github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0/go.mod h1:nN7ts3dFXKtCZWc//yfkpcQNKJABg16/uDVAZpLDalo=
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 v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
@@ -131,8 +131,8 @@ github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/labstack/echo/v5 v5.3.1 h1:75maCxkQVGualckLc/5s/ihgpH1a1Dc6AuGWNVNs6bw= github.com/labstack/echo/v5 v5.4.0 h1:iY674460IvSmUcj7MziL3YrgyTClghz5BpdZoKiGxR4=
github.com/labstack/echo/v5 v5.3.1/go.mod h1:4iEGNQiPPZnkfYpNR/L6fINd3NLiGWUD5+eBotFALas= github.com/labstack/echo/v5 v5.4.0/go.mod h1:4iEGNQiPPZnkfYpNR/L6fINd3NLiGWUD5+eBotFALas=
github.com/labstack/gommon v0.5.0 h1:6VSQ2NOzsnEJ5W6+84E0RbcaDDmgB6NIAzWCczTEe6c= github.com/labstack/gommon v0.5.0 h1:6VSQ2NOzsnEJ5W6+84E0RbcaDDmgB6NIAzWCczTEe6c=
github.com/labstack/gommon v0.5.0/go.mod h1:Rzlg7HHy1maLfzBYGg9NZcVuz1sA68HHhLjhcEllYE0= github.com/labstack/gommon v0.5.0/go.mod h1:Rzlg7HHy1maLfzBYGg9NZcVuz1sA68HHhLjhcEllYE0=
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=
@@ -157,10 +157,10 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.3.3 h1:OxxR9paxsluYi+zDUEXTTaIxtkK3viymW+Ka7vRhhME= github.com/moby/go-archive v0.3.3 h1:OxxR9paxsluYi+zDUEXTTaIxtkK3viymW+Ka7vRhhME=
github.com/moby/go-archive v0.3.3/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE= github.com/moby/go-archive v0.3.3/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
github.com/moby/moby/api v1.56.0 h1:GQzua3NA599ASSIICx0iFgiJeO9YkdDARvQsm23ZZuQ= github.com/moby/moby/api v1.56.1 h1:PpWkvVPB7Fr/No8w+TfyJ/I6rWZ2YPZhT3JorIg+06c=
github.com/moby/moby/api v1.56.0/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk= github.com/moby/moby/api v1.56.1/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk=
github.com/moby/moby/client v0.6.0 h1:AJjEB21QPbXSXjDsZorFBoDZPhMrfbpaPLgSMAW9Bgs= github.com/moby/moby/client v0.6.2 h1:ekSPlMccrZGcw6Scb8T9D39qE0mB+XupU/yS0zNw9N4=
github.com/moby/moby/client v0.6.0/go.mod h1:OCo00wNRyA3m4lmJ228W3JbyCN4ZNNYjpOXiJydBdcQ= github.com/moby/moby/client v0.6.2/go.mod h1:DGcnO7qmPJrqobx/ggQgQ/AUk+2wgVdQv7uxB0/mEsc=
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/mount v0.3.5 h1:eS3fsZTjHaBihwjp4/+5Z3jxqLXYsbwxqpVSfFv3M00= github.com/moby/sys/mount v0.3.5 h1:eS3fsZTjHaBihwjp4/+5Z3jxqLXYsbwxqpVSfFv3M00=
@@ -194,14 +194,14 @@ github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59u
github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE= github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo= github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
github.com/prometheus/common v0.71.0 h1:9KDAKb7Mj3HEVKyFCK6Dc/HIwlBzZIN2l7/lrHl3KK8= github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4=
github.com/prometheus/common v0.71.0/go.mod h1:CLJ5H8TEsGX8bl31BdMkfhIZ+QmZ9tBPPotUxUbfcmk= github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk=
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics= github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58= github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
github.com/shirou/gopsutil/v4 v4.26.8 h1:YQMTF/1J50B5+Y0vlo1eDRf5DoR7Gk69hY+8wjYkQeo= github.com/shirou/gopsutil/v4 v4.26.9 h1:CaBo/hBFqvlJoLvAQEKdHdf5GD97/MN2ACfyv63+2Ig=
github.com/shirou/gopsutil/v4 v4.26.8/go.mod h1:5O9FjBiXoTDFatIWjZZosqj4pV0DRtLx598xGbBehzM= github.com/shirou/gopsutil/v4 v4.26.9/go.mod h1:nKH+8wX2zxr/mDbsR+AAy6Qd2z4ZOQ7wfQaEp+yrYmg=
github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo= github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q= github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik= github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
@@ -237,56 +237,56 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/bridges/otelslog v0.20.1 h1:5sHc4ToTFjfSZCtGAAM6jPunICAmJX73htv372T4ipc= go.opentelemetry.io/contrib/bridges/otelslog v0.21.0 h1:/CdgE52i1OEPHhsGUqrggYNRaZy4v4YgVenGd52dRBI=
go.opentelemetry.io/contrib/bridges/otelslog v0.20.1/go.mod h1:oa6kgvyz/3GYW04dohd0++xJIH4xdQY8PAbpeCMaM8M= go.opentelemetry.io/contrib/bridges/otelslog v0.21.0/go.mod h1:9pBdbVbuehIDzxN4accnzzxEUPT8Nq7grbZMo+J1VN4=
go.opentelemetry.io/contrib/bridges/prometheus v0.71.0 h1:9qgxsFLskbDMXl8WMqThoF6w8yGJgCumn9qRc67OmnI= go.opentelemetry.io/contrib/bridges/prometheus v0.72.0 h1:OUIUu7ovHCEvG4IE5LQd2PNBH2eVYh50w/1ROwHEM7M=
go.opentelemetry.io/contrib/bridges/prometheus v0.71.0/go.mod h1:2rCjF4F2siiTeLCzJsaGZ3CK0XIoimCSKXEBPdv+Je0= go.opentelemetry.io/contrib/bridges/prometheus v0.72.0/go.mod h1:pCrnkC07AZ5+h+0A0Z/1GYyqB+xu3A8LaEEhxvv9s6o=
go.opentelemetry.io/contrib/exporters/autoexport v0.71.0 h1:VCsJbp0YLyPtx2tu5Vgv2a2/qLoaMCj8hT2uZ34+Mx0= go.opentelemetry.io/contrib/exporters/autoexport v0.72.0 h1:TPXrl7vEkmh69tp7dHTpz+Y6AUenFg1Du3Nx3a6dlxY=
go.opentelemetry.io/contrib/exporters/autoexport v0.71.0/go.mod h1:qxZqn7e10f6ajmMCkg/47rMS7qQYfaOl2nj/4aytHUQ= go.opentelemetry.io/contrib/exporters/autoexport v0.72.0/go.mod h1:0jE6K8nsOgeiF53rcEhy71cgakek+XEWi4OPROUqLoI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 h1:3g7B90UzBltIDKq1/5mrTGxTnOFDV0ICOhLoxiZ8jlg= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0 h1:LxwW/9ctSCv+QkE/cLR7M91ZIkXNMqJtEMi1vCw9U8s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0/go.mod h1:Ef8SuTh59BT7+ofpDxN9z+yOlc4t2GjLmKDgYNJL/NU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0/go.mod h1:tOsftB4SslBwwErVEPaenU2RpThXWPIU8DoJHEC4dyw=
go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc= go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM=
go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE= go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.22.0 h1:Bu39F5tzJct+f2IZbB8989fwyTps3c8e7EsUQsz+vs8= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.23.0 h1:5Ek4qQ5XX0PSQfY7lMW3CvcR36yy8YFDfGnYnaku8zE=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.22.0/go.mod h1:dJUwod88EsFgYCqrDHaSPzhiY9pBUpt0d85/qSfua7k= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.23.0/go.mod h1:NUKIMOKSexQdTmS1pSxOQBDiVes3DHRier2ePsGW/gQ=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.22.0 h1:lYk7RmxdLK865qLwibroNGldHa1U7SWKYYvNjlK7PIo= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.23.0 h1:5WVp40k2fJHuc5BtNTgKk+EugKFT8Eg3lywFfCPI/nE=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.22.0/go.mod h1:6GvlND0H0xdUJanOtIAn0xfwLkauh1tmsYEEVSMDdqY= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.23.0/go.mod h1:59nH0RYCGHRlwt15WyioeSYINBbuObeBpTowPhEKWHY=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0 h1:qkDYCAFiZXLcs1L4aY+tP2wguQ4kURANqHOQMA2et2s= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.47.0 h1:LaLVGJtIzHcvHjVs2OftmKcc+M9mdOqS8vZ1fBD1Yes=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0/go.mod h1:tkipS4DRzmpAmvg+Gw4++O1IdDq6TVDnvnYU6cmbQVs= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.47.0/go.mod h1:vF6H5sNDuQBJWgSX00iZgbIqk2MZZoVMH0ar5t5bOpw=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.46.0 h1:AP23h/mFgb/lc7tdck1Kfn9qxsM8TAeNPCU5C3pzaps= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0 h1:7kszRN4Ze5GeAkUnBjBSUIunVBMA+mZIG04auueqguM=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.46.0/go.mod h1:K4EqCe1b4kGk5WR690ntg9LaBfsPoV32FwthbyoptuA= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0/go.mod h1:CLzakYy8jOdkBfcveCWYkeKJxujcF4xPhh2Z7IfE98U=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 h1:OFnwLJr+pF3iHrlGSzbxyuo6/6HyBlnlN1CWEJmBVcw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0/go.mod h1:716wFneO0ov19A2beH5hjfh9AK5z/VWNAtDijp1Y0/g= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 h1:w53CDeOA/Kurp7yRsegSr6pbbr759dOvJ+yNmWM6Hxs= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0 h1:UFpxOpYPmMNUtOWhdb+nC1WELIgVf8z9higURrbzYU4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0/go.mod h1:BOmGMCbAtvcJiSJ+hLuhgPLdDbimnraSl8irz3iY8sY= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0/go.mod h1:YBGjxe3lt0jtXoEXxGrGhv/jM7oUBUDVK7zhAvNyEjc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.46.0 h1:KrC1YrQeSt46ITMWAbgQx1M1eV1/1TKzttrBzymPmss= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 h1:aXZXsZ012wOgVkqaiQv+Z/d8V0xKjmg70F6m4CE94lc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.46.0/go.mod h1:zDSEzoEqsOrgBeGvH66KRgxh90VonFyJqBHA0Pk3+rM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0/go.mod h1:ziu1gUIJhAaxM3EV1e3Ralk5AAyL1UOYz3lJJ4VSI38=
go.opentelemetry.io/otel/exporters/prometheus v0.68.0 h1:QOf2IftqQwITVRJpnn0M7M9ZCbgWfxz4P7i9C9yc2N4= go.opentelemetry.io/otel/exporters/prometheus v0.69.0 h1:LQVBTGnHvzrfGJpDoCSVTY7eaLZArvaTSq5l84Skxew=
go.opentelemetry.io/otel/exporters/prometheus v0.68.0/go.mod h1:bgSvqu2TWGXiz7yr5UTMfObH8oqxJWHTnubQ3ef9BO4= go.opentelemetry.io/otel/exporters/prometheus v0.69.0/go.mod h1:moif0nlgj/M16NRT8lDZVt8MC52+EOAJfR8EKRJ8LyU=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.22.0 h1:kvMAiLEudKmk+CSG+iYbU8vTUGNNDaf/V09OO5lrTwI= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.23.0 h1:PW/E9Tw6ZykC88pey0MZY8hKOUeEw+hX6PU+oXA4A9o=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.22.0/go.mod h1:L9Dlksri+MdT1cb2gIiA1cJJYW3Y92ipvDjNxYEyaDI= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.23.0/go.mod h1:hUw4VNfiOnQ/yzGgy6ZdWVnudXw7iRfImhd6FjXOnvI=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.46.0 h1:PR9eAf7o0dQs3hshZNZpE9aW2dXWX/KdDf6pJilVD3U= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.47.0 h1:ww5DYBpMKcqLdxjWrhGUIh6O+ZfvZUaMk4niVyNQPEQ=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.46.0/go.mod h1:2Z4KyNdH1uuzivdinyfGsxzNNT/Rl45pwtVwfYVI0xk= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.47.0/go.mod h1:Av5t+Ezx5S/Ks2dsEfWhny+GK+wrZ1K7NZ6aHSWnX3U=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.46.0 h1:KdRxPiAoMptR3vfWzvjjvutTsSiwbC2uG0496rzZNfo= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.47.0 h1:N3YQCxjxQ/bMjyc3heladfRm9t9RTksGQH8z4w6yU/0=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.46.0/go.mod h1:K/qSA+3G7Eovxi4K09wzrAgkWRnosS0DAOZeEpve7sM= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.47.0/go.mod h1:Mp8HOFqcaUyypCuGv9IhDdTHnJ56lSudSHMd+pVSCEA=
go.opentelemetry.io/otel/log v0.22.0 h1:5DBNnfvaJ6CVdkJ+Jle8Tzs50aSSv49TXGj9XRsEYw0= go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI=
go.opentelemetry.io/otel/log v0.22.0/go.mod h1:gzOt/R67vF2GniAqWu8Qv0SXy89f71muHcrkz76PCdc= go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw=
go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8= go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY=
go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o= go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU=
go.opentelemetry.io/otel/metric/x v0.68.0 h1:TA/cBT23D3MnxYPwHL7YFOdYGdx0A0v+s7Mzotpd1dU= go.opentelemetry.io/otel/metric/x v0.69.0 h1:DjRLr15H83v+hCW7JA9NoJvOkYTtmq5YoDRbe9deYpM=
go.opentelemetry.io/otel/metric/x v0.68.0/go.mod h1:agudOmvWhwUTjgibWDzxD2PoWYnpw5Ht5jISYOD2Hd4= go.opentelemetry.io/otel/metric/x v0.69.0/go.mod h1:uVvsMPMFFyj/HUQfrUnH3JjnOQ1dwFDorgFLRBasM0k=
go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI= go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I=
go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM= go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0=
go.opentelemetry.io/otel/sdk/log v0.22.0 h1:PRL+s6P63XT4E/bheEflopPUpVxuvANqZwtt89yhoGk= go.opentelemetry.io/otel/sdk/log v1.47.0 h1:W4N/ZfgpkRRnmARnqYoQ6BrWA0mrj8IzVDNLlpmUJZY=
go.opentelemetry.io/otel/sdk/log v0.22.0/go.mod h1:JNp0sBELrjCTcu5W3GzABVypeU6vDJjBS+X0JISuz+g= go.opentelemetry.io/otel/sdk/log v1.47.0/go.mod h1:Wu74Z2EmOKXi2FleSNAb0Hyau2G+r5kubSEMJw5aAMs=
go.opentelemetry.io/otel/sdk/log/logtest v0.22.0 h1:infPnfNrhCNgOUZRs3gWUg8vhoBUHihq02gwK05gzlg= go.opentelemetry.io/otel/sdk/log/logtest v0.23.0 h1:j3pm42QCrKYYLLkI6OrV1QHFkUfDA6/4EP+Y1PzggLg=
go.opentelemetry.io/otel/sdk/log/logtest v0.22.0/go.mod h1:gkQZA3z15Bv3KU9vigBTi8dFechSozRP7v94X4VZv+s= go.opentelemetry.io/otel/sdk/log/logtest v0.23.0/go.mod h1:zt0gEnuTZgDvqvczHwYswz93eqMaIr6upTTsdV/HUlU=
go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE= go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw=
go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4= go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw=
go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c= go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM=
go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI= go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0=
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= go.opentelemetry.io/proto/otlp v1.11.1 h1:SCrPqH9NE5CmBrOqpjNCiT0X5mGwMyGERXdogfR7Yjw=
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= go.opentelemetry.io/proto/otlp v1.11.1/go.mod h1:wq8W4aSf6bOy8RZyESOeQFr9Pu7wydWX/TY4sgvrTPg=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
@@ -299,13 +299,12 @@ golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/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.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
@@ -319,12 +318,12 @@ golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
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=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260831171406-18b4a7587f8a h1:i3TAXhpKc7TUP1VAPiBBrv45kamjoizCC3rOC0cAbOs= google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc h1:rZHRz0ogq4Pid1IKtA/ivHi8XONVF5tqUQXA2TtgpkE=
google.golang.org/genproto/googleapis/api v0.0.0-20260831171406-18b4a7587f8a/go.mod h1:CvYJHpbzPlT0fb/PsgtAamdwru/GVxUsomFdXTpOTI8= google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:K/n0XLazCJHjD73YuMyNJsN38BPPy1kfFnKxUXaThFU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260831171406-18b4a7587f8a h1:3Dnd1cDaZlB68lziofO+bJXpjOy8UfRv8Unt+yH8tQ4= google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc h1:4bNTbnb44EqGVy9HaQxSY2jnafSUdgV3qHtedvNpDKg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260831171406-18b4a7587f8a/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA= google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc=
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0=
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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=

24
package-lock.json generated
View File

@@ -5,9 +5,9 @@
"packages": { "packages": {
"": { "": {
"devDependencies": { "devDependencies": {
"prettier": "3.9.6", "prettier": "3.9.9",
"prettier-plugin-gherkin": "^4.0.0", "prettier-plugin-gherkin": "^4.0.0",
"prettier-plugin-sh": "^0.19.0" "prettier-plugin-sh": "^0.20.2"
} }
}, },
"node_modules/@cucumber/gherkin": { "node_modules/@cucumber/gherkin": {
@@ -114,9 +114,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.9.6", "version": "3.9.9",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz",
"integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", "integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@@ -142,14 +142,14 @@
} }
}, },
"node_modules/prettier-plugin-sh": { "node_modules/prettier-plugin-sh": {
"version": "0.19.0", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/prettier-plugin-sh/-/prettier-plugin-sh-0.19.0.tgz", "resolved": "https://registry.npmjs.org/prettier-plugin-sh/-/prettier-plugin-sh-0.20.2.tgz",
"integrity": "sha512-39VXFZH/cOGtcuu8aeSvqp/hhwomOR4QroZUj+jBz2cNb3os9s0sqFZSNlYts6jdtLLDU7D2YT3Z1+abtb7adQ==", "integrity": "sha512-4QVyY7Vav0f5i6AVa80SFwUJXaWsAv9t0yUZIivwI0ucxTei6Jwhso/khWqaKDj4QGdwjP2py1nxQrDUeZ0jyQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@reteps/dockerfmt": "^0.5.4", "@reteps/dockerfmt": "^0.5.4",
"sh-syntax": "^0.6.0" "sh-syntax": "^0.7.0"
}, },
"engines": { "engines": {
"node": ">=16.0.0" "node": ">=16.0.0"
@@ -169,9 +169,9 @@
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/sh-syntax": { "node_modules/sh-syntax": {
"version": "0.6.0", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/sh-syntax/-/sh-syntax-0.6.0.tgz", "resolved": "https://registry.npmjs.org/sh-syntax/-/sh-syntax-0.7.0.tgz",
"integrity": "sha512-52VK6z/cdZHv7UURjIcwfBUQZrAhIEEe0bY4lrkfypjnFIKsDZdD3Uaz/dBiw/sF8BeX0Mssv140s8EnrsJ9dQ==", "integrity": "sha512-sgMyAxpHiGgAaNg+gD3ad41vha24CzIjjghD4mdiON1lUafZPLJNYknyjcXmU4geilPstd69zCKkpccY0YvajA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {

View File

@@ -1,7 +1,7 @@
{ {
"devDependencies": { "devDependencies": {
"prettier": "3.9.6", "prettier": "3.9.9",
"prettier-plugin-gherkin": "^4.0.0", "prettier-plugin-gherkin": "^4.0.0",
"prettier-plugin-sh": "^0.19.0" "prettier-plugin-sh": "^0.20.2"
} }
} }

View File

@@ -11,7 +11,6 @@ import (
"go.opentelemetry.io/contrib/bridges/otelslog" "go.opentelemetry.io/contrib/bridges/otelslog"
"go.opentelemetry.io/contrib/exporters/autoexport" "go.opentelemetry.io/contrib/exporters/autoexport"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/log/global"
"go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/log" "go.opentelemetry.io/otel/sdk/log"
"go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric"
@@ -190,7 +189,7 @@ func InitLoggerProvider(logger *slog.Logger, serviceName, serviceVersion string)
loggerProvider := log.NewLoggerProvider(logOpts...) loggerProvider := log.NewLoggerProvider(logOpts...)
otelHandler := otelslog.NewHandler(serviceName, otelslog.WithLoggerProvider(loggerProvider)) otelHandler := otelslog.NewHandler(serviceName, otelslog.WithLoggerProvider(loggerProvider))
global.SetLoggerProvider(loggerProvider) otel.SetLoggerProvider(loggerProvider)
return loggerProvider.Shutdown, otelHandler, nil return loggerProvider.Shutdown, otelHandler, nil
} }

View File

@@ -166,13 +166,17 @@ func newContext(echoCtx *echo.Context, logger *slog.Logger, fs *gotenberg.FileSy
// downloaded remotely ("download from" feature). // downloaded remotely ("download from" feature).
var totalBytesRead atomic.Int64 var totalBytesRead atomic.Int64
bodyLimitErr := func() error {
return WrapError(
fmt.Errorf("body limit reached (> %d)", bodyLimit),
NewSentinelHttpError(http.StatusRequestEntityTooLarge, "The request body exceeds the configured size limit. Increase it with --api-body-limit, or send a smaller request."),
)
}
addReadBytes := func(n int64) error { addReadBytes := func(n int64) error {
newTotal := totalBytesRead.Add(n) newTotal := totalBytesRead.Add(n)
if bodyLimit != 0 && newTotal > bodyLimit { if bodyLimit != 0 && newTotal > bodyLimit {
return WrapError( return bodyLimitErr()
fmt.Errorf("body limit reached (> %d)", bodyLimit),
NewSentinelHttpError(http.StatusRequestEntityTooLarge, "The request body exceeds the configured size limit. Increase it with --api-body-limit, or send a smaller request."),
)
} }
return nil return nil
} }
@@ -217,8 +221,25 @@ func newContext(echoCtx *echo.Context, logger *slog.Logger, fs *gotenberg.FileSy
} }
}() }()
// The multipart parser reads the whole request and spools file parts to
// disk before the counter above sees a single byte, so the limit must
// also bound the raw body itself. Content-Length allows an early
// rejection, but it may be absent or lie, hence the reader.
if bodyLimit > 0 {
req := echoCtx.Request()
if req.ContentLength > bodyLimit {
return nil, cancel, fmt.Errorf("check content length: %w", bodyLimitErr())
}
req.Body = http.MaxBytesReader(echoCtx.Response(), req.Body, bodyLimit)
}
form, err := echoCtx.MultipartForm() form, err := echoCtx.MultipartForm()
if err != nil { if err != nil {
var maxBytesErr *http.MaxBytesError
if errors.As(err, &maxBytesErr) {
return nil, cancel, fmt.Errorf("get multipart form: %w", bodyLimitErr())
}
if errors.Is(err, http.ErrNotMultipart) { if errors.Is(err, http.ErrNotMultipart) {
return nil, cancel, WrapError( return nil, cancel, WrapError(
fmt.Errorf("get multipart form: %w", err), fmt.Errorf("get multipart form: %w", err),

View File

@@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"log/slog" "log/slog"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
@@ -902,3 +903,100 @@ func TestNewContext_DownloadFromRedirectVerdictStaysGeneric(t *testing.T) {
} }
} }
} }
type countingReadCloser struct {
r io.Reader
n int64
}
func (c *countingReadCloser) Read(p []byte) (int, error) {
n, err := c.r.Read(p)
c.n += int64(n)
return n, err
}
func (*countingReadCloser) Close() error { return nil }
// The body limit must stop the multipart parser itself. It used to apply
// only after the parser had read the whole request and spooled file parts
// to disk.
func TestNewContext_BodyLimitBoundsIngress(t *testing.T) {
body := new(bytes.Buffer)
writer := multipart.NewWriter(body)
err := writer.WriteField("foo", "bar")
if err != nil {
t.Fatalf("write multipart field: %v", err)
}
part, err := writer.CreateFormFile("files", "large.bin")
if err != nil {
t.Fatalf("create multipart file: %v", err)
}
_, err = part.Write(bytes.Repeat([]byte("A"), 2<<20))
if err != nil {
t.Fatalf("write multipart file: %v", err)
}
err = writer.Close()
if err != nil {
t.Fatalf("close multipart writer: %v", err)
}
size := int64(body.Len())
for _, tc := range []struct {
scenario string
bodyLimit int64
knownLength bool
expectErr bool
maxBytesConsumed int64
}{
{scenario: "known length over the limit is rejected before reading", bodyLimit: 1024, knownLength: true, expectErr: true, maxBytesConsumed: 0},
{scenario: "unknown length over the limit stops at the limit", bodyLimit: 1024, knownLength: false, expectErr: true, maxBytesConsumed: 1024 + 1},
{scenario: "unknown length one byte over the limit", bodyLimit: size - 1, knownLength: false, expectErr: true, maxBytesConsumed: size},
{scenario: "body exactly at the limit", bodyLimit: size, knownLength: true, expectErr: false},
{scenario: "body exactly at the limit with unknown length", bodyLimit: size, knownLength: false, expectErr: false},
{scenario: "no limit", bodyLimit: 0, knownLength: false, expectErr: false},
} {
t.Run(tc.scenario, func(t *testing.T) {
counted := &countingReadCloser{r: bytes.NewReader(body.Bytes())}
req := httptest.NewRequest(http.MethodPost, "/forms/pdfengines/merge", nil)
req.Body = counted
req.ContentLength = -1
if tc.knownLength {
req.ContentLength = size
}
req.Header.Set("Content-Type", writer.FormDataContentType())
echoCtx := echo.New().NewContext(req, httptest.NewRecorder())
logger := slog.New(slog.DiscardHandler)
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
_, cancel, err := newContext(echoCtx, logger, fs, 10*time.Second, tc.bodyLimit, downloadFromConfig{disable: true})
if cancel != nil {
defer cancel()
}
if !tc.expectErr {
if err != nil {
t.Fatalf("newContext returned error: %v", err)
}
if counted.n != size {
t.Fatalf("consumed %d bytes, want %d", counted.n, size)
}
return
}
if err == nil {
t.Fatal("expected the body limit to reject the request")
}
var httpErr HttpError
if !errors.As(err, &httpErr) {
t.Fatalf("expected an HttpError, got: %v", err)
}
if status, _ := httpErr.HttpError(); status != http.StatusRequestEntityTooLarge {
t.Fatalf("status = %d, want %d", status, http.StatusRequestEntityTooLarge)
}
if counted.n > tc.maxBytesConsumed {
t.Fatalf("consumed %d/%d bytes before rejection, want at most %d", counted.n, size, tc.maxBytesConsumed)
}
})
}
}

View File

@@ -12,10 +12,12 @@ import (
"time" "time"
cdprotobrowser "github.com/chromedp/cdproto/browser" cdprotobrowser "github.com/chromedp/cdproto/browser"
"github.com/chromedp/cdproto/cdp"
"github.com/chromedp/cdproto/fetch" "github.com/chromedp/cdproto/fetch"
"github.com/chromedp/cdproto/network" "github.com/chromedp/cdproto/network"
"github.com/chromedp/cdproto/page" "github.com/chromedp/cdproto/page"
"github.com/chromedp/cdproto/runtime" "github.com/chromedp/cdproto/runtime"
"github.com/chromedp/cdproto/target"
"github.com/chromedp/chromedp" "github.com/chromedp/chromedp"
"github.com/dlclark/regexp2/v2" "github.com/dlclark/regexp2/v2"
"github.com/shirou/gopsutil/v4/process" "github.com/shirou/gopsutil/v4/process"
@@ -62,14 +64,29 @@ type browserArguments struct {
hyphenDataDirPath string hyphenDataDirPath string
// Tasks specific. // Tasks specific.
allowList []*regexp2.Regexp allowList []*regexp2.Regexp
denyList []*regexp2.Regexp denyList []*regexp2.Regexp
denyPrivateIPs bool denyPrivateIPs bool
denyPublicIPs bool denyPublicIPs bool
clearCache bool sharedBrowserContext bool
clearCookies bool clearCache bool
clearStorage bool clearCookies bool
disableJavaScript bool clearStorage bool
disableJavaScript bool
}
// restrictsNetwork reports whether the outbound policy can reject a network
// URL. The default deny-list only rejects file:// URLs, so it does not count.
func (a browserArguments) restrictsNetwork() bool {
if a.denyPrivateIPs || a.denyPublicIPs || len(a.allowList) > 0 {
return true
}
for _, pattern := range a.denyList {
if pattern.String() != defaultDenyListPattern {
return true
}
}
return false
} }
type chromiumBrowser struct { type chromiumBrowser struct {
@@ -202,7 +219,7 @@ func (b *chromiumBrowser) Start(logger *slog.Logger) error {
} }
opts = append(opts, chromedp.ProxyServer(b.pinningProxy.URL())) opts = append(opts, chromedp.ProxyServer(b.pinningProxy.URL()))
if b.arguments.denyPrivateIPs || b.arguments.denyPublicIPs { if b.arguments.restrictsNetwork() {
// Chromium implicitly bypasses the proxy for loopback and // Chromium implicitly bypasses the proxy for loopback and
// link-local destinations. A WebSocket handshake is never surfaced // link-local destinations. A WebSocket handshake is never surfaced
// as a fetch.EventRequestPaused, so listenForEventRequestPaused // as a fetch.EventRequestPaused, so listenForEventRequestPaused
@@ -210,17 +227,14 @@ func (b *chromiumBrowser) Start(logger *slog.Logger) error {
// it. Left alone, a page could open a WebSocket to 127.0.0.1, ::1, // it. Left alone, a page could open a WebSocket to 127.0.0.1, ::1,
// localhost, or the link-local cloud metadata endpoint // localhost, or the link-local cloud metadata endpoint
// (169.254.169.254) and reach it unfiltered. "<-loopback>" removes // (169.254.169.254) and reach it unfiltered. "<-loopback>" removes
// the implicit bypass so those handshakes also traverse the pinning // the implicit bypass so those handshakes also traverse the
// proxy and go through [gotenberg.DecideOutbound] like every other // pinning proxy and go through [gotenberg.DecideOutbound] like
// request. // every other request.
// //
// Gated on the IP-class policy: it is the control this closes, and // Gated on a policy that can reject a network URL: without one,
// under it loopback and link-local HTTP sub-resources are already // loopback is not restricted, and routing it through the proxy
// blocked by listenForEventRequestPaused before they would reach // would only turn a refused loopback sub-resource into a 502
// the proxy, so this adds only the missing WebSocket coverage. When // response instead of a loading failure.
// the policy is off, loopback is not restricted, and routing it
// through the proxy would merely change how an unreachable loopback
// sub-resource reports its failure.
opts = append(opts, chromedp.Flag("proxy-bypass-list", "<-loopback>")) opts = append(opts, chromedp.Flag("proxy-bypass-list", "<-loopback>"))
} }
} }
@@ -435,6 +449,55 @@ func (b *chromiumBrowser) screenshot(ctx context.Context, logger *slog.Logger, u
}) })
} }
// newIsolatedTarget creates a browser context and opens a blank tab in it, so
// cookies, cache, and storage never leak to another conversion, even a
// concurrent one. The caller must dispose the browser context.
//
// chromedp.WithNewBrowserContext does not work here: it opens the tab without
// a new window, which headless Chromium rejects with "no browser is open", and
// it leaks the browser context on that failure.
func (b *chromiumBrowser) newIsolatedTarget(ctx context.Context, logger *slog.Logger) (cdp.BrowserContextID, target.ID, error) {
c := chromedp.FromContext(b.ctx)
if c == nil || c.Browser == nil {
return "", "", errors.New("browser not allocated")
}
executor := cdp.WithExecutor(ctx, c.Browser)
browserContextID, err := target.CreateBrowserContext().WithDisposeOnDetach(true).Do(executor)
if err != nil {
return "", "", fmt.Errorf("create browser context: %w", err)
}
targetID, err := target.CreateTarget("about:blank").
WithBrowserContextID(browserContextID).
WithNewWindow(true).
Do(executor)
if err != nil {
b.disposeBrowserContext(logger, browserContextID)
return "", "", fmt.Errorf("create target: %w", err)
}
return browserContextID, targetID, nil
}
// disposeBrowserContext disposes a browser context created by
// [chromiumBrowser.newIsolatedTarget]. It uses a fresh context, as the
// conversion's one may already be done.
func (b *chromiumBrowser) disposeBrowserContext(logger *slog.Logger, browserContextID cdp.BrowserContextID) {
c := chromedp.FromContext(b.ctx)
if c == nil || c.Browser == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := target.DisposeBrowserContext(browserContextID).Do(cdp.WithExecutor(ctx, c.Browser))
if err != nil {
logger.ErrorContext(ctx, fmt.Sprintf("dispose browser context: %s", err))
}
}
func (b *chromiumBrowser) do(ctx context.Context, logger *slog.Logger, url string, options Options, aggregate *networkAggregate, tasks chromedp.Tasks) error { func (b *chromiumBrowser) do(ctx context.Context, logger *slog.Logger, url string, options Options, aggregate *networkAggregate, tasks chromedp.Tasks) error {
if !b.isStarted.Load() { if !b.isStarted.Load() {
return errors.New("browser not started, cannot handle tasks") return errors.New("browser not started, cannot handle tasks")
@@ -461,7 +524,18 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *slog.Logger, url strin
timeoutCtx, timeoutCancel := context.WithTimeout(b.ctx, time.Until(deadline)) timeoutCtx, timeoutCancel := context.WithTimeout(b.ctx, time.Until(deadline))
defer timeoutCancel() defer timeoutCancel()
taskCtx, taskCancel := chromedp.NewContext(timeoutCtx) var taskOpts []chromedp.ContextOption
if !b.arguments.sharedBrowserContext {
browserContextID, targetID, err := b.newIsolatedTarget(timeoutCtx, logger)
if err != nil {
return fmt.Errorf("create isolated browser context: %w", err)
}
// Deferred before taskCancel, so it runs after the tab is closed.
defer b.disposeBrowserContext(logger, browserContextID)
taskOpts = append(taskOpts, chromedp.WithTargetID(targetID))
}
taskCtx, taskCancel := chromedp.NewContext(timeoutCtx, taskOpts...)
defer taskCancel() defer taskCancel()
// Accumulate per-conversion network activity for telemetry. // Accumulate per-conversion network activity for telemetry.

View File

@@ -6,6 +6,8 @@ import (
"slices" "slices"
"strings" "strings"
"testing" "testing"
"github.com/dlclark/regexp2/v2"
) )
// TestChromiumBrowser_Start_rejectsOverlappingStart guards against the latch // TestChromiumBrowser_Start_rejectsOverlappingStart guards against the latch
@@ -59,3 +61,36 @@ func TestChromiumDisableFeatures(t *testing.T) {
} }
} }
} }
// A WebSocket the outbound policy rejects must reach the pinning proxy, so
// the implicit loopback proxy bypass is removed whenever a policy can reject
// a network URL. See GHSA-73vq-mh8c-27wg.
func TestBrowserArguments_restrictsNetwork(t *testing.T) {
re := func(patterns ...string) []*regexp2.Regexp {
var res []*regexp2.Regexp
for _, p := range patterns {
res = append(res, regexp2.MustCompile(p, regexp2.None))
}
return res
}
for _, tc := range []struct {
scenario string
args browserArguments
expect bool
}{
{scenario: "default deny-list only", args: browserArguments{denyList: re(defaultDenyListPattern)}, expect: false},
{scenario: "no lists", args: browserArguments{}, expect: false},
{scenario: "allow-list", args: browserArguments{allowList: re(`^https://example\.com`), denyList: re(defaultDenyListPattern)}, expect: true},
{scenario: "custom deny-list", args: browserArguments{denyList: re(`^http://internal`)}, expect: true},
{scenario: "default and custom deny-list", args: browserArguments{denyList: re(defaultDenyListPattern, `^http://internal`)}, expect: true},
{scenario: "deny private IPs", args: browserArguments{denyList: re(defaultDenyListPattern), denyPrivateIPs: true}, expect: true},
{scenario: "deny public IPs", args: browserArguments{denyPublicIPs: true}, expect: true},
} {
t.Run(tc.scenario, func(t *testing.T) {
if got := tc.args.restrictsNetwork(); got != tc.expect {
t.Fatalf("restrictsNetwork() = %t, want %t", got, tc.expect)
}
})
}
}

View File

@@ -97,6 +97,10 @@ var (
ErrPageRangesExceedsPageCount = errors.New("page ranges exceeds page count") ErrPageRangesExceedsPageCount = errors.New("page ranges exceeds page count")
) )
// defaultDenyListPattern is the default --chromium-deny-list entry. It only
// rejects file:// URLs outside /tmp.
const defaultDenyListPattern = `^file:(?!//\/tmp/).*`
// Chromium is a module that provides both an [Api] and routes for converting // Chromium is a module that provides both an [Api] and routes for converting
// an HTML document to PDF. // an HTML document to PDF.
type Chromium struct { type Chromium struct {
@@ -476,12 +480,13 @@ func (mod *Chromium) Descriptor() gotenberg.ModuleDescriptor {
fs.String("chromium-proxy-server", "", "Set the outbound proxy server; this switch only affects HTTP and HTTPS requests") fs.String("chromium-proxy-server", "", "Set the outbound proxy server; this switch only affects HTTP and HTTPS requests")
fs.Bool("chromium-enable-environment-proxy", false, "Route Chromium's outbound requests through the proxy defined by the standard HTTP_PROXY, HTTPS_PROXY, and NO_PROXY variables, including credentials. Use this instead of --chromium-proxy-server for authenticated proxies, and leave --chromium-proxy-server and --chromium-host-resolver-rules unset") fs.Bool("chromium-enable-environment-proxy", false, "Route Chromium's outbound requests through the proxy defined by the standard HTTP_PROXY, HTTPS_PROXY, and NO_PROXY variables, including credentials. Use this instead of --chromium-proxy-server for authenticated proxies, and leave --chromium-proxy-server and --chromium-host-resolver-rules unset")
fs.StringSlice("chromium-allow-list", []string{}, `Set the allowed URLs for Chromium using regular expressions - supports multiple values. A match bypasses --chromium-deny-private-ips (CHROMIUM_DENY_PRIVATE_IPS) and --chromium-deny-public-ips (CHROMIUM_DENY_PUBLIC_IPS), so terminate the host or the pattern also matches suffix hosts, for example ^https?://internal\.svc(:|/|$)`) fs.StringSlice("chromium-allow-list", []string{}, `Set the allowed URLs for Chromium using regular expressions - supports multiple values. A match bypasses --chromium-deny-private-ips (CHROMIUM_DENY_PRIVATE_IPS) and --chromium-deny-public-ips (CHROMIUM_DENY_PUBLIC_IPS), so terminate the host or the pattern also matches suffix hosts, for example ^https?://internal\.svc(:|/|$)`)
fs.StringSlice("chromium-deny-list", []string{`^file:(?!//\/tmp/).*`}, "Set the denied URLs for Chromium using regular expressions - supports multiple values") fs.StringSlice("chromium-deny-list", []string{defaultDenyListPattern}, "Set the denied URLs for Chromium using regular expressions - supports multiple values")
fs.Bool("chromium-deny-private-ips", false, "Reject URLs whose host resolves to a non-public IP address (loopback, RFC1918, link-local, unique-local). Enable on deployments that accept untrusted form input to mitigate SSRF against internal services") fs.Bool("chromium-deny-private-ips", false, "Reject URLs whose host resolves to a non-public IP address (loopback, RFC1918, link-local, unique-local). Enable on deployments that accept untrusted form input to mitigate SSRF against internal services")
fs.Bool("chromium-deny-public-ips", false, "Reject URLs whose host resolves to a public IP address. Enable on air-gapped or data-governed deployments to prevent outbound traffic from leaving a private network") fs.Bool("chromium-deny-public-ips", false, "Reject URLs whose host resolves to a public IP address. Enable on air-gapped or data-governed deployments to prevent outbound traffic from leaving a private network")
fs.Bool("chromium-clear-cache", false, "Clear Chromium cache between each conversion") fs.Bool("chromium-shared-browser-context", false, "Run every conversion in one shared browser context, so cookies, cache, and storage carry over between conversions. Only enable it when all callers trust each other: a conversion can then reuse cookies another one set")
fs.Bool("chromium-clear-cookies", false, "Clear Chromium cookies between each conversion") fs.Bool("chromium-clear-cache", false, "Clear Chromium cache between each conversion - only applies with --chromium-shared-browser-context")
fs.Bool("chromium-clear-storage", false, "Clear Chromium local storage between each conversion (session storage is already isolated per conversion)") fs.Bool("chromium-clear-cookies", false, "Clear Chromium cookies between each conversion - only applies with --chromium-shared-browser-context")
fs.Bool("chromium-clear-storage", false, "Clear Chromium local storage between each conversion - only applies with --chromium-shared-browser-context")
fs.Bool("chromium-disable-javascript", false, "Disable JavaScript") fs.Bool("chromium-disable-javascript", false, "Disable JavaScript")
fs.Bool("chromium-disable-routes", false, "Disable the routes") fs.Bool("chromium-disable-routes", false, "Disable the routes")
@@ -515,6 +520,8 @@ func (mod *Chromium) Provision(ctx *gotenberg.Context) error {
return errors.New("CHROMIUM_HYPHEN_DATA_DIR_PATH environment variable is not set; set it to the absolute path of the Chromium hyphenation data directory (it ships in the Gotenberg image)") return errors.New("CHROMIUM_HYPHEN_DATA_DIR_PATH environment variable is not set; set it to the absolute path of the Chromium hyphenation data directory (it ships in the Gotenberg image)")
} }
sharedBrowserContext := flags.MustBool("chromium-shared-browser-context")
mod.args = browserArguments{ mod.args = browserArguments{
binPath: binPath, binPath: binPath,
allowInsecureLocalhost: flags.MustBool("chromium-allow-insecure-localhost"), allowInsecureLocalhost: flags.MustBool("chromium-allow-insecure-localhost"),
@@ -527,13 +534,16 @@ func (mod *Chromium) Provision(ctx *gotenberg.Context) error {
wsUrlReadTimeout: flags.MustDuration("chromium-start-timeout"), wsUrlReadTimeout: flags.MustDuration("chromium-start-timeout"),
hyphenDataDirPath: hyphenDataDirPath, hyphenDataDirPath: hyphenDataDirPath,
allowList: flags.MustRegexpSlice("chromium-allow-list"), allowList: flags.MustRegexpSlice("chromium-allow-list"),
denyList: flags.MustRegexpSlice("chromium-deny-list"), denyList: flags.MustRegexpSlice("chromium-deny-list"),
denyPrivateIPs: flags.MustBool("chromium-deny-private-ips"), denyPrivateIPs: flags.MustBool("chromium-deny-private-ips"),
denyPublicIPs: flags.MustBool("chromium-deny-public-ips"), denyPublicIPs: flags.MustBool("chromium-deny-public-ips"),
clearCache: flags.MustBool("chromium-clear-cache"), sharedBrowserContext: sharedBrowserContext,
clearCookies: flags.MustBool("chromium-clear-cookies"), // An isolated conversion starts from an empty browser context that
clearStorage: flags.MustBool("chromium-clear-storage"), // is disposed afterward, so there is nothing to clear.
clearCache: sharedBrowserContext && flags.MustBool("chromium-clear-cache"),
clearCookies: sharedBrowserContext && flags.MustBool("chromium-clear-cookies"),
clearStorage: sharedBrowserContext && flags.MustBool("chromium-clear-storage"),
disableJavaScript: flags.MustBool("chromium-disable-javascript"), disableJavaScript: flags.MustBool("chromium-disable-javascript"),
} }

View File

@@ -468,6 +468,8 @@ func listenForEventLoadingFailed(ctx context.Context, logger *slog.Logger, optio
"net::ERR_CONNECTION_REFUSED", "net::ERR_CONNECTION_REFUSED",
"net::ERR_CONNECTION_ABORTED", "net::ERR_CONNECTION_ABORTED",
"net::ERR_CONNECTION_FAILED", "net::ERR_CONNECTION_FAILED",
// What a pinning proxy CONNECT that cannot reach its target reports.
"net::ERR_TUNNEL_CONNECTION_FAILED",
"net::ERR_NAME_NOT_RESOLVED", "net::ERR_NAME_NOT_RESOLVED",
"net::ERR_INTERNET_DISCONNECTED", "net::ERR_INTERNET_DISCONNECTED",
"net::ERR_ADDRESS_UNREACHABLE", "net::ERR_ADDRESS_UNREACHABLE",

View File

@@ -522,7 +522,7 @@ func (p *pinningProxy) handleForward(w http.ResponseWriter, req *http.Request) {
} else { } else {
p.logger.WarnContext(req.Context(), fmt.Sprintf("forward RoundTrip failed for '%s': %s", req.URL, err)) p.logger.WarnContext(req.Context(), fmt.Sprintf("forward RoundTrip failed for '%s': %s", req.URL, err))
} }
http.Error(w, "upstream error", http.StatusBadGateway) resetClient(w)
return return
} }
defer resp.Body.Close() defer resp.Body.Close()
@@ -585,3 +585,23 @@ func isClientCancellation(ctx context.Context, err error) bool {
func (p *pinningProxy) dialThroughUpstreamProxy(ctx context.Context, proxyURL *url.URL, target string) (net.Conn, error) { func (p *pinningProxy) dialThroughUpstreamProxy(ctx context.Context, proxyURL *url.URL, target string) (net.Conn, error) {
return gotenberg.DialThroughProxy(ctx, proxyURL, target, p.dialBypass) return gotenberg.DialThroughProxy(ctx, proxyURL, target, p.dialBypass)
} }
// resetClient aborts the client connection with a TCP reset instead of
// answering. Chromium treats a proxy response as the origin's own, so a 502
// would reach the page as an ordinary HTTP error; a reset surfaces as a
// network failure, as it would without the proxy.
func resetClient(w http.ResponseWriter) {
hj, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "upstream error", http.StatusBadGateway)
return
}
conn, _, err := hj.Hijack()
if err != nil {
return
}
if tcpConn, ok := conn.(*net.TCPConn); ok {
_ = tcpConn.SetLinger(0)
}
_ = conn.Close()
}

View File

@@ -15,6 +15,7 @@ import (
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"syscall"
"testing" "testing"
"time" "time"
@@ -764,6 +765,47 @@ func TestPinningProxy_CONNECT_DialFailure_LoggedAtWarn(t *testing.T) {
} }
} }
// A forward request whose target cannot be reached resets the client
// connection rather than answering 502. Chromium treats a proxy response as
// the origin's own, so a 502 would hide the failure from
// failOnResourceLoadingFailed.
func TestPinningProxy_Forward_DialFailure_ResetsClient(t *testing.T) {
p := newPinningProxy(nil, nil, false, false, false)
p.decide = func(_ context.Context, _ string, _, _ []*regexp2.Regexp, _ time.Time) (gotenberg.OutboundDecision, error) {
return gotenberg.OutboundDecision{Pinned: []netip.Addr{netip.MustParseAddr("127.0.0.1")}}, nil
}
p.dialPinned = func(_ context.Context, _ string, _ []netip.Addr, _ string) (net.Conn, error) {
return nil, errors.New("connection refused")
}
logger := slog.New(slog.DiscardHandler)
err := p.Start(logger)
if err != nil {
t.Fatalf("start pinning proxy: %v", err)
}
t.Cleanup(func() { _ = p.Stop(logger) })
conn, err := net.Dial("tcp", strings.TrimPrefix(p.URL(), "http://"))
if err != nil {
t.Fatalf("dial proxy: %v", err)
}
defer conn.Close()
_ = conn.SetDeadline(time.Now().Add(5 * time.Second))
_, err = fmt.Fprintf(conn, "GET http://real.example/ HTTP/1.1\r\nHost: real.example\r\n\r\n")
if err != nil {
t.Fatalf("write request: %v", err)
}
resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
if err == nil {
_ = resp.Body.Close()
t.Fatalf("got status %d, want the connection reset", resp.StatusCode)
}
if !errors.Is(err, syscall.ECONNRESET) && !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) {
t.Fatalf("read response: %v, want a connection reset", err)
}
}
// TestPinningProxy_Forward_RoundTripCancellation_LoggedAtDebug verifies // TestPinningProxy_Forward_RoundTripCancellation_LoggedAtDebug verifies
// the handleForward dial-cancellation path: when the inner Transport's // the handleForward dial-cancellation path: when the inner Transport's
// dial returns a canceled error (client hung up mid-dial), the proxy // dial returns a canceled error (client hung up mid-dial), the proxy
@@ -790,12 +832,9 @@ func TestPinningProxy_Forward_RoundTripCancellation_LoggedAtDebug(t *testing.T)
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
} }
resp, err := client.Get("http://www.google.com/") resp, err := client.Get("http://www.google.com/")
if err != nil { if err == nil {
t.Fatalf("GET via proxy: %v", err) _ = resp.Body.Close()
} t.Fatalf("GET via proxy: got status %d, want the connection reset", resp.StatusCode)
defer resp.Body.Close()
if resp.StatusCode != http.StatusBadGateway {
t.Fatalf("status = %d, want 502", resp.StatusCode)
} }
records := rec.snapshot() records := rec.snapshot()

View File

@@ -266,6 +266,10 @@ type Options struct {
// NativeTiledWatermarkText specifies the tiled watermark text. // NativeTiledWatermarkText specifies the tiled watermark text.
NativeTiledWatermarkText string NativeTiledWatermarkText string
// GenerateTaggedPdf specifies whether to generate a tagged (accessible)
// PDF without declaring PDF/UA conformance. PdfFormats.PdfUa implies it.
GenerateTaggedPdf 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.
PdfFormats gotenberg.PdfFormats PdfFormats gotenberg.PdfFormats
@@ -318,6 +322,7 @@ func DefaultOptions() Options {
NativeWatermarkRotateAngle: 0, NativeWatermarkRotateAngle: 0,
NativeWatermarkFontName: "Helvetica", NativeWatermarkFontName: "Helvetica",
NativeTiledWatermarkText: "", NativeTiledWatermarkText: "",
GenerateTaggedPdf: false,
PdfFormats: gotenberg.PdfFormats{ PdfFormats: gotenberg.PdfFormats{
PdfA: "", PdfA: "",
PdfUa: false, PdfUa: false,

View File

@@ -437,7 +437,7 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *slog.Logger, input
args = append( args = append(
args, args,
"--export", "PDFUACompliance=false", "--export", "PDFUACompliance=false",
"--export", "UseTaggedPDF=false", "--export", fmt.Sprintf("UseTaggedPDF=%t", options.GenerateTaggedPdf),
"--export", "EnableTextAccessForAccessibilityTools=false", "--export", "EnableTextAccessForAccessibilityTools=false",
) )
} }

View File

@@ -75,6 +75,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
skipEmptyPages bool skipEmptyPages bool
addOriginalDocumentAsStream bool addOriginalDocumentAsStream bool
singlePageSheets bool singlePageSheets bool
generateTaggedPdf bool
losslessImageCompression bool losslessImageCompression bool
quality int quality int
reduceImageResolution bool reduceImageResolution bool
@@ -126,6 +127,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
Bool("skipEmptyPages", &skipEmptyPages, defaultOptions.SkipEmptyPages). Bool("skipEmptyPages", &skipEmptyPages, defaultOptions.SkipEmptyPages).
Bool("addOriginalDocumentAsStream", &addOriginalDocumentAsStream, defaultOptions.AddOriginalDocumentAsStream). Bool("addOriginalDocumentAsStream", &addOriginalDocumentAsStream, defaultOptions.AddOriginalDocumentAsStream).
Bool("singlePageSheets", &singlePageSheets, defaultOptions.SinglePageSheets). Bool("singlePageSheets", &singlePageSheets, defaultOptions.SinglePageSheets).
Bool("generateTaggedPdf", &generateTaggedPdf, defaultOptions.GenerateTaggedPdf).
Custom("initialView", func(value string) error { Custom("initialView", func(value string) error {
if value == "" { if value == "" {
initialView = defaultOptions.InitialView initialView = defaultOptions.InitialView
@@ -396,6 +398,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
NativeWatermarkRotateAngle: nativeWatermarkRotateAngle, NativeWatermarkRotateAngle: nativeWatermarkRotateAngle,
NativeWatermarkFontName: nativeWatermarkFontName, NativeWatermarkFontName: nativeWatermarkFontName,
NativeTiledWatermarkText: nativeTiledWatermarkText, NativeTiledWatermarkText: nativeTiledWatermarkText,
GenerateTaggedPdf: generateTaggedPdf,
} }
if nativePdfFormats && splitMode == zeroValuedSplitMode && !hasPostProcessing { if nativePdfFormats && splitMode == zeroValuedSplitMode && !hasPostProcessing {

View File

@@ -25,7 +25,7 @@ Available tags:
| Group | Tags | | Group | Tags |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Chromium | `chromium`, `chromium-concurrent`, `chromium-convert-html`, `chromium-convert-markdown`, `chromium-convert-url`, `chromium-screenshot-html`, `chromium-screenshot-markdown`, `chromium-screenshot-url`, `chromium-ssrf` | | Chromium | `chromium`, `chromium-concurrent`, `chromium-convert-html`, `chromium-image-deduplication`, `chromium-convert-markdown`, `chromium-convert-url`, `chromium-screenshot-html`, `chromium-screenshot-markdown`, `chromium-screenshot-url`, `chromium-ssrf` |
| LibreOffice | `libreoffice`, `libreoffice-convert`, `libreoffice-ssrf` | | LibreOffice | `libreoffice`, `libreoffice-convert`, `libreoffice-ssrf` |
| PDF Engines | `pdfengines`, `pdfengines-convert`, `pdfengines-merge`, `merge`, `pdfengines-split`, `split`, `pdfengines-flatten`, `flatten`, `pdfengines-optimize`, `optimize`, `pdfengines-rotate`, `rotate`, `pdfengines-embed`, `embed`, `pdfengines-encrypt`, `encrypt`, `pdfengines-watermark`, `watermark`, `pdfengines-stamp`, `stamp`, `pdfengines-metadata`, `metadata`, `pdfengines-bookmarks`, `bookmarks` | | PDF Engines | `pdfengines`, `pdfengines-convert`, `pdfengines-merge`, `merge`, `pdfengines-split`, `split`, `pdfengines-flatten`, `flatten`, `pdfengines-optimize`, `optimize`, `pdfengines-rotate`, `rotate`, `pdfengines-embed`, `embed`, `pdfengines-encrypt`, `encrypt`, `pdfengines-watermark`, `watermark`, `pdfengines-stamp`, `stamp`, `pdfengines-metadata`, `metadata`, `pdfengines-bookmarks`, `bookmarks` |
| Infra | `health`, `debug`, `root`, `version`, `output-filename`, `prometheus-metrics`, `webhook`, `download-from` | | Infra | `health`, `debug`, `root`, `version`, `output-filename`, `prometheus-metrics`, `webhook`, `download-from` |
@@ -67,6 +67,7 @@ Available tags:
- `there should be the following file(s) in the (response|webhook request):` (table of filenames) - `there should be the following file(s) in the (response|webhook request):` (table of filenames)
- `the "<name>" PDF should have <N> page(s)` - `the "<name>" PDF should have <N> page(s)`
- `the "<name>" PDF (should|should NOT) be set to landscape orientation` - `the "<name>" PDF (should|should NOT) be set to landscape orientation`
- `the "<name>" PDF (should|should NOT) be tagged`
- `the "<name>" PDF (should|should NOT) have the following content at page <N>:` (docstring) - `the "<name>" PDF (should|should NOT) have the following content at page <N>:` (docstring)
- `the "<name>" PDF (should|should NOT) have content matching "<regexp>" at page <N>` - `the "<name>" PDF (should|should NOT) have content matching "<regexp>" at page <N>`
- `the (response|webhook request) PDF(s) should be valid "<standard>" with a tolerance of <N> failed rule(s)` (standards: `PDF/A-1b`, `PDF/A-2b`, `PDF/A-3b`, `PDF/UA-1`, `PDF/UA-2`) - `the (response|webhook request) PDF(s) should be valid "<standard>" with a tolerance of <N> failed rule(s)` (standards: `PDF/A-1b`, `PDF/A-2b`, `PDF/A-3b`, `PDF/UA-1`, `PDF/UA-2`)
@@ -75,6 +76,7 @@ Available tags:
- `the (response|webhook request) PDF(s) (should|should NOT) allow "<action>"` (actions: `printing`, `copying`, `modifying`, `annotating`) - `the (response|webhook request) PDF(s) (should|should NOT) allow "<action>"` (actions: `printing`, `copying`, `modifying`, `annotating`)
- `the (response|webhook request) PDF(s) (should|should NOT) have the "<filename>" file embedded` - `the (response|webhook request) PDF(s) (should|should NOT) have the "<filename>" file embedded`
- `the "<name>" PDF should have <N> image(s)` - `the "<name>" PDF should have <N> image(s)`
- `the "<name>" PDF should reuse <N> image XObject(s) across <P> page(s)` (checks image appearances on every page and distinct PDF object IDs)
- `the Gotenberg container (should|should NOT) log the following entries:` (table of log substrings) - `the Gotenberg container (should|should NOT) log the following entries:` (table of log substrings)
- `all concurrent response status codes should be <code>` - `all concurrent response status codes should be <code>`
- `all concurrent responses should have <N> PDF(s)` - `all concurrent responses should have <N> PDF(s)`

View File

@@ -521,6 +521,22 @@ Feature: /forms/chromium/convert/html
| 'wss://127.0.0.1:9999/wss-test' targets a non-public address | | 'wss://127.0.0.1:9999/wss-test' targets a non-public address |
| CONNECT blocked for '127.0.0.1:9999' | | CONNECT blocked for '127.0.0.1:9999' |
# An allow-list is enforced on its own, without the IP-class flags. Chromium
# skips the proxy for loopback and link-local destinations unless told
# otherwise, so a WebSocket the allow-list rejects must still reach the
# pinning proxy to be severed.
@chromium-ssrf
Scenario: POST /forms/chromium/convert/html (WebSocket not in the allow-list is filtered)
Given I have a Gotenberg container with the following environment variable(s):
| CHROMIUM_ALLOW_LIST | ^file:///tmp/.* |
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
| files | testdata/ssrf-websocket-html/index.html | file |
| waitDelay | 1s | field |
Then the response status code should be 200
Then the Gotenberg container should log the following entries:
| 'ws://127.0.0.1:9999/ssrf-websocket' does not match any expression from the allowed list |
| CONNECT blocked for '127.0.0.1:9999' |
# EventSource issues an ordinary HTTP GET, so unlike a WebSocket it IS surfaced # EventSource issues an ordinary HTTP GET, so unlike a WebSocket it IS surfaced
# as a fetch.EventRequestPaused and blocked by listenForEventRequestPaused. # as a fetch.EventRequestPaused and blocked by listenForEventRequestPaused.
@chromium-ssrf @chromium-ssrf
@@ -609,6 +625,7 @@ Feature: /forms/chromium/convert/html
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
| files | testdata/feature-rich-html/index.html | file | | files | testdata/feature-rich-html/index.html | file |
| failOnResourceHttpStatusCodes | [499,599] | field | | failOnResourceHttpStatusCodes | [499,599] | field |
| skipNetworkIdleEvent | false | field |
Then the response status code should be 409 Then the response status code should be 409
Then the response header "Content-Type" should be "text/plain; charset=UTF-8" Then the response header "Content-Type" should be "text/plain; charset=UTF-8"
Then the response body should match string: Then the response body should match string:
@@ -627,12 +644,29 @@ Feature: /forms/chromium/convert/html
Then the response header "Content-Type" should be "application/pdf" Then the response header "Content-Type" should be "application/pdf"
Then there should be 1 PDF(s) in the response Then there should be 1 PDF(s) in the response
Scenario: POST /forms/chromium/convert/html (Fail On Resource Loading Failed) # Through the pinning proxy, an unreachable resource must still fail the
# conversion. The proxy resets the connection instead of answering 502, which
# Chromium would treat as an ordinary HTTP error.
Scenario: POST /forms/chromium/convert/html (Fail On Resource Loading Failed through the pinning proxy)
Given I have a default Gotenberg container Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
| files | testdata/feature-rich-html/index.html | file | | files | testdata/feature-rich-html/index.html | file |
| failOnResourceLoadingFailed | true | field | | failOnResourceLoadingFailed | true | field |
Then the response status code should be 409 Then the response status code should be 409
Then the response body should contain string:
"""
resource Stylesheet: net::ERR_CONNECTION_RESET
"""
Scenario: POST /forms/chromium/convert/html (Fail On Resource Loading Failed)
# A configured allow-list routes loopback through the pinning proxy, which
# reports a refused connection as a reset. See the scenario below.
Given I have a Gotenberg container with the following environment variable(s):
| CHROMIUM_ALLOW_LIST | |
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
| files | testdata/feature-rich-html/index.html | file |
| failOnResourceLoadingFailed | true | field |
Then the response status code should be 409
Then the response header "Content-Type" should be "text/plain; charset=UTF-8" Then the response header "Content-Type" should be "text/plain; charset=UTF-8"
Then the response body should contain string: Then the response body should contain string:
""" """

View File

@@ -400,6 +400,7 @@ Feature: /forms/chromium/convert/markdown
| files | testdata/feature-rich-markdown/index.html | file | | files | testdata/feature-rich-markdown/index.html | file |
| files | testdata/feature-rich-markdown/table.md | file | | files | testdata/feature-rich-markdown/table.md | file |
| failOnResourceHttpStatusCodes | [499,599] | field | | failOnResourceHttpStatusCodes | [499,599] | field |
| skipNetworkIdleEvent | false | field |
Then the response status code should be 409 Then the response status code should be 409
Then the response header "Content-Type" should be "text/plain; charset=UTF-8" Then the response header "Content-Type" should be "text/plain; charset=UTF-8"
Then the response body should match string: Then the response body should match string:
@@ -409,7 +410,11 @@ Feature: /forms/chromium/convert/markdown
""" """
Scenario: POST /forms/chromium/convert/markdown (Fail On Resource Loading Failed) Scenario: POST /forms/chromium/convert/markdown (Fail On Resource Loading Failed)
Given I have a default Gotenberg container # A configured allow-list routes loopback through the pinning proxy, which
# reports a refused connection as a reset. See the convert/html
# scenario.
Given I have a Gotenberg container with the following environment variable(s):
| CHROMIUM_ALLOW_LIST | |
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s):
| files | testdata/feature-rich-markdown/index.html | file | | files | testdata/feature-rich-markdown/index.html | file |
| files | testdata/feature-rich-markdown/table.md | file | | files | testdata/feature-rich-markdown/table.md | file |

View File

@@ -22,10 +22,9 @@ Feature: /forms/chromium/convert/url
Page 1 Page 1
""" """
# localStorage is per-origin and shared by the long-lived browser, so without # Each conversion runs in its own browser context, so localStorage never
# clearing it accumulates across same-origin conversions. # carries over.
# See https://github.com/gotenberg/gotenberg/issues/919. Scenario: POST /forms/chromium/convert/url (localStorage isolated per conversion)
Scenario: POST /forms/chromium/convert/url (localStorage leaks without clearing)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a static server Given I have a static server
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
@@ -37,14 +36,34 @@ Feature: /forms/chromium/convert/url
| url | http://host.docker.internal:%d/html/testdata/local-storage-html/index.html | field | | url | http://host.docker.internal:%d/html/testdata/local-storage-html/index.html | field |
| Gotenberg-Output-Filename | foo | header | | Gotenberg-Output-Filename | foo | header |
Then the response status code should be 200 Then the response status code should be 200
Then the "foo.pdf" PDF should have content matching "localStorageCount=1" at page 1
# With --chromium-shared-browser-context, localStorage is per-origin and
# shared by the long-lived browser, so without clearing it accumulates
# across same-origin conversions.
# See https://github.com/gotenberg/gotenberg/issues/919.
Scenario: POST /forms/chromium/convert/url (localStorage leaks in a shared browser context)
Given I have a Gotenberg container with the following environment variable(s):
| CHROMIUM_SHARED_BROWSER_CONTEXT | true |
Given I have a static server
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
| url | http://host.docker.internal:%d/html/testdata/local-storage-html/index.html | field |
| Gotenberg-Output-Filename | foo | header |
Then the response status code should be 200
Then the "foo.pdf" PDF should have content matching "localStorageCount=1" at page 1
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
| url | http://host.docker.internal:%d/html/testdata/local-storage-html/index.html | field |
| Gotenberg-Output-Filename | foo | header |
Then the response status code should be 200
Then the "foo.pdf" PDF should have content matching "localStorageCount=2" at page 1 Then the "foo.pdf" PDF should have content matching "localStorageCount=2" at page 1
# With --chromium-clear-storage every conversion clears the origin's # With --chromium-clear-storage every conversion in a shared browser context
# localStorage first, so the counter never carries over. # clears the origin's localStorage first, so the counter never carries over.
# See https://github.com/gotenberg/gotenberg/issues/919. # See https://github.com/gotenberg/gotenberg/issues/919.
Scenario: POST /forms/chromium/convert/url (Clear Storage) Scenario: POST /forms/chromium/convert/url (Clear Storage)
Given I have a Gotenberg container with the following environment variable(s): Given I have a Gotenberg container with the following environment variable(s):
| CHROMIUM_CLEAR_STORAGE | true | | CHROMIUM_SHARED_BROWSER_CONTEXT | true |
| CHROMIUM_CLEAR_STORAGE | true |
Given I have a static server Given I have a static server
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
| url | http://host.docker.internal:%d/html/testdata/local-storage-html/index.html | field | | url | http://host.docker.internal:%d/html/testdata/local-storage-html/index.html | field |
@@ -210,6 +229,36 @@ Feature: /forms/chromium/convert/url
Then the server request cookie "cookie_1" should be "foo" Then the server request cookie "cookie_1" should be "foo"
Then the server request cookie "cookie_2" should be "" Then the server request cookie "cookie_2" should be ""
# A cookie set by one conversion must not reach the next one.
Scenario: POST /forms/chromium/convert/url (Cookies isolated per conversion)
Given I have a default Gotenberg container
Given I have a static server
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
| url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field |
| cookies | [{"name":"cookie_1","value":"foo","domain":"host.docker.internal:%d"}] | field |
Then the response status code should be 200
Then the server request cookie "cookie_1" should be "foo"
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
| url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field |
Then the response status code should be 200
Then the server request cookie "cookie_1" should be ""
# With --chromium-shared-browser-context, cookies carry over between
# conversions.
Scenario: POST /forms/chromium/convert/url (Cookies in a shared browser context)
Given I have a Gotenberg container with the following environment variable(s):
| CHROMIUM_SHARED_BROWSER_CONTEXT | true |
Given I have a static server
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
| url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field |
| cookies | [{"name":"cookie_1","value":"foo","domain":"host.docker.internal:%d"}] | field |
Then the response status code should be 200
Then the server request cookie "cookie_1" should be "foo"
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
| url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field |
Then the response status code should be 200
Then the server request cookie "cookie_1" should be "foo"
# See https://github.com/gotenberg/gotenberg/issues/1130. # See https://github.com/gotenberg/gotenberg/issues/1130.
Scenario: POST /forms/chromium/convert/url (case-insensitive sameSite) Scenario: POST /forms/chromium/convert/url (case-insensitive sameSite)
Given I have a default Gotenberg container Given I have a default Gotenberg container
@@ -649,6 +698,7 @@ Feature: /forms/chromium/convert/url
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
| url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field | | url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field |
| failOnResourceHttpStatusCodes | [499,599] | field | | failOnResourceHttpStatusCodes | [499,599] | field |
| skipNetworkIdleEvent | false | field |
Then the response status code should be 409 Then the response status code should be 409
Then the response header "Content-Type" should be "text/plain; charset=UTF-8" Then the response header "Content-Type" should be "text/plain; charset=UTF-8"
Then the response body should contain string: Then the response body should contain string:
@@ -661,7 +711,11 @@ Feature: /forms/chromium/convert/url
""" """
Scenario: POST /forms/chromium/convert/url (Fail On Resource Loading Failed) Scenario: POST /forms/chromium/convert/url (Fail On Resource Loading Failed)
Given I have a default Gotenberg container # A configured allow-list routes loopback through the pinning proxy, which
# reports a refused connection as a reset. See the convert/html
# scenario.
Given I have a Gotenberg container with the following environment variable(s):
| CHROMIUM_ALLOW_LIST | |
Given I have a static server Given I have a static server
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
| url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field | | url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field |

View File

@@ -0,0 +1,57 @@
@chromium
@chromium-convert-html
@chromium-image-deduplication
Feature: Chromium header and footer image deduplication
# See: https://github.com/gotenberg/gotenberg/issues/1077.
# See: https://github.com/gotenberg/gotenberg/issues/1669.
# The PNG must appear on every page, but its image XObject must be shared.
Scenario: POST /forms/chromium/convert/html (Header image reused across pages)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
| files | testdata/page-1-html/index.html | file |
| files | testdata/header-footer-image/header.html | file |
| marginTop | 1 | field |
| marginBottom | 1 | field |
| Gotenberg-Output-Filename | header | header |
Then the response status code should be 200
Then the response header "Content-Type" should be "application/pdf"
Then there should be 1 PDF(s) in the response
Then the "header.pdf" PDF should have 1 page(s)
Then the "header.pdf" PDF should reuse 1 image XObject(s) across 1 page(s)
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
| files | testdata/pages-3-html/index.html | file |
| files | testdata/header-footer-image/header.html | file |
| marginTop | 1 | field |
| marginBottom | 1 | field |
| Gotenberg-Output-Filename | header | header |
Then the response status code should be 200
Then the response header "Content-Type" should be "application/pdf"
Then there should be 1 PDF(s) in the response
Then the "header.pdf" PDF should have 3 page(s)
Then the "header.pdf" PDF should reuse 1 image XObject(s) across 3 page(s)
Scenario: POST /forms/chromium/convert/html (Footer image reused across pages)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
| files | testdata/page-1-html/index.html | file |
| files | testdata/header-footer-image/footer.html | file |
| marginTop | 1 | field |
| marginBottom | 1 | field |
| Gotenberg-Output-Filename | footer | header |
Then the response status code should be 200
Then the response header "Content-Type" should be "application/pdf"
Then there should be 1 PDF(s) in the response
Then the "footer.pdf" PDF should have 1 page(s)
Then the "footer.pdf" PDF should reuse 1 image XObject(s) across 1 page(s)
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
| files | testdata/pages-3-html/index.html | file |
| files | testdata/header-footer-image/footer.html | file |
| marginTop | 1 | field |
| marginBottom | 1 | field |
| Gotenberg-Output-Filename | footer | header |
Then the response status code should be 200
Then the response header "Content-Type" should be "application/pdf"
Then there should be 1 PDF(s) in the response
Then the "footer.pdf" PDF should have 3 page(s)
Then the "footer.pdf" PDF should reuse 1 image XObject(s) across 3 page(s)

View File

@@ -98,6 +98,7 @@ Feature: /debug
"chromium-max-queue-size": "0", "chromium-max-queue-size": "0",
"chromium-proxy-server": "", "chromium-proxy-server": "",
"chromium-restart-after": "100", "chromium-restart-after": "100",
"chromium-shared-browser-context": "false",
"chromium-start-timeout": "20s", "chromium-start-timeout": "20s",
"gotenberg-build-debug-data": "true", "gotenberg-build-debug-data": "true",
"gotenberg-graceful-shutdown-duration": "30s", "gotenberg-graceful-shutdown-duration": "30s",
@@ -237,6 +238,7 @@ Feature: /debug
"chromium-max-concurrency": "6", "chromium-max-concurrency": "6",
"chromium-proxy-server": "", "chromium-proxy-server": "",
"chromium-restart-after": "100", "chromium-restart-after": "100",
"chromium-shared-browser-context": "false",
"chromium-start-timeout": "20s", "chromium-start-timeout": "20s",
"gotenberg-build-debug-data": "true", "gotenberg-build-debug-data": "true",
"gotenberg-graceful-shutdown-duration": "30s", "gotenberg-graceful-shutdown-duration": "30s",

View File

@@ -142,6 +142,24 @@ Feature: /forms/libreoffice/convert
| foo.pdf | | foo.pdf |
Then the "foo.pdf" PDF should be set to landscape orientation Then the "foo.pdf" PDF should be set to landscape orientation
Scenario: POST /forms/libreoffice/convert (Tagged PDF)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s):
| files | testdata/page_1.docx | file |
| Gotenberg-Output-Filename | foo | header |
Then there should be 1 PDF(s) in the response
Then there should be the following file(s) in the response:
| foo.pdf |
Then the "foo.pdf" PDF should NOT be tagged
When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s):
| files | testdata/page_1.docx | file |
| generateTaggedPdf | true | field |
| Gotenberg-Output-Filename | foo | header |
Then there should be 1 PDF(s) in the response
Then there should be the following file(s) in the response:
| foo.pdf |
Then the "foo.pdf" PDF should be tagged
Scenario: POST /forms/libreoffice/convert (Native Page Ranges - Single Document) Scenario: POST /forms/libreoffice/convert (Native Page Ranges - Single Document)
Given I have a default Gotenberg container Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s):
@@ -215,6 +233,7 @@ Feature: /forms/libreoffice/convert
| skipEmptyPages | foo | field | | skipEmptyPages | foo | field |
| addOriginalDocumentAsStream | foo | field | | addOriginalDocumentAsStream | foo | field |
| singlePageSheets | foo | field | | singlePageSheets | foo | field |
| generateTaggedPdf | foo | field |
| initialView | 5 | field | | initialView | 5 | field |
| initialPage | -1 | field | | initialPage | -1 | field |
| magnification | 9 | field | | magnification | 9 | field |
@@ -255,6 +274,7 @@ Feature: /forms/libreoffice/convert
form field 'skipEmptyPages' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax) form field 'skipEmptyPages' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax)
form field 'addOriginalDocumentAsStream' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax) form field 'addOriginalDocumentAsStream' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax)
form field 'singlePageSheets' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax) form field 'singlePageSheets' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax)
form field 'generateTaggedPdf' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax)
form field 'initialView' is invalid (got '5', resulting to value is not 0, 1 or 2) form field 'initialView' is invalid (got '5', resulting to value is not 0, 1 or 2)
form field 'initialPage' is invalid (got '-1', resulting to value is inferior to 1) form field 'initialPage' is invalid (got '-1', resulting to value is inferior to 1)
form field 'magnification' is invalid (got '9', resulting to value is not 0, 1, 2, 3 or 4) form field 'magnification' is invalid (got '9', resulting to value is not 0, 1, 2, 3 or 4)

View File

@@ -0,0 +1,72 @@
package scenario
import (
"context"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
)
func (s *scenario) thePdfShouldReuseImageXObjects(ctx context.Context, name string, images, pages int) error {
path := filepath.Join(s.workdir, s.resp.Header().Get("Gotenberg-Trace"), name)
if _, err := os.Stat(path); err != nil {
return fmt.Errorf("stat PDF %q: %w", path, err)
}
cmd := []string{"pdfimages", "-list", filepath.Base(path)}
output, err := execCommandInIntegrationToolsContainer(ctx, cmd, path)
if err != nil {
return fmt.Errorf("exec %q: %w", cmd, err)
}
return checkImageXObjectReuse(output, images, pages)
}
func checkImageXObjectReuse(output string, images, pages int) error {
lines := strings.Split(strings.TrimSpace(output), "\n")
if len(lines) < 2 || !strings.HasPrefix(strings.TrimSpace(lines[0]), "page") {
return fmt.Errorf("invalid pdfimages -list output: %q", output)
}
objects := make(map[[2]int]struct{})
imagesPerPage := make(map[int]int)
for _, line := range lines[2:] {
fields := strings.Fields(line)
if len(fields) < 12 {
return fmt.Errorf("invalid pdfimages -list row: %q", line)
}
page, err := strconv.Atoi(fields[0])
if err != nil {
return fmt.Errorf("parse image page %q: %w", fields[0], err)
}
if page < 1 || page > pages {
return fmt.Errorf("image on page %d outside expected range 1-%d", page, pages)
}
// pdfimages lists appearances, including repeated references to the
// same image. Columns 11 and 12 identify the underlying PDF object.
var ref [2]int
for i := range ref {
ref[i], err = strconv.Atoi(fields[10+i])
if err != nil {
return fmt.Errorf("parse image object ID in %q: %w", line, err)
}
}
objects[ref] = struct{}{}
imagesPerPage[page]++
}
for page := 1; page <= pages; page++ {
if imagesPerPage[page] != images {
return fmt.Errorf("expected %d image appearance(s) on page %d, but actual is %d", images, page, imagesPerPage[page])
}
}
if len(objects) != images {
return fmt.Errorf("expected %d unique image XObject(s) shared across %d page(s), but actual is %d", images, pages, len(objects))
}
return nil
}

View File

@@ -0,0 +1,106 @@
package scenario
import (
"fmt"
"strings"
"testing"
)
func TestCheckImageXObjectReuse(t *testing.T) {
t.Parallel()
const header = `page num type width height color comp bpc enc interp object ID x-ppi y-ppi size ratio
--------------------------------------------------------------------------------------------
`
imageRow := func(page, object, generation int) string {
return fmt.Sprintf("%d %d image 32 32 rgb 3 8 image no %d %d 96 96 3104B 101%%\n", page, page-1, object, generation)
}
tests := []struct {
name string
output string
pages int
wantErr string
}{
{
name: "one page baseline",
output: header + imageRow(1, 7, 0),
pages: 1,
},
{
name: "three pages share one image object",
output: header + imageRow(1, 7, 0) + imageRow(2, 7, 0) + imageRow(3, 7, 0),
pages: 3,
},
{
name: "regression embeds a different object per page",
output: header + imageRow(1, 7, 0) + imageRow(2, 8, 0) + imageRow(3, 9, 0),
pages: 3,
wantErr: "expected 1 unique image XObject(s) shared across 3 page(s), but actual is 3",
},
{
name: "object generation is part of its identity",
output: header + imageRow(1, 7, 0) + imageRow(2, 7, 1) + imageRow(3, 7, 0),
pages: 3,
wantErr: "expected 1 unique image XObject(s) shared across 3 page(s), but actual is 2",
},
{
name: "image missing from a page",
output: header + imageRow(1, 7, 0) + imageRow(3, 7, 0),
pages: 3,
wantErr: "expected 1 image appearance(s) on page 2, but actual is 0",
},
{
name: "no image cannot pass",
output: header,
pages: 1,
wantErr: "expected 1 image appearance(s) on page 1, but actual is 0",
},
{
name: "repeated image only on first page cannot pass",
output: header + imageRow(1, 7, 0) + imageRow(1, 7, 0) + imageRow(1, 7, 0),
pages: 3,
wantErr: "expected 1 image appearance(s) on page 1, but actual is 3",
},
{
name: "image on unexpected page",
output: header + imageRow(1, 7, 0) + imageRow(2, 7, 0),
pages: 1,
wantErr: "image on page 2 outside expected range 1-1",
},
{
name: "tool error cannot pass",
output: "Syntax Error: Couldn't find trailer dictionary",
pages: 1,
wantErr: "invalid pdfimages -list output:",
},
{
name: "malformed image row",
output: header + "1 0 image\n",
pages: 1,
wantErr: "invalid pdfimages -list row:",
},
{
name: "inline image has no reusable object ID",
output: header + "1 0 image 32 32 rgb 3 8 image no [inline] - 96 96 3104B 101%\n",
pages: 1,
wantErr: "parse image object ID",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := checkImageXObjectReuse(tt.output, 1, tt.pages)
if tt.wantErr == "" {
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
return
}
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("expected error containing %q, got %v", tt.wantErr, err)
}
})
}
}

View File

@@ -1795,10 +1795,12 @@ func InitializeScenario(ctx *godog.ScenarioContext) {
ctx.Then(`^the (response|webhook request) PDF\(s\) should declare Factur-X XMP with conformance level "([^"]*)"$`, s.thePdfsShouldDeclareFacturXConformanceLevel) ctx.Then(`^the (response|webhook request) PDF\(s\) should declare Factur-X XMP with conformance level "([^"]*)"$`, s.thePdfsShouldDeclareFacturXConformanceLevel)
ctx.Then(`^the "([^"]*)" PDF should have (\d+) page\(s\)$`, s.thePdfShouldHavePages) ctx.Then(`^the "([^"]*)" PDF should have (\d+) page\(s\)$`, s.thePdfShouldHavePages)
ctx.Then(`^the "([^"]*)" PDF (should|should NOT) be set to landscape orientation$`, s.thePdfShouldBeSetToLandscapeOrientation) ctx.Then(`^the "([^"]*)" PDF (should|should NOT) be set to landscape orientation$`, s.thePdfShouldBeSetToLandscapeOrientation)
ctx.Then(`^the "([^"]*)" PDF (should|should NOT) be tagged$`, s.thePdfShouldBeTagged)
ctx.Then(`^the "([^"]*)" PDF (should|should NOT) have a document outline$`, s.thePdfShouldHaveADocumentOutline) ctx.Then(`^the "([^"]*)" PDF (should|should NOT) have a document outline$`, s.thePdfShouldHaveADocumentOutline)
ctx.Then(`^the "([^"]*)" PDF (should|should NOT) have the following content at page (\d+):$`, s.thePdfShouldHaveTheFollowingContentAtPage) ctx.Then(`^the "([^"]*)" PDF (should|should NOT) have the following content at page (\d+):$`, s.thePdfShouldHaveTheFollowingContentAtPage)
ctx.Then(`^the "([^"]*)" PDF (should|should NOT) have content matching "([^"]*)" at page (\d+)$`, s.thePdfShouldHaveContentMatchingAtPage) ctx.Then(`^the "([^"]*)" PDF (should|should NOT) have content matching "([^"]*)" at page (\d+)$`, s.thePdfShouldHaveContentMatchingAtPage)
ctx.Then(`^the "([^"]*)" PDF should have (\d+) image\(s\)$`, s.thePdfShouldHaveImages) ctx.Then(`^the "([^"]*)" PDF should have (\d+) image\(s\)$`, s.thePdfShouldHaveImages)
ctx.Then(`^the "([^"]*)" PDF should reuse (\d+) image XObject\(s\) across (\d+) page\(s\)$`, s.thePdfShouldReuseImageXObjects)
ctx.Then(`^the "([^"]*)" image should be (\d+)x(\d+) pixels$`, s.theImageShouldBePixels) ctx.Then(`^the "([^"]*)" image should be (\d+)x(\d+) pixels$`, s.theImageShouldBePixels)
ctx.Then(`^the "([^"]*)" image pixel at (\d+),(\d+) should be "([^"]*)"$`, s.theImagePixelShouldBe) ctx.Then(`^the "([^"]*)" image pixel at (\d+),(\d+) should be "([^"]*)"$`, s.theImagePixelShouldBe)
ctx.Then(`^the "([^"]*)" file size should be (less|greater) than (\d+) KB$`, s.theFileSizeShouldBe) ctx.Then(`^the "([^"]*)" file size should be (less|greater) than (\d+) KB$`, s.theFileSizeShouldBe)

View File

@@ -0,0 +1,44 @@
package scenario
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
)
func (s *scenario) thePdfShouldBeTagged(ctx context.Context, name, kind string) error {
path := filepath.Join(s.workdir, s.resp.Header().Get("Gotenberg-Trace"), name)
if _, err := os.Stat(path); err != nil {
return fmt.Errorf("stat PDF %q: %w", path, err)
}
cmd := []string{"pdfinfo", filepath.Base(path)}
output, err := execCommandInIntegrationToolsContainer(ctx, cmd, path)
if err != nil {
return fmt.Errorf("exec %q: %w", cmd, err)
}
return checkTagged(output, kind != "should NOT")
}
// checkTagged reads the "Tagged" line of pdfinfo, which reflects the Marked
// entry of the document catalog's MarkInfo dictionary.
func checkTagged(output string, want bool) error {
for line := range strings.SplitSeq(output, "\n") {
value, found := strings.CutPrefix(strings.TrimSpace(line), "Tagged:")
if !found {
continue
}
tagged := strings.TrimSpace(value) == "yes"
if tagged != want {
return fmt.Errorf("expected tagged PDF to be %t, but actual is %t", want, tagged)
}
return nil
}
return fmt.Errorf("invalid pdfinfo output, no Tagged line: %q", output)
}

View File

@@ -0,0 +1,67 @@
package scenario
import (
"strings"
"testing"
)
func TestCheckTagged(t *testing.T) {
t.Parallel()
pdfinfo := func(tagged string) string {
return "Producer: LibreOffice 26.8\nTagged: " + tagged + "\nPages: 1\n"
}
tests := []struct {
name string
output string
want bool
wantErr string
}{
{
name: "tagged PDF expected and found",
output: pdfinfo("yes"),
want: true,
},
{
name: "untagged PDF expected and found",
output: pdfinfo("no"),
want: false,
},
{
name: "tagged PDF expected but untagged",
output: pdfinfo("no"),
want: true,
wantErr: "expected tagged PDF to be true, but actual is false",
},
{
name: "untagged PDF expected but tagged",
output: pdfinfo("yes"),
want: false,
wantErr: "expected tagged PDF to be false, but actual is true",
},
{
name: "no Tagged line",
output: "Pages: 1\n",
want: true,
wantErr: "invalid pdfinfo output, no Tagged line:",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
err := checkTagged(tc.output, tc.want)
if tc.wantErr == "" {
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
return
}
if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
t.Fatalf("expected error containing %q, got: %v", tc.wantErr, err)
}
})
}
}

View File

@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
</head>
<body style="margin: 0; width: 100%">
<!-- An opaque RGB PNG avoids a separate alpha-mask image XObject. -->
<img
alt="Regression test logo"
width="32"
height="32"
src="data:image/png;base64,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"
/>
</body>
</html>

View File

@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
</head>
<body style="margin: 0; width: 100%">
<!-- An opaque RGB PNG avoids a separate alpha-mask image XObject. -->
<img
alt="Regression test logo"
width="32"
height="32"
src="data:image/png;base64,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"
/>
</body>
</html>