mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 08:32:16 +01:00
Compare commits
22 Commits
v8.34.0
...
5b21dbf1a9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b21dbf1a9 | ||
|
|
0a62359691 | ||
|
|
7418b5cab3 | ||
|
|
337a5cca64 | ||
|
|
387fce2cf6 | ||
|
|
c89991cc0d | ||
|
|
0c8d681c35 | ||
|
|
3d300c2a09 | ||
|
|
8aa5a4f0e7 | ||
|
|
a92a7fedba | ||
|
|
d0e3991d16 | ||
|
|
d67ef724f9 | ||
|
|
68a0a9e5c1 | ||
|
|
b5f77c3b73 | ||
|
|
be5dcf943d | ||
|
|
9ca1e302bd | ||
|
|
3617bc6587 | ||
|
|
874adf076c | ||
|
|
9d83255ecd | ||
|
|
3b43bfbf33 | ||
|
|
7614964109 | ||
|
|
7b054da4e7 |
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@@ -12,6 +12,12 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
ignore:
|
||||
# Held at v0.14.2: v0.15.x breaks the headless print-mode paint pipeline
|
||||
# (rAF / ResizeObserver / IntersectionObserver stop firing, blank charts).
|
||||
# See https://github.com/gotenberg/gotenberg/issues/1535.
|
||||
- dependency-name: "github.com/chromedp/chromedp"
|
||||
- dependency-name: "github.com/chromedp/cdproto"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
|
||||
12
.github/workflows/continuous-delivery.yml
vendored
12
.github/workflows/continuous-delivery.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
tags_aws_lambda_libreoffice: ${{ steps.build_push.outputs.tags_aws_lambda_libreoffice }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Build and push
|
||||
id: build_push
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
tags_aws_lambda_libreoffice: ${{ steps.build_push.outputs.tags_aws_lambda_libreoffice }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Build and push
|
||||
id: build_push
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
tags_aws_lambda_libreoffice: ${{ steps.build_push.outputs.tags_aws_lambda_libreoffice }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Build and push
|
||||
id: build_push
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
tags_aws_lambda_libreoffice: ${{ steps.build_push.outputs.tags_aws_lambda_libreoffice }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Build and push
|
||||
id: build_push
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
tags_aws_lambda_libreoffice: ${{ steps.build_push.outputs.tags_aws_lambda_libreoffice }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Build and push
|
||||
id: build_push
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Merge
|
||||
uses: ./.github/actions/merge
|
||||
|
||||
36
.github/workflows/continuous-integration.yml
vendored
36
.github/workflows/continuous-integration.yml
vendored
@@ -21,10 +21,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
@@ -38,10 +38,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
|
||||
@@ -59,10 +59,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Build, test and push
|
||||
id: build_test_push
|
||||
@@ -117,7 +117,7 @@ jobs:
|
||||
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Build, test and push
|
||||
id: build_test_push
|
||||
@@ -147,7 +147,7 @@ jobs:
|
||||
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Build, test and push
|
||||
id: build_test_push
|
||||
@@ -177,7 +177,7 @@ jobs:
|
||||
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Build, test and push
|
||||
id: build_test_push
|
||||
@@ -207,7 +207,7 @@ jobs:
|
||||
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Build, test and push
|
||||
id: build_test_push
|
||||
@@ -253,7 +253,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Merge
|
||||
uses: ./.github/actions/merge
|
||||
@@ -322,7 +322,7 @@ jobs:
|
||||
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Build, test and push
|
||||
id: build_test_push
|
||||
@@ -351,7 +351,7 @@ jobs:
|
||||
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Build, test and push
|
||||
id: build_test_push
|
||||
@@ -380,7 +380,7 @@ jobs:
|
||||
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Build, test and push
|
||||
id: build_test_push
|
||||
@@ -409,7 +409,7 @@ jobs:
|
||||
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Build, test and push
|
||||
id: build_test_push
|
||||
@@ -438,7 +438,7 @@ jobs:
|
||||
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Build, test and push
|
||||
id: build_test_push
|
||||
@@ -460,7 +460,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Merge
|
||||
uses: ./.github/actions/merge
|
||||
|
||||
2
.github/workflows/pull-request-cleanup.yml
vendored
2
.github/workflows/pull-request-cleanup.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Cleanup
|
||||
uses: ./.github/actions/clean
|
||||
|
||||
14
Makefile
14
Makefile
@@ -18,6 +18,8 @@ GOTENBERG_BUILD_DEBUG_DATA=true
|
||||
API_PORT=3000
|
||||
API_PORT_FROM_ENV=
|
||||
API_BIND_IP=
|
||||
API_TLS_CERT_FILE=
|
||||
API_TLS_KEY_FILE=
|
||||
API_START_TIMEOUT=30s
|
||||
API_TIMEOUT=30s
|
||||
API_BODY_LIMIT=
|
||||
@@ -28,6 +30,9 @@ GOTENBERG_API_BASIC_AUTH_USERNAME=
|
||||
GOTENBERG_API_BASIC_AUTH_PASSWORD=
|
||||
API_DOWNLOAD_FROM_ALLOW_LIST=
|
||||
API_DOWNLOAD_FROM_DENY_LIST=^https?://(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|169\.254\.|0\.0\.0\.0|127\.|localhost|\[::1\]|\[fd)
|
||||
API_DOWNLOAD_FROM_DENY_PRIVATE_IPS=false
|
||||
API_DOWNLOAD_FROM_DENY_PUBLIC_IPS=false
|
||||
API_DOWNLOAD_FROM_ENABLE_ENVIRONMENT_PROXY=false
|
||||
API_DOWNLOAD_FROM_MAX_RETRY=4
|
||||
API_DISABLE_DOWNLOAD_FROM=false
|
||||
API_DISABLE_HEALTH_CHECK_ROUTE_TELEMETRY=true
|
||||
@@ -47,8 +52,11 @@ CHROMIUM_DISABLE_WEB_SECURITY=false
|
||||
CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES=false
|
||||
CHROMIUM_HOST_RESOLVER_RULES=
|
||||
CHROMIUM_PROXY_SERVER=
|
||||
CHROMIUM_ENABLE_ENVIRONMENT_PROXY=false
|
||||
CHROMIUM_ALLOW_LIST=
|
||||
CHROMIUM_DENY_LIST=^file:(?!//\/tmp/).*
|
||||
CHROMIUM_DENY_PRIVATE_IPS=false
|
||||
CHROMIUM_DENY_PUBLIC_IPS=false
|
||||
CHROMIUM_CLEAR_CACHE=false
|
||||
CHROMIUM_CLEAR_COOKIES=false
|
||||
CHROMIUM_DISABLE_JAVASCRIPT=false
|
||||
@@ -60,6 +68,9 @@ LIBREOFFICE_AUTO_START=false
|
||||
LIBREOFFICE_START_TIMEOUT=20s
|
||||
LIBREOFFICE_ALLOW_LIST=
|
||||
LIBREOFFICE_DENY_LIST=
|
||||
LIBREOFFICE_DENY_PRIVATE_IPS=false
|
||||
LIBREOFFICE_DENY_PUBLIC_IPS=false
|
||||
LIBREOFFICE_ENABLE_ENVIRONMENT_PROXY=false
|
||||
LIBREOFFICE_DISABLE_ROUTES=false
|
||||
LOG_LEVEL=info
|
||||
LOG_FIELDS_PREFIX=
|
||||
@@ -97,6 +108,9 @@ OTEL_EXPORTER_OTLP_INSECURE=true
|
||||
WEBHOOK_ENABLE_SYNC_MODE=false
|
||||
WEBHOOK_ALLOW_LIST=
|
||||
WEBHOOK_DENY_LIST=^https?://(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|169\.254\.|0\.0\.0\.0|127\.|localhost|\[::1\]|\[fd)
|
||||
WEBHOOK_DENY_PRIVATE_IPS=false
|
||||
WEBHOOK_DENY_PUBLIC_IPS=false
|
||||
WEBHOOK_ENABLE_ENVIRONMENT_PROXY=false
|
||||
WEBHOOK_MAX_RETRY=4
|
||||
WEBHOOK_RETRY_MIN_WAIT=1s
|
||||
WEBHOOK_RETRY_MAX_WAIT=30s
|
||||
|
||||
14
compose.yaml
14
compose.yaml
@@ -21,6 +21,8 @@ services:
|
||||
- "--api-port=${API_PORT}"
|
||||
- "--api-port-from-env=${API_PORT_FROM_ENV}"
|
||||
- "--api-bind-ip=${API_BIND_IP}"
|
||||
- "--api-tls-cert-file=${API_TLS_CERT_FILE}"
|
||||
- "--api-tls-key-file=${API_TLS_KEY_FILE}"
|
||||
- "--api-start-timeout=${API_START_TIMEOUT}"
|
||||
- "--api-timeout=${API_TIMEOUT}"
|
||||
- "--api-body-limit=${API_BODY_LIMIT}"
|
||||
@@ -29,6 +31,9 @@ services:
|
||||
- "--api-enable-basic-auth=${API_ENABLE_BASIC_AUTH}"
|
||||
- "--api-download-from-allow-list=${API_DOWNLOAD_FROM_ALLOW_LIST}"
|
||||
- "--api-download-from-deny-list=${API_DOWNLOAD_FROM_DENY_LIST}"
|
||||
- "--api-download-from-deny-private-ips=${API_DOWNLOAD_FROM_DENY_PRIVATE_IPS}"
|
||||
- "--api-download-from-deny-public-ips=${API_DOWNLOAD_FROM_DENY_PUBLIC_IPS}"
|
||||
- "--api-download-from-enable-environment-proxy=${API_DOWNLOAD_FROM_ENABLE_ENVIRONMENT_PROXY}"
|
||||
- "--api-download-from-max-retry=${API_DOWNLOAD_FROM_MAX_RETRY}"
|
||||
- "--api-disable-download-from=${API_DISABLE_DOWNLOAD_FROM}"
|
||||
- "--api-disable-health-check-route-telemetry=${API_DISABLE_HEALTH_CHECK_ROUTE_TELEMETRY}"
|
||||
@@ -48,8 +53,11 @@ services:
|
||||
- "--chromium-allow-file-access-from-files=${CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES}"
|
||||
- "--chromium-host-resolver-rules=${CHROMIUM_HOST_RESOLVER_RULES}"
|
||||
- "--chromium-proxy-server=${CHROMIUM_PROXY_SERVER}"
|
||||
- "--chromium-enable-environment-proxy=${CHROMIUM_ENABLE_ENVIRONMENT_PROXY}"
|
||||
- "--chromium-allow-list=${CHROMIUM_ALLOW_LIST}"
|
||||
- "--chromium-deny-list=${CHROMIUM_DENY_LIST}"
|
||||
- "--chromium-deny-private-ips=${CHROMIUM_DENY_PRIVATE_IPS}"
|
||||
- "--chromium-deny-public-ips=${CHROMIUM_DENY_PUBLIC_IPS}"
|
||||
- "--chromium-clear-cache=${CHROMIUM_CLEAR_CACHE}"
|
||||
- "--chromium-clear-cookies=${CHROMIUM_CLEAR_COOKIES}"
|
||||
- "--chromium-disable-javascript=${CHROMIUM_DISABLE_JAVASCRIPT}"
|
||||
@@ -61,6 +69,9 @@ services:
|
||||
- "--libreoffice-start-timeout=${LIBREOFFICE_START_TIMEOUT}"
|
||||
- "--libreoffice-allow-list=${LIBREOFFICE_ALLOW_LIST}"
|
||||
- "--libreoffice-deny-list=${LIBREOFFICE_DENY_LIST}"
|
||||
- "--libreoffice-deny-private-ips=${LIBREOFFICE_DENY_PRIVATE_IPS}"
|
||||
- "--libreoffice-deny-public-ips=${LIBREOFFICE_DENY_PUBLIC_IPS}"
|
||||
- "--libreoffice-enable-environment-proxy=${LIBREOFFICE_ENABLE_ENVIRONMENT_PROXY}"
|
||||
- "--libreoffice-disable-routes=${LIBREOFFICE_DISABLE_ROUTES}"
|
||||
- "--log-level=${LOG_LEVEL}"
|
||||
- "--log-fields-prefix=${LOG_FIELDS_PREFIX}"
|
||||
@@ -91,6 +102,9 @@ services:
|
||||
- "--webhook-enable-sync-mode=${WEBHOOK_ENABLE_SYNC_MODE}"
|
||||
- "--webhook-allow-list=${WEBHOOK_ALLOW_LIST}"
|
||||
- "--webhook-deny-list=${WEBHOOK_DENY_LIST}"
|
||||
- "--webhook-deny-private-ips=${WEBHOOK_DENY_PRIVATE_IPS}"
|
||||
- "--webhook-deny-public-ips=${WEBHOOK_DENY_PUBLIC_IPS}"
|
||||
- "--webhook-enable-environment-proxy=${WEBHOOK_ENABLE_ENVIRONMENT_PROXY}"
|
||||
- "--webhook-max-retry=${WEBHOOK_MAX_RETRY}"
|
||||
- "--webhook-retry-min-wait=${WEBHOOK_RETRY_MIN_WAIT}"
|
||||
- "--webhook-retry-max-wait=${WEBHOOK_RETRY_MAX_WAIT}"
|
||||
|
||||
55
go.mod
55
go.mod
@@ -4,24 +4,24 @@ go 1.26.2
|
||||
|
||||
require (
|
||||
github.com/alexliesenfeld/health v0.8.1
|
||||
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d
|
||||
github.com/chromedp/chromedp v0.14.2
|
||||
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d // pinned with chromedp v0.14.2, see below
|
||||
github.com/chromedp/chromedp v0.14.2 // pinned: v0.15.x breaks the headless print-mode paint pipeline (rAF / ResizeObserver / IntersectionObserver stop firing, blank charts). See https://github.com/gotenberg/gotenberg/issues/1535.
|
||||
github.com/cucumber/godog v0.15.1
|
||||
github.com/dlclark/regexp2 v1.12.0
|
||||
github.com/gomarkdown/markdown v0.0.0-20260417124207-7d523f7318df
|
||||
github.com/gomarkdown/markdown v0.0.0-20260614204949-e08cff860f76
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8
|
||||
github.com/labstack/echo/v4 v4.15.2
|
||||
github.com/labstack/echo/v4 v4.15.4
|
||||
github.com/labstack/gommon v0.5.0
|
||||
github.com/mholt/archives v0.1.5
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
github.com/moby/moby/api v1.54.2
|
||||
github.com/moby/moby/client v0.4.1
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/shirou/gopsutil/v4 v4.26.5
|
||||
github.com/moby/moby/api v1.55.0
|
||||
github.com/moby/moby/client v0.5.1
|
||||
github.com/prometheus/client_golang v1.24.1
|
||||
github.com/shirou/gopsutil/v4 v4.26.6
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/testcontainers/testcontainers-go v0.42.0
|
||||
github.com/testcontainers/testcontainers-go v0.43.0
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.19.0
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.69.0
|
||||
go.opentelemetry.io/otel v1.44.0
|
||||
@@ -31,10 +31,10 @@ require (
|
||||
go.opentelemetry.io/otel/sdk/log v0.20.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0
|
||||
go.opentelemetry.io/otel/trace v1.44.0
|
||||
golang.org/x/net v0.55.0
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/term v0.43.0
|
||||
golang.org/x/text v0.37.0
|
||||
golang.org/x/net v0.57.0
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/term v0.45.0
|
||||
golang.org/x/text v0.40.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -65,8 +65,8 @@ require (
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/ebitengine/purego v0.10.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20260520185125-572e7c383686 // indirect
|
||||
github.com/felixge/httpsnoop v1.1.0 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20260601182631-00ed12fed2a6 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
@@ -81,32 +81,32 @@ require (
|
||||
github.com/hashicorp/go-memdb v1.3.5 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/klauspost/compress v1.19.1 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
||||
github.com/minio/minlz v1.1.1 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/go-archive v0.2.0 // indirect
|
||||
github.com/moby/patternmatcher v0.6.1 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
github.com/moby/sys/sequential v0.7.0 // indirect
|
||||
github.com/moby/sys/user v0.4.0 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.2.3 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.2.5 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.26 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.27 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/common v0.70.1 // indirect
|
||||
github.com/prometheus/otlptranslator v1.0.0 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/prometheus/procfs v0.21.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
@@ -132,14 +132,13 @@ require (
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
|
||||
golang.org/x/crypto v0.52.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/crypto v0.54.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
google.golang.org/grpc v1.81.1 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260615183401-62b3387ff324 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260615183401-62b3387ff324 // indirect
|
||||
google.golang.org/grpc v1.82.1 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
100
go.sum
100
go.sum
@@ -72,10 +72,10 @@ github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/
|
||||
github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-json-experiment/json v0.0.0-20260520185125-572e7c383686 h1:NZBJxCpbHS1gzS6xAmyxbJznosZIIPk9IB42v62UvKA=
|
||||
github.com/go-json-experiment/json v0.0.0-20260520185125-572e7c383686/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
|
||||
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
|
||||
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
|
||||
github.com/go-json-experiment/json v0.0.0-20260601182631-00ed12fed2a6 h1:nxP4pPoyqOAgX8lYDFCfl3DyKeXErCvSvhcyzwGV9CE=
|
||||
github.com/go-json-experiment/json v0.0.0-20260601182631-00ed12fed2a6/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@@ -96,8 +96,8 @@ github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1
|
||||
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/gomarkdown/markdown v0.0.0-20260417124207-7d523f7318df h1:Mwihr/o+v4L5h56rwHLOE20+hh7Okhwno5BHz3zDuao=
|
||||
github.com/gomarkdown/markdown v0.0.0-20260417124207-7d523f7318df/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/gomarkdown/markdown v0.0.0-20260614204949-e08cff860f76 h1:Ltt9ldIaSYEsjA7sPY2c8r9dOmnKM1vlzhh3dxlhBHM=
|
||||
github.com/gomarkdown/markdown v0.0.0-20260614204949-e08cff860f76/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
@@ -130,8 +130,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
|
||||
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
@@ -144,8 +144,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
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/labstack/echo/v4 v4.15.2 h1:nnh2sCzGCVYnU+wCisMPiYapEg/QVo/gcI9ePKg5/T4=
|
||||
github.com/labstack/echo/v4 v4.15.2/go.mod h1:Xzp1Ns1RA2c9fY7nSgUJkpkUZGNbEIVHZbtbOMPktBI=
|
||||
github.com/labstack/echo/v4 v4.15.4 h1:DL45vVYa+BWE+XuW+zZNd9H0YEdZ80UAWJGcTVW4EVs=
|
||||
github.com/labstack/echo/v4 v4.15.4/go.mod h1:CuMetKIRwsuO/qlAgMq+KTAalwGoB/h4tC+yPdrTj1g=
|
||||
github.com/labstack/gommon v0.5.0 h1:6VSQ2NOzsnEJ5W6+84E0RbcaDDmgB6NIAzWCczTEe6c=
|
||||
github.com/labstack/gommon v0.5.0/go.mod h1:Rzlg7HHy1maLfzBYGg9NZcVuz1sA68HHhLjhcEllYE0=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||
@@ -154,8 +154,8 @@ github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRt
|
||||
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
||||
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=
|
||||
@@ -170,14 +170,14 @@ 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/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
|
||||
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
|
||||
github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
|
||||
github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||
github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY=
|
||||
github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ=
|
||||
github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
|
||||
github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||
github.com/moby/moby/client v0.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw=
|
||||
github.com/moby/moby/client v0.5.1/go.mod h1:odLstlZ6uSnfvAgVxMpvgmb8SUdd+siH2T0GBuxVAlM=
|
||||
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
|
||||
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
|
||||
github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o=
|
||||
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||
@@ -186,35 +186,35 @@ github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nwaples/rardecode/v2 v2.2.3 h1:qaVuy3ChZDbAQZshPLjHeNJKF3Cru8uo9jmgveKIy2A=
|
||||
github.com/nwaples/rardecode/v2 v2.2.3/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
|
||||
github.com/nwaples/rardecode/v2 v2.2.5 h1:L5doqgGfQwI7qADJMqnkrSB86rpPsqQDrHeO0HWa5JY=
|
||||
github.com/nwaples/rardecode/v2 v2.2.5/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
|
||||
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk=
|
||||
github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
|
||||
github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY=
|
||||
github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc=
|
||||
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/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
|
||||
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shirou/gopsutil/v4 v4.26.5 h1:RPcBXkpz7kOj9PqGFQOlBPZHsyaPvPVQc098y9RmCNM=
|
||||
github.com/shirou/gopsutil/v4 v4.26.5/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||
github.com/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs=
|
||||
github.com/shirou/gopsutil/v4 v4.26.6/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
|
||||
@@ -239,8 +239,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY=
|
||||
github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30=
|
||||
github.com/testcontainers/testcontainers-go v0.43.0 h1:oEQx5MW2DGd9z3AeEQfB2lPM0eLs7ztyaGRu75bFo5A=
|
||||
github.com/testcontainers/testcontainers-go v0.43.0/go.mod h1:+VxkT2NQnKOZPKi6praMuMKYHYyOGXr0XSBSlSMCzFo=
|
||||
github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
|
||||
github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
|
||||
github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4=
|
||||
@@ -314,34 +314,34 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0=
|
||||
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
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-20210616094352-59db8d763f22/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.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
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=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260615183401-62b3387ff324 h1:g0RAkxK/smSu/iRwC/KIX1mwUoVJtk2OjbgaeS4DmUM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260615183401-62b3387ff324/go.mod h1:Z4WJ5pJOYWFWcHEQUelD5QaZDknIQkpIL/+fyJOT9+A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260615183401-62b3387ff324 h1:9HZDLIdYBJXAnaFOr9WHrKVycfpY+75s9HGadC0305A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260615183401-62b3387ff324/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
|
||||
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
107
package-lock.json
generated
107
package-lock.json
generated
@@ -5,9 +5,9 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"devDependencies": {
|
||||
"prettier": "3.8.3",
|
||||
"prettier": "3.9.6",
|
||||
"prettier-plugin-gherkin": "^3.1.3",
|
||||
"prettier-plugin-sh": "^0.18.0"
|
||||
"prettier-plugin-sh": "^0.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cucumber/gherkin": {
|
||||
@@ -34,15 +34,80 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@reteps/dockerfmt": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@reteps/dockerfmt/-/dockerfmt-0.5.2.tgz",
|
||||
"integrity": "sha512-Hbr7yen4fP5TxGM54ucXa4o5NwWXatJ6Bd9I8gp0PValYbI4Rug2Gu+rVv7K7o/efQc3F5ctqWJz47rYaa8zBw==",
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@reteps/dockerfmt/-/dockerfmt-0.5.4.tgz",
|
||||
"integrity": "sha512-HEGgXVVOb+JtGUSSzXl/XPKFIZjMDTUoHarCjaQdkY+cb5M9K/O3b5xm+x0IPIk3SfHurbc0bSgcFsQlzjitxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"dockerfmt": "dist/launcher.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^v12.20.0 || ^14.13.0 || >=16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@reteps/dockerfmt-darwin-arm64": "0.5.4",
|
||||
"@reteps/dockerfmt-darwin-x64": "0.5.4",
|
||||
"@reteps/dockerfmt-linux-arm64": "0.5.4",
|
||||
"@reteps/dockerfmt-linux-x64": "0.5.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@reteps/dockerfmt-darwin-arm64": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@reteps/dockerfmt-darwin-arm64/-/dockerfmt-darwin-arm64-0.5.4.tgz",
|
||||
"integrity": "sha512-urMqV+dQyvVI8/WrXwClX9e1PEyS35wFdwJjpZYmL09AkV4Io5U1oam8UBKK7jZk0+YsdF88ay6e86Kn6DIyQg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@reteps/dockerfmt-darwin-x64": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@reteps/dockerfmt-darwin-x64/-/dockerfmt-darwin-x64-0.5.4.tgz",
|
||||
"integrity": "sha512-fJORy6DFxbgDiMqxpLTPZlb5KUY0Vq0iR4NGnyKnuYZ9LdZUS508DK2kt/AJ87/jIKNV1qRG0JXG1Tc6xdvWjw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@reteps/dockerfmt-linux-arm64": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@reteps/dockerfmt-linux-arm64/-/dockerfmt-linux-arm64-0.5.4.tgz",
|
||||
"integrity": "sha512-6pVakO06eXtDuvxy1Dnjs/gQyUoGGycle8PRSt5IFRwLi/AVaOQwfkfmW0WP8VH9wNUeti6BfJ3ksTn2G+XMxg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@reteps/dockerfmt-linux-x64": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@reteps/dockerfmt-linux-x64/-/dockerfmt-linux-x64-0.5.4.tgz",
|
||||
"integrity": "sha512-OD6SIlUV1D4TgJoTui3FMBAZsGbTSPYsiT0BKhD6jMUcJb3GpFTa7dY9rL8rP9FUqfL7OTHVUGUOL4Rh64Olog==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||
@@ -58,9 +123,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.8.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
|
||||
"integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
|
||||
"version": "3.9.6",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz",
|
||||
"integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -86,14 +151,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-sh": {
|
||||
"version": "0.18.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-sh/-/prettier-plugin-sh-0.18.1.tgz",
|
||||
"integrity": "sha512-uZmU22wBMevjh3rmCatNQqiEer2+5KLa0xYCBX6zQQUQkcNzVL+s6FbPKK6ZSUNUbQk6jMAcQHrYPvuL2W6ihQ==",
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-sh/-/prettier-plugin-sh-0.19.0.tgz",
|
||||
"integrity": "sha512-39VXFZH/cOGtcuu8aeSvqp/hhwomOR4QroZUj+jBz2cNb3os9s0sqFZSNlYts6jdtLLDU7D2YT3Z1+abtb7adQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reteps/dockerfmt": "^0.5.1",
|
||||
"sh-syntax": "^0.5.8"
|
||||
"@reteps/dockerfmt": "^0.5.4",
|
||||
"sh-syntax": "^0.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
@@ -113,14 +178,11 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/sh-syntax": {
|
||||
"version": "0.5.8",
|
||||
"resolved": "https://registry.npmjs.org/sh-syntax/-/sh-syntax-0.5.8.tgz",
|
||||
"integrity": "sha512-JfVoxf4FxQI5qpsPbkHhZo+n6N9YMJobyl4oGEUBb/31oQYlgTjkXQD8PBiafS2UbWoxrTO0Z5PJUBXEPAG1Zw==",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/sh-syntax/-/sh-syntax-0.6.0.tgz",
|
||||
"integrity": "sha512-52VK6z/cdZHv7UURjIcwfBUQZrAhIEEe0bY4lrkfypjnFIKsDZdD3Uaz/dBiw/sF8BeX0Mssv140s8EnrsJ9dQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
@@ -128,13 +190,6 @@
|
||||
"url": "https://opencollective.com/sh-syntax"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"prettier": "3.8.3",
|
||||
"prettier": "3.9.6",
|
||||
"prettier-plugin-gherkin": "^3.1.3",
|
||||
"prettier-plugin-sh": "^0.18.0"
|
||||
"prettier-plugin-sh": "^0.19.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package gotenberg
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
@@ -12,6 +15,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/dlclark/regexp2"
|
||||
"golang.org/x/net/http/httpproxy"
|
||||
)
|
||||
|
||||
// ErrNonPublicIP indicates that an outbound URL targets an IP address that
|
||||
@@ -389,9 +393,37 @@ func (rt *outboundRoundTripper) RoundTrip(req *http.Request) (*http.Response, er
|
||||
//
|
||||
// The default posture is permissive; callers pass [WithDenyPrivateIPs]
|
||||
// or [WithDenyPublicIPs] to opt into IP-class rejection.
|
||||
func NewOutboundHttpClient(timeout time.Duration, allowList, denyList []*regexp2.Regexp, opts ...DecideOption) *http.Client {
|
||||
//
|
||||
// When enableEnvironmentProxy is true, the client routes through the proxy
|
||||
// defined by the standard HTTP_PROXY, HTTPS_PROXY, and NO_PROXY variables,
|
||||
// including any credentials embedded in those URLs. In that mode the proxy
|
||||
// owns DNS and egress, so destination dial pinning does not apply; the URL
|
||||
// allow/deny and IP-class validation still runs. Callers gate this behind
|
||||
// their module's opt-in flag. See
|
||||
// https://github.com/gotenberg/gotenberg/issues/1592.
|
||||
func NewOutboundHttpClient(timeout time.Duration, allowList, denyList []*regexp2.Regexp, enableEnvironmentProxy bool, opts ...DecideOption) *http.Client {
|
||||
base := http.DefaultTransport.(*http.Transport).Clone()
|
||||
base.DialContext = secureDialContext
|
||||
|
||||
if enableEnvironmentProxy {
|
||||
// Route through the operator's proxy (standard env vars, credentials
|
||||
// included). NO_PROXY hosts get a direct, unpinned dial.
|
||||
// httpproxy.FromEnvironment reads the environment now rather than
|
||||
// caching it process-wide like http.ProxyFromEnvironment.
|
||||
proxyFunc := httpproxy.FromEnvironment().ProxyFunc()
|
||||
base.Proxy = func(req *http.Request) (*url.URL, error) {
|
||||
return proxyFunc(req.URL)
|
||||
}
|
||||
base.DialContext = outboundDialer.DialContext
|
||||
} else {
|
||||
// Default: ignore any proxy environment variables and pin the dial to
|
||||
// the IPs resolved during validation, closing the DNS-rebinding
|
||||
// window. Clearing Proxy is deliberate: the cloned default transport
|
||||
// carries http.ProxyFromEnvironment, which combined with the pinned
|
||||
// dialer would connect to the destination IP on the proxy's port.
|
||||
base.Proxy = nil
|
||||
base.DialContext = secureDialContext
|
||||
}
|
||||
|
||||
return &http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: &outboundRoundTripper{
|
||||
@@ -452,3 +484,99 @@ func DialPinned(ctx context.Context, network string, addrs []netip.Addr, port st
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
// DialThroughProxy opens a TCP tunnel to target (a host:port) through the
|
||||
// HTTP CONNECT proxy at proxyURL, authenticating with any credentials
|
||||
// embedded in proxyURL. dialProxy dials the proxy's own address; callers pass
|
||||
// a plain dialer. Chromium and soffice cannot authenticate to a proxy
|
||||
// themselves, so Gotenberg performs the CONNECT handshake on their behalf.
|
||||
// The returned connection carries the raw tunnel for the caller to splice
|
||||
// with the client. See https://github.com/gotenberg/gotenberg/issues/1592.
|
||||
func DialThroughProxy(ctx context.Context, proxyURL *url.URL, target string, dialProxy func(ctx context.Context, network, addr string) (net.Conn, error)) (net.Conn, error) {
|
||||
conn, err := dialProxy(ctx, "tcp", proxyHostPort(proxyURL))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dial proxy: %w", err)
|
||||
}
|
||||
|
||||
if proxyURL.Scheme == "https" {
|
||||
tlsConn := tls.Client(conn, &tls.Config{ServerName: proxyURL.Hostname()})
|
||||
err = tlsConn.HandshakeContext(ctx)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, fmt.Errorf("TLS handshake with proxy: %w", err)
|
||||
}
|
||||
conn = tlsConn
|
||||
}
|
||||
|
||||
// Bound the CONNECT handshake by the request deadline; cleared once the
|
||||
// tunnel is established so splicing manages its own lifetime.
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
_ = conn.SetDeadline(deadline)
|
||||
}
|
||||
|
||||
connectReq := &http.Request{
|
||||
Method: http.MethodConnect,
|
||||
URL: &url.URL{Opaque: target},
|
||||
Host: target,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
if user := proxyURL.User; user != nil {
|
||||
password, _ := user.Password()
|
||||
connectReq.Header.Set("Proxy-Authorization", proxyAuthHeader(user.Username(), password))
|
||||
}
|
||||
|
||||
err = connectReq.Write(conn)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, fmt.Errorf("write CONNECT to proxy: %w", err)
|
||||
}
|
||||
|
||||
br := bufio.NewReader(conn)
|
||||
resp, err := http.ReadResponse(br, connectReq)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, fmt.Errorf("read CONNECT response from proxy: %w", err)
|
||||
}
|
||||
// A CONNECT response carries no body; discard defensively.
|
||||
_ = resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
_ = conn.Close()
|
||||
return nil, fmt.Errorf("proxy refused CONNECT to %q with status %d", target, resp.StatusCode)
|
||||
}
|
||||
|
||||
_ = conn.SetDeadline(time.Time{})
|
||||
|
||||
// The reader may hold bytes the proxy sent right after the response;
|
||||
// overlay it so those tunnel bytes are not lost when splicing.
|
||||
return &bufferedConn{Conn: conn, r: br}, nil
|
||||
}
|
||||
|
||||
// proxyHostPort returns proxyURL's host:port, defaulting the port from the
|
||||
// scheme when the URL omits it.
|
||||
func proxyHostPort(proxyURL *url.URL) string {
|
||||
port := proxyURL.Port()
|
||||
if port == "" {
|
||||
port = "80"
|
||||
if proxyURL.Scheme == "https" {
|
||||
port = "443"
|
||||
}
|
||||
}
|
||||
return net.JoinHostPort(proxyURL.Hostname(), port)
|
||||
}
|
||||
|
||||
// proxyAuthHeader builds a Basic Proxy-Authorization header value.
|
||||
func proxyAuthHeader(username, password string) string {
|
||||
return "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password))
|
||||
}
|
||||
|
||||
// bufferedConn overlays a [bufio.Reader] on a [net.Conn] so that bytes
|
||||
// buffered while reading a proxy's CONNECT response are not lost when the
|
||||
// tunnel is spliced.
|
||||
type bufferedConn struct {
|
||||
net.Conn
|
||||
r *bufio.Reader
|
||||
}
|
||||
|
||||
func (c *bufferedConn) Read(b []byte) (int, error) {
|
||||
return c.r.Read(b)
|
||||
}
|
||||
|
||||
153
pkg/gotenberg/outbound_proxy_test.go
Normal file
153
pkg/gotenberg/outbound_proxy_test.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package gotenberg
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// connectCapture records the CONNECT request a proxy stub received.
|
||||
type connectCapture struct {
|
||||
mu sync.Mutex
|
||||
method string
|
||||
host string
|
||||
auth string
|
||||
}
|
||||
|
||||
func (c *connectCapture) set(method, host, auth string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.method, c.host, c.auth = method, host, auth
|
||||
}
|
||||
|
||||
func (c *connectCapture) get() (string, string, string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.method, c.host, c.auth
|
||||
}
|
||||
|
||||
// startConnectProxyStub starts a raw TCP server that behaves like an HTTP
|
||||
// CONNECT proxy: it reads the CONNECT request, records it, replies 200 with a
|
||||
// greeting appended to the same write (to exercise buffered-byte handling),
|
||||
// then echoes tunnel bytes back to the caller.
|
||||
func startConnectProxyStub(t *testing.T, capture *connectCapture) string {
|
||||
t.Helper()
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = l.Close() })
|
||||
|
||||
go func() {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
br := bufio.NewReader(conn)
|
||||
req, err := http.ReadRequest(br)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
capture.set(req.Method, req.Host, req.Header.Get("Proxy-Authorization"))
|
||||
|
||||
// The greeting rides along with the response so the client's CONNECT
|
||||
// response parser buffers it; bufferedConn must not drop it.
|
||||
_, _ = conn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\nTUNNEL-HELLO"))
|
||||
_, _ = io.Copy(conn, br)
|
||||
}()
|
||||
|
||||
return l.Addr().String()
|
||||
}
|
||||
|
||||
func TestDialThroughProxy(t *testing.T) {
|
||||
capture := &connectCapture{}
|
||||
addr := startConnectProxyStub(t, capture)
|
||||
|
||||
proxyURL := &url.URL{Scheme: "http", Host: addr, User: url.UserPassword("alice", "s3cr3t")}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
conn, err := DialThroughProxy(ctx, proxyURL, "example.com:443", func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, network, addr)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("DialThroughProxy: %v", err)
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
// The greeting buffered while reading the CONNECT response must survive.
|
||||
greeting := make([]byte, len("TUNNEL-HELLO"))
|
||||
_, err = io.ReadFull(conn, greeting)
|
||||
if err != nil {
|
||||
t.Fatalf("read greeting: %v", err)
|
||||
}
|
||||
if string(greeting) != "TUNNEL-HELLO" {
|
||||
t.Fatalf("greeting = %q, want TUNNEL-HELLO", greeting)
|
||||
}
|
||||
|
||||
// The tunnel must round-trip bytes.
|
||||
_, err = conn.Write([]byte("ping"))
|
||||
if err != nil {
|
||||
t.Fatalf("write to tunnel: %v", err)
|
||||
}
|
||||
echo := make([]byte, 4)
|
||||
_, err = io.ReadFull(conn, echo)
|
||||
if err != nil {
|
||||
t.Fatalf("read echo: %v", err)
|
||||
}
|
||||
if string(echo) != "ping" {
|
||||
t.Fatalf("echo = %q, want ping", echo)
|
||||
}
|
||||
|
||||
method, host, auth := capture.get()
|
||||
if method != http.MethodConnect {
|
||||
t.Fatalf("proxy saw method %q, want CONNECT", method)
|
||||
}
|
||||
if host != "example.com:443" {
|
||||
t.Fatalf("proxy saw target %q, want example.com:443", host)
|
||||
}
|
||||
wantAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte("alice:s3cr3t"))
|
||||
if auth != wantAuth {
|
||||
t.Fatalf("proxy saw Proxy-Authorization %q, want %q", auth, wantAuth)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialThroughProxy_RefusedStatus(t *testing.T) {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = l.Close() })
|
||||
|
||||
go func() {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
br := bufio.NewReader(conn)
|
||||
_, _ = http.ReadRequest(br)
|
||||
_, _ = conn.Write([]byte("HTTP/1.1 407 Proxy Authentication Required\r\n\r\n"))
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err = DialThroughProxy(ctx, &url.URL{Scheme: "http", Host: l.Addr().String()}, "example.com:443", func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, network, addr)
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected an error when the proxy refuses CONNECT, got nil")
|
||||
}
|
||||
}
|
||||
@@ -57,12 +57,13 @@ type Api struct {
|
||||
}
|
||||
|
||||
type downloadFromConfig struct {
|
||||
allowList []*regexp2.Regexp
|
||||
denyList []*regexp2.Regexp
|
||||
denyPrivateIPs bool
|
||||
denyPublicIPs bool
|
||||
maxRetry int
|
||||
disable bool
|
||||
allowList []*regexp2.Regexp
|
||||
denyList []*regexp2.Regexp
|
||||
denyPrivateIPs bool
|
||||
denyPublicIPs bool
|
||||
enableEnvironmentProxy bool
|
||||
maxRetry int
|
||||
disable bool
|
||||
}
|
||||
|
||||
// Router is a module interface that adds routes to the [Api].
|
||||
@@ -201,6 +202,7 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
|
||||
fs.StringSlice("api-download-from-deny-list", []string{}, "Set the denied URLs for the download from feature using regular expressions - supports multiple values")
|
||||
fs.Bool("api-download-from-deny-private-ips", false, "Reject downloadFrom URLs whose host resolves to a non-public IP address (loopback, RFC1918, link-local, unique-local). Enable on deployments that accept untrusted downloadFrom sources to mitigate SSRF against internal services")
|
||||
fs.Bool("api-download-from-deny-public-ips", false, "Reject downloadFrom URLs whose host resolves to a public IP address. Enable on air-gapped or data-governed deployments to prevent downloads from reaching the public internet")
|
||||
fs.Bool("api-download-from-enable-environment-proxy", false, "Route downloadFrom fetches through the proxy defined by the standard HTTP_PROXY, HTTPS_PROXY, and NO_PROXY variables, including credentials")
|
||||
fs.Int("api-download-from-max-retry", 4, "Set the maximum number of retries for the download from feature")
|
||||
fs.Bool("api-disable-download-from", false, "Disable the download from feature")
|
||||
fs.Bool("api-disable-health-check-route-telemetry", true, "Disable telemetry for health check route")
|
||||
@@ -239,12 +241,13 @@ func (a *Api) Provision(ctx *gotenberg.Context) error {
|
||||
a.rootPath = flags.MustString("api-root-path")
|
||||
a.correlationIdHeader = flags.MustDeprecatedString("api-trace-header", "api-correlation-id-header")
|
||||
a.downloadFromCfg = downloadFromConfig{
|
||||
allowList: flags.MustRegexpSlice("api-download-from-allow-list"),
|
||||
denyList: flags.MustRegexpSlice("api-download-from-deny-list"),
|
||||
denyPrivateIPs: flags.MustBool("api-download-from-deny-private-ips"),
|
||||
denyPublicIPs: flags.MustBool("api-download-from-deny-public-ips"),
|
||||
maxRetry: flags.MustInt("api-download-from-max-retry"),
|
||||
disable: flags.MustBool("api-disable-download-from"),
|
||||
allowList: flags.MustRegexpSlice("api-download-from-allow-list"),
|
||||
denyList: flags.MustRegexpSlice("api-download-from-deny-list"),
|
||||
denyPrivateIPs: flags.MustBool("api-download-from-deny-private-ips"),
|
||||
denyPublicIPs: flags.MustBool("api-download-from-deny-public-ips"),
|
||||
enableEnvironmentProxy: flags.MustBool("api-download-from-enable-environment-proxy"),
|
||||
maxRetry: flags.MustInt("api-download-from-max-retry"),
|
||||
disable: flags.MustBool("api-disable-download-from"),
|
||||
}
|
||||
a.disableHealthCheckRouteTelemetry = flags.MustDeprecatedBool("api-disable-health-check-logging", "api-disable-health-check-route-telemetry")
|
||||
a.disableRootRouteTelemetry = flags.MustBool("api-disable-root-route-telemetry")
|
||||
|
||||
@@ -281,7 +281,7 @@ func newContext(echoCtx echo.Context, logger *slog.Logger, fs *gotenberg.FileSys
|
||||
}
|
||||
|
||||
client := &retryablehttp.Client{
|
||||
HTTPClient: gotenberg.NewOutboundHttpClient(time.Until(deadline), downloadFromCfg.allowList, downloadFromCfg.denyList, ipOpts...),
|
||||
HTTPClient: gotenberg.NewOutboundHttpClient(time.Until(deadline), downloadFromCfg.allowList, downloadFromCfg.denyList, downloadFromCfg.enableEnvironmentProxy, ipOpts...),
|
||||
RetryMax: downloadFromCfg.maxRetry,
|
||||
RetryWaitMin: time.Duration(1) * time.Second,
|
||||
RetryWaitMax: time.Until(deadline),
|
||||
|
||||
@@ -38,6 +38,7 @@ type browserArguments struct {
|
||||
allowFileAccessFromFiles bool
|
||||
hostResolverRules string
|
||||
proxyServer string
|
||||
enableEnvironmentProxy bool
|
||||
wsUrlReadTimeout time.Duration
|
||||
hyphenDataDirPath string
|
||||
|
||||
@@ -58,6 +59,14 @@ type chromiumBrowser struct {
|
||||
userProfileDirPath string
|
||||
ctxMu sync.RWMutex
|
||||
isStarted atomic.Bool
|
||||
// startMu serializes Start calls. The supervisor's runWithDeadline
|
||||
// abandons a Start goroutine when the request deadline expires while
|
||||
// Chromium's startup handshake is still hanging; the abandoned goroutine
|
||||
// keeps running, holding the resources it acquired (the pinning proxy).
|
||||
// Serializing here prevents a second, overlapping Start from colliding
|
||||
// with the in-flight one on the shared pinning proxy.
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1599.
|
||||
startMu sync.Mutex
|
||||
|
||||
arguments browserArguments
|
||||
fs *gotenberg.FileSystem
|
||||
@@ -69,7 +78,7 @@ func newChromiumBrowser(arguments browserArguments) browser {
|
||||
initialCtx: context.Background(),
|
||||
arguments: arguments,
|
||||
fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)),
|
||||
pinningProxy: newPinningProxy(arguments.allowList, arguments.denyList, arguments.denyPrivateIPs, arguments.denyPublicIPs),
|
||||
pinningProxy: newPinningProxy(arguments.allowList, arguments.denyList, arguments.denyPrivateIPs, arguments.denyPublicIPs, arguments.enableEnvironmentProxy),
|
||||
}
|
||||
b.isStarted.Store(false)
|
||||
|
||||
@@ -77,6 +86,19 @@ func newChromiumBrowser(arguments browserArguments) browser {
|
||||
}
|
||||
|
||||
func (b *chromiumBrowser) Start(logger *slog.Logger) error {
|
||||
// Refuse to run while a previous Start is still in flight. That previous
|
||||
// Start may be a goroutine the supervisor abandoned after the request
|
||||
// deadline expired while the Chromium startup handshake was hanging; it
|
||||
// still holds the pinning proxy it started. An abandoned goroutine keeps
|
||||
// holding startMu until it unwinds (bounded by --chromium-start-timeout),
|
||||
// so no overlapping Start can collide with it on the shared pinning proxy
|
||||
// and latch Chromium into a permanent "pinning proxy already started"
|
||||
// state. See https://github.com/gotenberg/gotenberg/issues/1599.
|
||||
if !b.startMu.TryLock() {
|
||||
return errors.New("browser start already in progress")
|
||||
}
|
||||
defer b.startMu.Unlock()
|
||||
|
||||
if b.isStarted.Load() {
|
||||
return errors.New("browser is already started")
|
||||
}
|
||||
|
||||
39
pkg/modules/chromium/browser_test.go
Normal file
39
pkg/modules/chromium/browser_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestChromiumBrowser_Start_rejectsOverlappingStart guards against the latch
|
||||
// reported in https://github.com/gotenberg/gotenberg/issues/1599. When the
|
||||
// supervisor abandons a Start goroutine on request-deadline expiry, that
|
||||
// goroutine keeps running and holds startMu (and the pinning proxy it started)
|
||||
// until it unwinds. A second Start must be refused rather than proceeding to
|
||||
// start the pinning proxy a second time.
|
||||
func TestChromiumBrowser_Start_rejectsOverlappingStart(t *testing.T) {
|
||||
b := &chromiumBrowser{initialCtx: context.Background()}
|
||||
|
||||
// Simulate a Start still in flight.
|
||||
b.startMu.Lock()
|
||||
defer b.startMu.Unlock()
|
||||
|
||||
err := b.Start(slog.New(slog.DiscardHandler))
|
||||
if err == nil {
|
||||
t.Fatal("expected an error when a start is already in progress, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "already in progress") {
|
||||
t.Fatalf("expected an 'already in progress' error, got %q", err)
|
||||
}
|
||||
|
||||
// The guard must return before touching any startup resource, so no user
|
||||
// profile directory is created and the browser stays not started.
|
||||
if b.userProfileDirPath != "" {
|
||||
t.Fatalf("expected no user profile directory to be created, got %q", b.userProfileDirPath)
|
||||
}
|
||||
if b.isStarted.Load() {
|
||||
t.Fatal("expected the browser to stay not started")
|
||||
}
|
||||
}
|
||||
@@ -300,7 +300,8 @@ type PdfOptions struct {
|
||||
PreferCssPageSize bool
|
||||
|
||||
// GenerateDocumentOutline defines whether the document outline should be
|
||||
// embedded into the PDF.
|
||||
// embedded into the PDF. Chromium derives the outline from the tagged-PDF
|
||||
// structure tree, so enabling this implies GenerateTaggedPdf.
|
||||
GenerateDocumentOutline bool
|
||||
|
||||
// GenerateTaggedPdf defines whether to generate tagged (accessible)
|
||||
@@ -459,6 +460,7 @@ func (mod *Chromium) Descriptor() gotenberg.ModuleDescriptor {
|
||||
fs.Bool("chromium-allow-file-access-from-files", false, "Allow file:// URIs to read other file:// URIs")
|
||||
fs.String("chromium-host-resolver-rules", "", "Set custom mappings to the host resolver")
|
||||
fs.String("chromium-proxy-server", "", "Set the outbound proxy server; this switch only affects HTTP and HTTPS requests")
|
||||
fs.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")
|
||||
fs.StringSlice("chromium-deny-list", []string{`^file:(?!//\/tmp/).*`}, "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")
|
||||
@@ -506,6 +508,7 @@ func (mod *Chromium) Provision(ctx *gotenberg.Context) error {
|
||||
allowFileAccessFromFiles: flags.MustBool("chromium-allow-file-access-from-files"),
|
||||
hostResolverRules: flags.MustString("chromium-host-resolver-rules"),
|
||||
proxyServer: flags.MustString("chromium-proxy-server"),
|
||||
enableEnvironmentProxy: flags.MustBool("chromium-enable-environment-proxy"),
|
||||
wsUrlReadTimeout: flags.MustDuration("chromium-start-timeout"),
|
||||
hyphenDataDirPath: hyphenDataDirPath,
|
||||
|
||||
|
||||
@@ -9,10 +9,12 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dlclark/regexp2"
|
||||
"golang.org/x/net/http/httpproxy"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
@@ -43,6 +45,14 @@ type pinningProxy struct {
|
||||
// allow-list opt-in). Tests may override it.
|
||||
dialBypass func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
|
||||
// upstreamProxy resolves the upstream (corporate) proxy for a
|
||||
// destination URL from the standard proxy environment variables, or
|
||||
// returns a nil URL to connect directly. It is nil unless the operator
|
||||
// opted into proxy-environment honoring. When set, the pinning proxy
|
||||
// performs the authenticated proxy handshake that Chromium cannot. See
|
||||
// https://github.com/gotenberg/gotenberg/issues/1592.
|
||||
upstreamProxy func(*url.URL) (*url.URL, error)
|
||||
|
||||
listener net.Listener
|
||||
server *http.Server
|
||||
wg sync.WaitGroup
|
||||
@@ -57,8 +67,8 @@ type pinningProxy struct {
|
||||
// [gotenberg.DecideOutbound] on every request the proxy sees, so
|
||||
// Chromium inherits whatever posture the operator selected. The
|
||||
// returned proxy is not yet listening; call Start.
|
||||
func newPinningProxy(allowList, denyList []*regexp2.Regexp, denyPrivateIPs, denyPublicIPs bool) *pinningProxy {
|
||||
return &pinningProxy{
|
||||
func newPinningProxy(allowList, denyList []*regexp2.Regexp, denyPrivateIPs, denyPublicIPs, enableEnvironmentProxy bool) *pinningProxy {
|
||||
p := &pinningProxy{
|
||||
allowList: allowList,
|
||||
denyList: denyList,
|
||||
decide: func(ctx context.Context, rawURL string, allow, deny []*regexp2.Regexp, deadline time.Time) (gotenberg.OutboundDecision, error) {
|
||||
@@ -73,6 +83,14 @@ func newPinningProxy(allowList, denyList []*regexp2.Regexp, denyPrivateIPs, deny
|
||||
return dialer.DialContext(ctx, network, addr)
|
||||
},
|
||||
}
|
||||
|
||||
if enableEnvironmentProxy {
|
||||
// Honor the standard proxy environment variables, credentials
|
||||
// included. httpproxy reads the environment now and applies NO_PROXY.
|
||||
p.upstreamProxy = httpproxy.FromEnvironment().ProxyFunc()
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// Start binds the proxy to 127.0.0.1 on an ephemeral port and serves in a
|
||||
@@ -188,8 +206,24 @@ func (p *pinningProxy) handleConnect(w http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// When the operator routes egress through an authenticated proxy,
|
||||
// Chromium cannot supply the credentials itself, so the pinning proxy
|
||||
// performs the CONNECT (and authentication) upstream. The decision above
|
||||
// still gated the destination through the allow/deny and IP-class rules.
|
||||
var proxyURL *url.URL
|
||||
if p.upstreamProxy != nil {
|
||||
proxyURL, err = p.upstreamProxy(&url.URL{Scheme: "https", Host: req.Host})
|
||||
if err != nil {
|
||||
p.logger.WarnContext(req.Context(), fmt.Sprintf("resolve upstream proxy for '%s': %s", req.Host, err))
|
||||
http.Error(w, "upstream proxy error", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var upstream net.Conn
|
||||
switch {
|
||||
case proxyURL != nil:
|
||||
upstream, err = p.dialThroughUpstreamProxy(req.Context(), proxyURL, req.Host)
|
||||
case decision.Bypass:
|
||||
upstream, err = p.dialBypass(req.Context(), "tcp", req.Host)
|
||||
case len(decision.Pinned) > 0:
|
||||
@@ -275,17 +309,35 @@ func (p *pinningProxy) handleForward(w http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var proxyURL *url.URL
|
||||
if p.upstreamProxy != nil {
|
||||
proxyURL, err = p.upstreamProxy(req.URL)
|
||||
if err != nil {
|
||||
p.logger.WarnContext(req.Context(), fmt.Sprintf("resolve upstream proxy for '%s': %s", req.URL.Redacted(), err))
|
||||
http.Error(w, "upstream proxy error", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
outReq := req.Clone(req.Context())
|
||||
outReq.RequestURI = ""
|
||||
stripHopByHopHeaders(outReq.Header)
|
||||
|
||||
// Build a fresh transport per request. The decision contains the pinned
|
||||
// IPs to dial; reusing a transport across requests would leak the
|
||||
// decision's closure across unrelated targets.
|
||||
transport := &http.Transport{
|
||||
// Build a fresh transport per request. The decision contains the
|
||||
// pinned IPs to dial; reusing a transport across requests would
|
||||
// leak the decision's closure across unrelated targets.
|
||||
DisableKeepAlives: true,
|
||||
Proxy: nil,
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
}
|
||||
if proxyURL != nil {
|
||||
// The upstream proxy owns DNS and egress; Go adds Proxy-Authorization
|
||||
// from the URL's credentials. The decision above already gated the
|
||||
// destination, and dialBypass dials the proxy host directly.
|
||||
transport.Proxy = http.ProxyURL(proxyURL)
|
||||
transport.DialContext = p.dialBypass
|
||||
} else {
|
||||
transport.Proxy = nil
|
||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
_, port, splitErr := net.SplitHostPort(addr)
|
||||
if splitErr != nil {
|
||||
return nil, fmt.Errorf("split forward addr %q: %w", addr, splitErr)
|
||||
@@ -298,7 +350,7 @@ func (p *pinningProxy) handleForward(w http.ResponseWriter, req *http.Request) {
|
||||
default:
|
||||
return nil, errors.New("no pinned addresses and not bypassed")
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
defer transport.CloseIdleConnections()
|
||||
|
||||
@@ -364,3 +416,11 @@ func isClientCancellation(ctx context.Context, err error) bool {
|
||||
}
|
||||
return ctx.Err() != nil
|
||||
}
|
||||
|
||||
// dialThroughUpstreamProxy tunnels to target through the upstream proxy,
|
||||
// letting [gotenberg.DialThroughProxy] perform the authenticated CONNECT that
|
||||
// Chromium cannot. dialBypass dials the proxy itself and is overridable in
|
||||
// tests. See https://github.com/gotenberg/gotenberg/issues/1592.
|
||||
func (p *pinningProxy) dialThroughUpstreamProxy(ctx context.Context, proxyURL *url.URL, target string) (net.Conn, error) {
|
||||
return gotenberg.DialThroughProxy(ctx, proxyURL, target, p.dialBypass)
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ func TestPinningProxy_Forward_Pinned_Success(t *testing.T) {
|
||||
upstreamURL := mustParseURL(t, upstream.URL)
|
||||
|
||||
var decideCalls atomic.Int32
|
||||
p := newPinningProxy(nil, nil, false, false)
|
||||
p := newPinningProxy(nil, nil, false, false, false)
|
||||
p.decide = func(_ context.Context, _ string, _, _ []*regexp2.Regexp, _ time.Time) (gotenberg.OutboundDecision, error) {
|
||||
decideCalls.Add(1)
|
||||
return gotenberg.OutboundDecision{Pinned: []netip.Addr{netip.MustParseAddr("127.0.0.1")}}, nil
|
||||
@@ -151,7 +151,7 @@ func TestPinningProxy_Forward_Pinned_Success(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPinningProxy_Forward_BlockedByDecide(t *testing.T) {
|
||||
p := newPinningProxy(nil, nil, false, false)
|
||||
p := newPinningProxy(nil, nil, false, false, false)
|
||||
p.decide = func(_ context.Context, _ string, _, _ []*regexp2.Regexp, _ time.Time) (gotenberg.OutboundDecision, error) {
|
||||
return gotenberg.OutboundDecision{}, fmt.Errorf("nope: %w", gotenberg.ErrFiltered)
|
||||
}
|
||||
@@ -187,7 +187,7 @@ func TestPinningProxy_Forward_Bypass(t *testing.T) {
|
||||
upstreamURL := mustParseURL(t, upstream.URL)
|
||||
|
||||
var bypassCalls atomic.Int32
|
||||
p := newPinningProxy(nil, nil, false, false)
|
||||
p := newPinningProxy(nil, nil, false, false, false)
|
||||
p.decide = func(_ context.Context, _ string, _, _ []*regexp2.Regexp, _ time.Time) (gotenberg.OutboundDecision, error) {
|
||||
return gotenberg.OutboundDecision{Bypass: true}, nil
|
||||
}
|
||||
@@ -236,7 +236,7 @@ func TestPinningProxy_Forward_StripsHopByHopHeaders(t *testing.T) {
|
||||
t.Cleanup(upstream.Close)
|
||||
upstreamURL := mustParseURL(t, upstream.URL)
|
||||
|
||||
p := newPinningProxy(nil, nil, false, false)
|
||||
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
|
||||
}
|
||||
@@ -276,7 +276,7 @@ func TestPinningProxy_Forward_StripsHopByHopHeaders(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPinningProxy_Forward_RejectsNonAbsoluteURL(t *testing.T) {
|
||||
p := newPinningProxy(nil, nil, false, false)
|
||||
p := newPinningProxy(nil, nil, false, false, false)
|
||||
p.decide = func(_ context.Context, _ string, _, _ []*regexp2.Regexp, _ time.Time) (gotenberg.OutboundDecision, error) {
|
||||
t.Fatal("decide must not be called for malformed proxy request")
|
||||
return gotenberg.OutboundDecision{}, nil
|
||||
@@ -316,7 +316,7 @@ func TestPinningProxy_CONNECT_Pinned_Success(t *testing.T) {
|
||||
t.Cleanup(stop)
|
||||
|
||||
var decideCalls atomic.Int32
|
||||
p := newPinningProxy(nil, nil, false, false)
|
||||
p := newPinningProxy(nil, nil, false, false, false)
|
||||
p.decide = func(_ context.Context, _ string, _, _ []*regexp2.Regexp, _ time.Time) (gotenberg.OutboundDecision, error) {
|
||||
decideCalls.Add(1)
|
||||
return gotenberg.OutboundDecision{Pinned: []netip.Addr{netip.MustParseAddr("127.0.0.1")}}, nil
|
||||
@@ -386,7 +386,7 @@ func TestPinningProxy_CONNECT_Pinned_Success(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPinningProxy_CONNECT_BlockedByDecide(t *testing.T) {
|
||||
p := newPinningProxy(nil, nil, false, false)
|
||||
p := newPinningProxy(nil, nil, false, false, false)
|
||||
p.decide = func(_ context.Context, _ string, _, _ []*regexp2.Regexp, _ time.Time) (gotenberg.OutboundDecision, error) {
|
||||
return gotenberg.OutboundDecision{}, fmt.Errorf("nope: %w", gotenberg.ErrFiltered)
|
||||
}
|
||||
@@ -445,7 +445,7 @@ func TestPinningProxy_DNSRebind_SingleResolution(t *testing.T) {
|
||||
return gotenberg.OutboundDecision{}, fmt.Errorf("rebind lookup: %w", gotenberg.ErrFiltered)
|
||||
}
|
||||
|
||||
p := newPinningProxy(nil, nil, false, false)
|
||||
p := newPinningProxy(nil, nil, false, false, false)
|
||||
p.decide = stubDecide
|
||||
p.dialPinned = func(_ context.Context, network string, addrs []netip.Addr, _ string) (net.Conn, error) {
|
||||
if len(addrs) != 1 || addrs[0].String() != "93.184.216.34" {
|
||||
@@ -488,7 +488,7 @@ func TestPinningProxy_DNSRebind_SingleResolution(t *testing.T) {
|
||||
// [TestPinningProxy_CONNECT_BlockedByDecide].
|
||||
func TestPinningProxy_CONNECT_ClientCancellation_LoggedAtDebug(t *testing.T) {
|
||||
rec := &recordingHandler{}
|
||||
p := newPinningProxy(nil, nil, false, false)
|
||||
p := newPinningProxy(nil, nil, false, false, false)
|
||||
p.decide = func(_ context.Context, _ string, _, _ []*regexp2.Regexp, _ time.Time) (gotenberg.OutboundDecision, error) {
|
||||
// Mimic the wrap chain produced by outbound.resolveHost when the
|
||||
// DNS lookup is canceled mid-flight by Chromium hanging up.
|
||||
@@ -547,7 +547,7 @@ func TestPinningProxy_CONNECT_ClientCancellation_LoggedAtDebug(t *testing.T) {
|
||||
// HTTP forward requests aborted by the client must also log at debug.
|
||||
func TestPinningProxy_Forward_ClientCancellation_LoggedAtDebug(t *testing.T) {
|
||||
rec := &recordingHandler{}
|
||||
p := newPinningProxy(nil, nil, false, false)
|
||||
p := newPinningProxy(nil, nil, false, false, false)
|
||||
p.decide = func(_ context.Context, _ string, _, _ []*regexp2.Regexp, _ time.Time) (gotenberg.OutboundDecision, error) {
|
||||
return gotenberg.OutboundDecision{}, fmt.Errorf("validate host: %w", context.DeadlineExceeded)
|
||||
}
|
||||
@@ -596,7 +596,7 @@ func TestPinningProxy_Forward_ClientCancellation_LoggedAtDebug(t *testing.T) {
|
||||
// still surface at warn level so operators see real refusals.
|
||||
func TestPinningProxy_PolicyDenial_LoggedAtWarn(t *testing.T) {
|
||||
rec := &recordingHandler{}
|
||||
p := newPinningProxy(nil, nil, false, false)
|
||||
p := newPinningProxy(nil, nil, false, false, false)
|
||||
p.decide = func(_ context.Context, _ string, _, _ []*regexp2.Regexp, _ time.Time) (gotenberg.OutboundDecision, error) {
|
||||
return gotenberg.OutboundDecision{}, fmt.Errorf("denied: %w", gotenberg.ErrFiltered)
|
||||
}
|
||||
@@ -652,7 +652,7 @@ func TestPinningProxy_PolicyDenial_LoggedAtWarn(t *testing.T) {
|
||||
// [TestPinningProxy_CONNECT_DialFailure_LoggedAtWarn].
|
||||
func TestPinningProxy_CONNECT_DialCancellation_LoggedAtDebug(t *testing.T) {
|
||||
rec := &recordingHandler{}
|
||||
p := newPinningProxy(nil, nil, false, false)
|
||||
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
|
||||
}
|
||||
@@ -713,7 +713,7 @@ func TestPinningProxy_CONNECT_DialCancellation_LoggedAtDebug(t *testing.T) {
|
||||
// must still warn so operators see real problems.
|
||||
func TestPinningProxy_CONNECT_DialFailure_LoggedAtWarn(t *testing.T) {
|
||||
rec := &recordingHandler{}
|
||||
p := newPinningProxy(nil, nil, false, false)
|
||||
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
|
||||
}
|
||||
@@ -770,7 +770,7 @@ func TestPinningProxy_CONNECT_DialFailure_LoggedAtWarn(t *testing.T) {
|
||||
// logs at debug, not warn. Genuine RoundTrip failures still warn.
|
||||
func TestPinningProxy_Forward_RoundTripCancellation_LoggedAtDebug(t *testing.T) {
|
||||
rec := &recordingHandler{}
|
||||
p := newPinningProxy(nil, nil, false, false)
|
||||
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
|
||||
}
|
||||
@@ -868,7 +868,7 @@ func TestIsClientCancellation(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPinningProxy_StartTwice(t *testing.T) {
|
||||
p := newPinningProxy(nil, nil, false, false)
|
||||
p := newPinningProxy(nil, nil, false, false, false)
|
||||
err := p.Start(testLogger())
|
||||
if err != nil {
|
||||
t.Fatalf("first Start: %v", err)
|
||||
@@ -882,7 +882,7 @@ func TestPinningProxy_StartTwice(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPinningProxy_StopIdempotent(t *testing.T) {
|
||||
p := newPinningProxy(nil, nil, false, false)
|
||||
p := newPinningProxy(nil, nil, false, false, false)
|
||||
// Stop on a never-started proxy is a no-op.
|
||||
if err := p.Stop(testLogger()); err != nil {
|
||||
t.Fatalf("Stop on never-started proxy: %v", err)
|
||||
|
||||
76
pkg/modules/chromium/pinning_proxy_upstream_test.go
Normal file
76
pkg/modules/chromium/pinning_proxy_upstream_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dlclark/regexp2"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
|
||||
// TestPinningProxy_Forward_ThroughUpstreamProxy verifies that when the
|
||||
// operator opts into proxy-environment honoring, a plain HTTP request is
|
||||
// forwarded through the upstream (corporate) proxy with the credentials
|
||||
// Chromium cannot supply. See https://github.com/gotenberg/gotenberg/issues/1592.
|
||||
func TestPinningProxy_Forward_ThroughUpstreamProxy(t *testing.T) {
|
||||
var gotAuth atomic.Value
|
||||
gotAuth.Store("")
|
||||
|
||||
// Stand-in for the corporate proxy: an HTTP server that receives the
|
||||
// forwarded request and records the injected Proxy-Authorization.
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotAuth.Store(r.Header.Get("Proxy-Authorization"))
|
||||
_, _ = fmt.Fprint(w, "via-corporate-proxy")
|
||||
}))
|
||||
t.Cleanup(upstream.Close)
|
||||
|
||||
upstreamURL := mustParseURL(t, upstream.URL)
|
||||
upstreamURL.User = url.UserPassword("bob", "pw")
|
||||
|
||||
p := newPinningProxy(nil, nil, false, false, true)
|
||||
// Force every destination through our stub upstream proxy.
|
||||
p.upstreamProxy = func(_ *url.URL) (*url.URL, error) { return upstreamURL, nil }
|
||||
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) {
|
||||
t.Fatal("dialPinned must not be called when routing through an upstream proxy")
|
||||
return nil, nil
|
||||
}
|
||||
proxyURL := newProxyForTest(t, p)
|
||||
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{Proxy: http.ProxyURL(mustParseURL(t, proxyURL))},
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
resp, err := client.Get("http://example.com/")
|
||||
if err != nil {
|
||||
t.Fatalf("GET via proxy: %v", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read body: %v", err)
|
||||
}
|
||||
if string(body) != "via-corporate-proxy" {
|
||||
t.Fatalf("body = %q, want via-corporate-proxy", body)
|
||||
}
|
||||
|
||||
wantAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte("bob:pw"))
|
||||
if got := gotAuth.Load().(string); got != wantAuth {
|
||||
t.Fatalf("upstream proxy saw Proxy-Authorization %q, want %q", got, wantAuth)
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,29 @@ import (
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
|
||||
// resolvePdfOptions applies the cross-option constraints Chromium imposes
|
||||
// before printing.
|
||||
//
|
||||
// Chromium derives the PDF document outline from the tagged-PDF structure
|
||||
// tree, so [PdfOptions.GenerateDocumentOutline] produces no outline unless
|
||||
// tagged PDF is also generated. Requesting an outline therefore implies
|
||||
// tagged PDF. See https://github.com/gotenberg/gotenberg/issues/1579.
|
||||
func resolvePdfOptions(options PdfOptions) PdfOptions {
|
||||
if options.GenerateDocumentOutline {
|
||||
options.GenerateTaggedPdf = true
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
func printToPdfActionFunc(reqCtx context.Context, logger *slog.Logger, outputPath string, options PdfOptions) chromedp.ActionFunc {
|
||||
return func(ctx context.Context) error {
|
||||
if options.GenerateDocumentOutline && !options.GenerateTaggedPdf {
|
||||
logger.DebugContext(ctx, "document outline requested, enabling tagged PDF because Chromium derives the outline from the structure tree")
|
||||
}
|
||||
|
||||
options = resolvePdfOptions(options)
|
||||
|
||||
// ctx is the chromedp task context, derived from context.Background(),
|
||||
// so the span is started under reqCtx to keep print_to_pdf in the
|
||||
// conversion trace instead of orphaning it into a new one.
|
||||
|
||||
52
pkg/modules/chromium/tasks_test.go
Normal file
52
pkg/modules/chromium/tasks_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package chromium
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestResolvePdfOptions(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
generateOutline bool
|
||||
generateTaggedIn bool
|
||||
generateTaggedWant bool
|
||||
}{
|
||||
{
|
||||
scenario: "outline requested forces tagged PDF",
|
||||
generateOutline: true,
|
||||
generateTaggedIn: false,
|
||||
generateTaggedWant: true,
|
||||
},
|
||||
{
|
||||
scenario: "outline requested keeps tagged PDF on",
|
||||
generateOutline: true,
|
||||
generateTaggedIn: true,
|
||||
generateTaggedWant: true,
|
||||
},
|
||||
{
|
||||
scenario: "no outline leaves tagged PDF off",
|
||||
generateOutline: false,
|
||||
generateTaggedIn: false,
|
||||
generateTaggedWant: false,
|
||||
},
|
||||
{
|
||||
scenario: "no outline keeps tagged PDF on",
|
||||
generateOutline: false,
|
||||
generateTaggedIn: true,
|
||||
generateTaggedWant: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
options := DefaultPdfOptions()
|
||||
options.GenerateDocumentOutline = tc.generateOutline
|
||||
options.GenerateTaggedPdf = tc.generateTaggedIn
|
||||
|
||||
got := resolvePdfOptions(options)
|
||||
|
||||
if got.GenerateTaggedPdf != tc.generateTaggedWant {
|
||||
t.Errorf("expected GenerateTaggedPdf=%t, got %t", tc.generateTaggedWant, got.GenerateTaggedPdf)
|
||||
}
|
||||
if got.GenerateDocumentOutline != tc.generateOutline {
|
||||
t.Errorf("expected GenerateDocumentOutline=%t, got %t", tc.generateOutline, got.GenerateDocumentOutline)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -336,6 +336,7 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
|
||||
fs.StringSlice("libreoffice-deny-list", []string{}, "Set the denied URLs for LibreOffice outbound fetches using regular expressions - supports multiple values")
|
||||
fs.Bool("libreoffice-deny-private-ips", false, "Reject LibreOffice outbound URLs whose host resolves to a non-public IP address (loopback, RFC1918, link-local, unique-local). Enable on deployments that accept untrusted documents to mitigate SSRF against internal services")
|
||||
fs.Bool("libreoffice-deny-public-ips", false, "Reject LibreOffice outbound 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("libreoffice-enable-environment-proxy", false, "Route LibreOffice outbound fetches through the proxy defined by the standard HTTP_PROXY, HTTPS_PROXY, and NO_PROXY variables, including credentials")
|
||||
|
||||
return fs
|
||||
}(),
|
||||
@@ -363,10 +364,11 @@ func (a *Api) Provision(ctx *gotenberg.Context) error {
|
||||
unoBinPath: unoBinPath,
|
||||
startTimeout: flags.MustDuration("libreoffice-start-timeout"),
|
||||
proxyOptions: outboundProxyOptions{
|
||||
allowList: flags.MustRegexpSlice("libreoffice-allow-list"),
|
||||
denyList: flags.MustRegexpSlice("libreoffice-deny-list"),
|
||||
denyPrivateIPs: flags.MustBool("libreoffice-deny-private-ips"),
|
||||
denyPublicIPs: flags.MustBool("libreoffice-deny-public-ips"),
|
||||
allowList: flags.MustRegexpSlice("libreoffice-allow-list"),
|
||||
denyList: flags.MustRegexpSlice("libreoffice-deny-list"),
|
||||
denyPrivateIPs: flags.MustBool("libreoffice-deny-private-ips"),
|
||||
denyPublicIPs: flags.MustBool("libreoffice-deny-public-ips"),
|
||||
enableEnvironmentProxy: flags.MustBool("libreoffice-enable-environment-proxy"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -15,16 +15,18 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/dlclark/regexp2"
|
||||
"golang.org/x/net/http/httpproxy"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
|
||||
// outboundProxyOptions configures a [libreOfficeProxy].
|
||||
type outboundProxyOptions struct {
|
||||
allowList []*regexp2.Regexp
|
||||
denyList []*regexp2.Regexp
|
||||
denyPrivateIPs bool
|
||||
denyPublicIPs bool
|
||||
allowList []*regexp2.Regexp
|
||||
denyList []*regexp2.Regexp
|
||||
denyPrivateIPs bool
|
||||
denyPublicIPs bool
|
||||
enableEnvironmentProxy bool
|
||||
}
|
||||
|
||||
// libreOfficeProxy is an HTTP/HTTPS forward proxy that LibreOffice routes
|
||||
@@ -45,6 +47,12 @@ type libreOfficeProxy struct {
|
||||
opts outboundProxyOptions
|
||||
logger *slog.Logger
|
||||
|
||||
// upstreamProxy resolves the upstream (corporate) proxy for a destination
|
||||
// URL from the standard proxy environment variables, or returns a nil URL
|
||||
// to connect directly. Nil unless the operator opted into proxy-
|
||||
// environment honoring. See https://github.com/gotenberg/gotenberg/issues/1592.
|
||||
upstreamProxy func(*url.URL) (*url.URL, error)
|
||||
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
@@ -64,10 +72,15 @@ func newLibreOfficeProxy(logger *slog.Logger, opts outboundProxyOptions) (*libre
|
||||
|
||||
p := &libreOfficeProxy{
|
||||
listener: listener,
|
||||
client: gotenberg.NewOutboundHttpClient(0, opts.allowList, opts.denyList, decideOpts...),
|
||||
client: gotenberg.NewOutboundHttpClient(0, opts.allowList, opts.denyList, opts.enableEnvironmentProxy, decideOpts...),
|
||||
opts: opts,
|
||||
logger: logger.With(slog.String("logger", "libreoffice-proxy")),
|
||||
}
|
||||
if opts.enableEnvironmentProxy {
|
||||
// Honor the standard proxy environment variables, credentials
|
||||
// included. httpproxy reads the environment now and applies NO_PROXY.
|
||||
p.upstreamProxy = httpproxy.FromEnvironment().ProxyFunc()
|
||||
}
|
||||
p.server = &http.Server{
|
||||
Handler: p,
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
@@ -182,8 +195,25 @@ func (p *libreOfficeProxy) handleConnect(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
// When the operator routes egress through an authenticated proxy, soffice
|
||||
// cannot supply the credentials, so the proxy performs the CONNECT (and
|
||||
// authentication) upstream. The decision above still gated the destination.
|
||||
var proxyURL *url.URL
|
||||
if p.upstreamProxy != nil {
|
||||
proxyURL, err = p.upstreamProxy(&url.URL{Scheme: "https", Host: net.JoinHostPort(host, port)})
|
||||
if err != nil {
|
||||
p.logger.WarnContext(r.Context(), fmt.Sprintf("LibreOffice proxy resolve upstream proxy for '%s': %s", rawURL, err))
|
||||
http.Error(w, "proxy: upstream proxy error", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var dest net.Conn
|
||||
switch {
|
||||
case proxyURL != nil:
|
||||
dest, err = gotenberg.DialThroughProxy(r.Context(), proxyURL, r.Host, func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return net.DialTimeout(network, addr, 10*time.Second)
|
||||
})
|
||||
case len(decision.Pinned) > 0:
|
||||
dest, err = gotenberg.DialPinned(r.Context(), "tcp", decision.Pinned, port)
|
||||
default:
|
||||
|
||||
@@ -224,7 +224,7 @@ func webhookMiddleware(w *Webhook) api.Middleware {
|
||||
startTime: startTime,
|
||||
|
||||
client: &retryablehttp.Client{
|
||||
HTTPClient: gotenberg.NewOutboundHttpClient(w.clientTimeout, w.allowList, w.denyList, ipOpts...),
|
||||
HTTPClient: gotenberg.NewOutboundHttpClient(w.clientTimeout, w.allowList, w.denyList, w.enableEnvironmentProxy, ipOpts...),
|
||||
RetryMax: w.maxRetry,
|
||||
RetryWaitMin: w.retryMinWait,
|
||||
RetryWaitMax: w.retryMaxWait,
|
||||
|
||||
@@ -18,19 +18,20 @@ func init() {
|
||||
// Webhook is a module that provides a middleware for uploading output files
|
||||
// to any destinations in an asynchronous fashion.
|
||||
type Webhook struct {
|
||||
enableSyncMode bool
|
||||
allowList []*regexp2.Regexp
|
||||
denyList []*regexp2.Regexp
|
||||
errorAllowList []*regexp2.Regexp
|
||||
errorDenyList []*regexp2.Regexp
|
||||
denyPrivateIPs bool
|
||||
denyPublicIPs bool
|
||||
maxRetry int
|
||||
retryMinWait time.Duration
|
||||
retryMaxWait time.Duration
|
||||
clientTimeout time.Duration
|
||||
asyncCount atomic.Int64
|
||||
disable bool
|
||||
enableSyncMode bool
|
||||
allowList []*regexp2.Regexp
|
||||
denyList []*regexp2.Regexp
|
||||
errorAllowList []*regexp2.Regexp
|
||||
errorDenyList []*regexp2.Regexp
|
||||
denyPrivateIPs bool
|
||||
denyPublicIPs bool
|
||||
enableEnvironmentProxy bool
|
||||
maxRetry int
|
||||
retryMinWait time.Duration
|
||||
retryMaxWait time.Duration
|
||||
clientTimeout time.Duration
|
||||
asyncCount atomic.Int64
|
||||
disable bool
|
||||
}
|
||||
|
||||
// Descriptor returns an [Webhook]'s module descriptor.
|
||||
@@ -44,6 +45,7 @@ func (w *Webhook) Descriptor() gotenberg.ModuleDescriptor {
|
||||
fs.StringSlice("webhook-deny-list", []string{}, "Set the denied URLs for the webhook feature using regular expressions - supports multiple values")
|
||||
fs.Bool("webhook-deny-private-ips", false, "Reject webhook URLs whose host resolves to a non-public IP address (loopback, RFC1918, link-local, unique-local). Enable on deployments that accept untrusted webhook destinations to mitigate SSRF against internal services")
|
||||
fs.Bool("webhook-deny-public-ips", false, "Reject webhook URLs whose host resolves to a public IP address. Enable on air-gapped or data-governed deployments to prevent callbacks from leaving a private network")
|
||||
fs.Bool("webhook-enable-environment-proxy", false, "Route webhook callbacks through the proxy defined by the standard HTTP_PROXY, HTTPS_PROXY, and NO_PROXY variables, including credentials")
|
||||
fs.Int("webhook-max-retry", 4, "Set the maximum number of retries for the webhook feature")
|
||||
|
||||
// Deprecated flags.
|
||||
@@ -78,6 +80,7 @@ func (w *Webhook) Provision(ctx *gotenberg.Context) error {
|
||||
w.errorDenyList = flags.MustDeprecatedRegexpSlice("webhook-error-deny-list", "webhook-deny-list")
|
||||
w.denyPrivateIPs = flags.MustBool("webhook-deny-private-ips")
|
||||
w.denyPublicIPs = flags.MustBool("webhook-deny-public-ips")
|
||||
w.enableEnvironmentProxy = flags.MustBool("webhook-enable-environment-proxy")
|
||||
w.maxRetry = flags.MustInt("webhook-max-retry")
|
||||
w.retryMinWait = flags.MustDuration("webhook-retry-min-wait")
|
||||
w.retryMaxWait = flags.MustDuration("webhook-retry-max-wait")
|
||||
|
||||
@@ -548,6 +548,25 @@ Feature: /forms/chromium/convert/html
|
||||
Error: Exception 2
|
||||
"""
|
||||
|
||||
Scenario: POST /forms/chromium/convert/html (Document Outline)
|
||||
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-outline-html/index.html | file |
|
||||
| generateDocumentOutline | true | field |
|
||||
| Gotenberg-Output-Filename | foo | 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
|
||||
# generateTaggedPdf is left unset, yet the outline must still be embedded:
|
||||
# Gotenberg enables tagged PDF automatically because Chromium derives the
|
||||
# outline from the structure tree. See issue #1579.
|
||||
Then the "foo.pdf" PDF should have a document outline
|
||||
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-outline-html/index.html | file |
|
||||
| Gotenberg-Output-Filename | bar | header |
|
||||
Then the response status code should be 200
|
||||
Then the "bar.pdf" PDF should NOT have a document outline
|
||||
|
||||
Scenario: POST /forms/chromium/convert/html (Bad Request)
|
||||
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):
|
||||
|
||||
@@ -1077,6 +1077,60 @@ func (s *scenario) thePdfShouldBeSetToLandscapeOrientation(ctx context.Context,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) thePdfShouldHaveADocumentOutline(ctx context.Context, name, kind string) error {
|
||||
var path string
|
||||
if !strings.HasPrefix(name, "*_") {
|
||||
path = fmt.Sprintf("%s/%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"), name)
|
||||
|
||||
_, err := os.Stat(path)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("PDF %q does not exist", path)
|
||||
}
|
||||
} else {
|
||||
substr := strings.ReplaceAll(name, "*_", "")
|
||||
err := filepath.Walk(s.teststoreDir, func(currentPath string, info os.FileInfo, pathErr error) error {
|
||||
if pathErr != nil {
|
||||
return pathErr
|
||||
}
|
||||
if strings.Contains(info.Name(), substr) {
|
||||
path = currentPath
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("walk %q: %w", s.workdir, err)
|
||||
}
|
||||
}
|
||||
|
||||
cmd := []string{
|
||||
"verapdf",
|
||||
"-off",
|
||||
"--extract",
|
||||
"outlines",
|
||||
filepath.Base(path),
|
||||
}
|
||||
|
||||
output, err := execCommandInIntegrationToolsContainer(ctx, cmd, path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("exec %q: %w", cmd, err)
|
||||
}
|
||||
|
||||
// veraPDF emits an empty features report when the catalog holds no outline.
|
||||
hasOutline := !strings.Contains(output, "<featuresReport></featuresReport>")
|
||||
invert := kind == "should NOT"
|
||||
|
||||
if !invert && !hasOutline {
|
||||
return fmt.Errorf("PDF %q has no document outline", path)
|
||||
}
|
||||
|
||||
if invert && hasOutline {
|
||||
return fmt.Errorf("PDF %q has a document outline", path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// pdfPageText extracts the text of a single page from a produced PDF using
|
||||
// pdftotext. name is either a literal filename or a "*_" glob resolved against
|
||||
// the test store.
|
||||
@@ -1541,6 +1595,7 @@ 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 "([^"]*)" 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) 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 content matching "([^"]*)" at page (\d+)$`, s.thePdfShouldHaveContentMatchingAtPage)
|
||||
ctx.Then(`^the "([^"]*)" PDF should have (\d+) image\(s\)$`, s.thePdfShouldHaveImages)
|
||||
|
||||
16
test/integration/testdata/page-outline-html/index.html
vendored
Normal file
16
test/integration/testdata/page-outline-html/index.html
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Outline</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Chapter 1</h1>
|
||||
<p>Intro.</p>
|
||||
<h2>Section 1.1</h2>
|
||||
<p>Body.</p>
|
||||
<h1>Chapter 2</h1>
|
||||
<p>Intro.</p>
|
||||
<h2>Section 2.1</h2>
|
||||
<p>Body.</p>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user