mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 00:22:14 +01:00
Compare commits
27 Commits
v8.34.0
...
8b559eb699
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b559eb699 | ||
|
|
50e8e44bc1 | ||
|
|
b243283c9b | ||
|
|
a0d5e93ced | ||
|
|
7568fc379b | ||
|
|
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: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
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"
|
- package-ecosystem: "npm"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
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 }}
|
tags_aws_lambda_libreoffice: ${{ steps.build_push.outputs.tags_aws_lambda_libreoffice }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
id: build_push
|
id: build_push
|
||||||
@@ -50,7 +50,7 @@ jobs:
|
|||||||
tags_aws_lambda_libreoffice: ${{ steps.build_push.outputs.tags_aws_lambda_libreoffice }}
|
tags_aws_lambda_libreoffice: ${{ steps.build_push.outputs.tags_aws_lambda_libreoffice }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
id: build_push
|
id: build_push
|
||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
tags_aws_lambda_libreoffice: ${{ steps.build_push.outputs.tags_aws_lambda_libreoffice }}
|
tags_aws_lambda_libreoffice: ${{ steps.build_push.outputs.tags_aws_lambda_libreoffice }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
id: build_push
|
id: build_push
|
||||||
@@ -104,7 +104,7 @@ jobs:
|
|||||||
tags_aws_lambda_libreoffice: ${{ steps.build_push.outputs.tags_aws_lambda_libreoffice }}
|
tags_aws_lambda_libreoffice: ${{ steps.build_push.outputs.tags_aws_lambda_libreoffice }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
id: build_push
|
id: build_push
|
||||||
@@ -131,7 +131,7 @@ jobs:
|
|||||||
tags_aws_lambda_libreoffice: ${{ steps.build_push.outputs.tags_aws_lambda_libreoffice }}
|
tags_aws_lambda_libreoffice: ${{ steps.build_push.outputs.tags_aws_lambda_libreoffice }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
id: build_push
|
id: build_push
|
||||||
@@ -154,7 +154,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Merge
|
- name: Merge
|
||||||
uses: ./.github/actions/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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
|
|
||||||
@@ -38,10 +38,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version-file: .node-version
|
node-version-file: .node-version
|
||||||
|
|
||||||
@@ -59,10 +59,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Build, test and push
|
- name: Build, test and push
|
||||||
id: build_test_push
|
id: build_test_push
|
||||||
@@ -117,7 +117,7 @@ jobs:
|
|||||||
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Build, test and push
|
- name: Build, test and push
|
||||||
id: build_test_push
|
id: build_test_push
|
||||||
@@ -147,7 +147,7 @@ jobs:
|
|||||||
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Build, test and push
|
- name: Build, test and push
|
||||||
id: build_test_push
|
id: build_test_push
|
||||||
@@ -177,7 +177,7 @@ jobs:
|
|||||||
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Build, test and push
|
- name: Build, test and push
|
||||||
id: build_test_push
|
id: build_test_push
|
||||||
@@ -207,7 +207,7 @@ jobs:
|
|||||||
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Build, test and push
|
- name: Build, test and push
|
||||||
id: build_test_push
|
id: build_test_push
|
||||||
@@ -253,7 +253,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Merge
|
- name: Merge
|
||||||
uses: ./.github/actions/merge
|
uses: ./.github/actions/merge
|
||||||
@@ -322,7 +322,7 @@ jobs:
|
|||||||
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Build, test and push
|
- name: Build, test and push
|
||||||
id: build_test_push
|
id: build_test_push
|
||||||
@@ -351,7 +351,7 @@ jobs:
|
|||||||
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Build, test and push
|
- name: Build, test and push
|
||||||
id: build_test_push
|
id: build_test_push
|
||||||
@@ -380,7 +380,7 @@ jobs:
|
|||||||
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Build, test and push
|
- name: Build, test and push
|
||||||
id: build_test_push
|
id: build_test_push
|
||||||
@@ -409,7 +409,7 @@ jobs:
|
|||||||
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Build, test and push
|
- name: Build, test and push
|
||||||
id: build_test_push
|
id: build_test_push
|
||||||
@@ -438,7 +438,7 @@ jobs:
|
|||||||
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
tags_aws_lambda_libreoffice: ${{ steps.build_test_push.outputs.tags_aws_lambda_libreoffice }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Build, test and push
|
- name: Build, test and push
|
||||||
id: build_test_push
|
id: build_test_push
|
||||||
@@ -460,7 +460,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Merge
|
- name: Merge
|
||||||
uses: ./.github/actions/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
|
continue-on-error: true
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Cleanup
|
- name: Cleanup
|
||||||
uses: ./.github/actions/clean
|
uses: ./.github/actions/clean
|
||||||
|
|||||||
14
Makefile
14
Makefile
@@ -18,6 +18,8 @@ GOTENBERG_BUILD_DEBUG_DATA=true
|
|||||||
API_PORT=3000
|
API_PORT=3000
|
||||||
API_PORT_FROM_ENV=
|
API_PORT_FROM_ENV=
|
||||||
API_BIND_IP=
|
API_BIND_IP=
|
||||||
|
API_TLS_CERT_FILE=
|
||||||
|
API_TLS_KEY_FILE=
|
||||||
API_START_TIMEOUT=30s
|
API_START_TIMEOUT=30s
|
||||||
API_TIMEOUT=30s
|
API_TIMEOUT=30s
|
||||||
API_BODY_LIMIT=
|
API_BODY_LIMIT=
|
||||||
@@ -28,6 +30,9 @@ GOTENBERG_API_BASIC_AUTH_USERNAME=
|
|||||||
GOTENBERG_API_BASIC_AUTH_PASSWORD=
|
GOTENBERG_API_BASIC_AUTH_PASSWORD=
|
||||||
API_DOWNLOAD_FROM_ALLOW_LIST=
|
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_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_DOWNLOAD_FROM_MAX_RETRY=4
|
||||||
API_DISABLE_DOWNLOAD_FROM=false
|
API_DISABLE_DOWNLOAD_FROM=false
|
||||||
API_DISABLE_HEALTH_CHECK_ROUTE_TELEMETRY=true
|
API_DISABLE_HEALTH_CHECK_ROUTE_TELEMETRY=true
|
||||||
@@ -47,8 +52,11 @@ CHROMIUM_DISABLE_WEB_SECURITY=false
|
|||||||
CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES=false
|
CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES=false
|
||||||
CHROMIUM_HOST_RESOLVER_RULES=
|
CHROMIUM_HOST_RESOLVER_RULES=
|
||||||
CHROMIUM_PROXY_SERVER=
|
CHROMIUM_PROXY_SERVER=
|
||||||
|
CHROMIUM_ENABLE_ENVIRONMENT_PROXY=false
|
||||||
CHROMIUM_ALLOW_LIST=
|
CHROMIUM_ALLOW_LIST=
|
||||||
CHROMIUM_DENY_LIST=^file:(?!//\/tmp/).*
|
CHROMIUM_DENY_LIST=^file:(?!//\/tmp/).*
|
||||||
|
CHROMIUM_DENY_PRIVATE_IPS=false
|
||||||
|
CHROMIUM_DENY_PUBLIC_IPS=false
|
||||||
CHROMIUM_CLEAR_CACHE=false
|
CHROMIUM_CLEAR_CACHE=false
|
||||||
CHROMIUM_CLEAR_COOKIES=false
|
CHROMIUM_CLEAR_COOKIES=false
|
||||||
CHROMIUM_DISABLE_JAVASCRIPT=false
|
CHROMIUM_DISABLE_JAVASCRIPT=false
|
||||||
@@ -60,6 +68,9 @@ LIBREOFFICE_AUTO_START=false
|
|||||||
LIBREOFFICE_START_TIMEOUT=20s
|
LIBREOFFICE_START_TIMEOUT=20s
|
||||||
LIBREOFFICE_ALLOW_LIST=
|
LIBREOFFICE_ALLOW_LIST=
|
||||||
LIBREOFFICE_DENY_LIST=
|
LIBREOFFICE_DENY_LIST=
|
||||||
|
LIBREOFFICE_DENY_PRIVATE_IPS=false
|
||||||
|
LIBREOFFICE_DENY_PUBLIC_IPS=false
|
||||||
|
LIBREOFFICE_ENABLE_ENVIRONMENT_PROXY=false
|
||||||
LIBREOFFICE_DISABLE_ROUTES=false
|
LIBREOFFICE_DISABLE_ROUTES=false
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
LOG_FIELDS_PREFIX=
|
LOG_FIELDS_PREFIX=
|
||||||
@@ -97,6 +108,9 @@ OTEL_EXPORTER_OTLP_INSECURE=true
|
|||||||
WEBHOOK_ENABLE_SYNC_MODE=false
|
WEBHOOK_ENABLE_SYNC_MODE=false
|
||||||
WEBHOOK_ALLOW_LIST=
|
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_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_MAX_RETRY=4
|
||||||
WEBHOOK_RETRY_MIN_WAIT=1s
|
WEBHOOK_RETRY_MIN_WAIT=1s
|
||||||
WEBHOOK_RETRY_MAX_WAIT=30s
|
WEBHOOK_RETRY_MAX_WAIT=30s
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# ARG instructions do not create additional layers. Instead, next layers will
|
# ARG instructions do not create additional layers. Instead, next layers will
|
||||||
# concatenate them. Also, we have to repeat ARG instructions in each build
|
# concatenate them. Also, we have to repeat ARG instructions in each build
|
||||||
# stage that uses them.
|
# stage that uses them.
|
||||||
ARG GOLANG_VERSION=1.26.2
|
ARG GOLANG_VERSION=1.26.5
|
||||||
|
|
||||||
# ----------------------------------------------
|
# ----------------------------------------------
|
||||||
# pdfcpu binary build stage
|
# pdfcpu binary build stage
|
||||||
@@ -11,7 +11,7 @@ ARG GOLANG_VERSION=1.26.2
|
|||||||
FROM golang:$GOLANG_VERSION AS pdfcpu-binary-stage
|
FROM golang:$GOLANG_VERSION AS pdfcpu-binary-stage
|
||||||
|
|
||||||
# See https://github.com/pdfcpu/pdfcpu/releases.
|
# See https://github.com/pdfcpu/pdfcpu/releases.
|
||||||
ARG PDFCPU_VERSION=v0.12.0
|
ARG PDFCPU_VERSION=v0.13.0
|
||||||
ENV CGO_ENABLED=0
|
ENV CGO_ENABLED=0
|
||||||
|
|
||||||
# Define the working directory outside of $GOPATH (we're using go modules).
|
# Define the working directory outside of $GOPATH (we're using go modules).
|
||||||
|
|||||||
14
compose.yaml
14
compose.yaml
@@ -21,6 +21,8 @@ services:
|
|||||||
- "--api-port=${API_PORT}"
|
- "--api-port=${API_PORT}"
|
||||||
- "--api-port-from-env=${API_PORT_FROM_ENV}"
|
- "--api-port-from-env=${API_PORT_FROM_ENV}"
|
||||||
- "--api-bind-ip=${API_BIND_IP}"
|
- "--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-start-timeout=${API_START_TIMEOUT}"
|
||||||
- "--api-timeout=${API_TIMEOUT}"
|
- "--api-timeout=${API_TIMEOUT}"
|
||||||
- "--api-body-limit=${API_BODY_LIMIT}"
|
- "--api-body-limit=${API_BODY_LIMIT}"
|
||||||
@@ -29,6 +31,9 @@ services:
|
|||||||
- "--api-enable-basic-auth=${API_ENABLE_BASIC_AUTH}"
|
- "--api-enable-basic-auth=${API_ENABLE_BASIC_AUTH}"
|
||||||
- "--api-download-from-allow-list=${API_DOWNLOAD_FROM_ALLOW_LIST}"
|
- "--api-download-from-allow-list=${API_DOWNLOAD_FROM_ALLOW_LIST}"
|
||||||
- "--api-download-from-deny-list=${API_DOWNLOAD_FROM_DENY_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-download-from-max-retry=${API_DOWNLOAD_FROM_MAX_RETRY}"
|
||||||
- "--api-disable-download-from=${API_DISABLE_DOWNLOAD_FROM}"
|
- "--api-disable-download-from=${API_DISABLE_DOWNLOAD_FROM}"
|
||||||
- "--api-disable-health-check-route-telemetry=${API_DISABLE_HEALTH_CHECK_ROUTE_TELEMETRY}"
|
- "--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-allow-file-access-from-files=${CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES}"
|
||||||
- "--chromium-host-resolver-rules=${CHROMIUM_HOST_RESOLVER_RULES}"
|
- "--chromium-host-resolver-rules=${CHROMIUM_HOST_RESOLVER_RULES}"
|
||||||
- "--chromium-proxy-server=${CHROMIUM_PROXY_SERVER}"
|
- "--chromium-proxy-server=${CHROMIUM_PROXY_SERVER}"
|
||||||
|
- "--chromium-enable-environment-proxy=${CHROMIUM_ENABLE_ENVIRONMENT_PROXY}"
|
||||||
- "--chromium-allow-list=${CHROMIUM_ALLOW_LIST}"
|
- "--chromium-allow-list=${CHROMIUM_ALLOW_LIST}"
|
||||||
- "--chromium-deny-list=${CHROMIUM_DENY_LIST}"
|
- "--chromium-deny-list=${CHROMIUM_DENY_LIST}"
|
||||||
|
- "--chromium-deny-private-ips=${CHROMIUM_DENY_PRIVATE_IPS}"
|
||||||
|
- "--chromium-deny-public-ips=${CHROMIUM_DENY_PUBLIC_IPS}"
|
||||||
- "--chromium-clear-cache=${CHROMIUM_CLEAR_CACHE}"
|
- "--chromium-clear-cache=${CHROMIUM_CLEAR_CACHE}"
|
||||||
- "--chromium-clear-cookies=${CHROMIUM_CLEAR_COOKIES}"
|
- "--chromium-clear-cookies=${CHROMIUM_CLEAR_COOKIES}"
|
||||||
- "--chromium-disable-javascript=${CHROMIUM_DISABLE_JAVASCRIPT}"
|
- "--chromium-disable-javascript=${CHROMIUM_DISABLE_JAVASCRIPT}"
|
||||||
@@ -61,6 +69,9 @@ services:
|
|||||||
- "--libreoffice-start-timeout=${LIBREOFFICE_START_TIMEOUT}"
|
- "--libreoffice-start-timeout=${LIBREOFFICE_START_TIMEOUT}"
|
||||||
- "--libreoffice-allow-list=${LIBREOFFICE_ALLOW_LIST}"
|
- "--libreoffice-allow-list=${LIBREOFFICE_ALLOW_LIST}"
|
||||||
- "--libreoffice-deny-list=${LIBREOFFICE_DENY_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}"
|
- "--libreoffice-disable-routes=${LIBREOFFICE_DISABLE_ROUTES}"
|
||||||
- "--log-level=${LOG_LEVEL}"
|
- "--log-level=${LOG_LEVEL}"
|
||||||
- "--log-fields-prefix=${LOG_FIELDS_PREFIX}"
|
- "--log-fields-prefix=${LOG_FIELDS_PREFIX}"
|
||||||
@@ -91,6 +102,9 @@ services:
|
|||||||
- "--webhook-enable-sync-mode=${WEBHOOK_ENABLE_SYNC_MODE}"
|
- "--webhook-enable-sync-mode=${WEBHOOK_ENABLE_SYNC_MODE}"
|
||||||
- "--webhook-allow-list=${WEBHOOK_ALLOW_LIST}"
|
- "--webhook-allow-list=${WEBHOOK_ALLOW_LIST}"
|
||||||
- "--webhook-deny-list=${WEBHOOK_DENY_LIST}"
|
- "--webhook-deny-list=${WEBHOOK_DENY_LIST}"
|
||||||
|
- "--webhook-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-max-retry=${WEBHOOK_MAX_RETRY}"
|
||||||
- "--webhook-retry-min-wait=${WEBHOOK_RETRY_MIN_WAIT}"
|
- "--webhook-retry-min-wait=${WEBHOOK_RETRY_MIN_WAIT}"
|
||||||
- "--webhook-retry-max-wait=${WEBHOOK_RETRY_MAX_WAIT}"
|
- "--webhook-retry-max-wait=${WEBHOOK_RETRY_MAX_WAIT}"
|
||||||
|
|||||||
76
go.mod
76
go.mod
@@ -4,37 +4,37 @@ go 1.26.2
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alexliesenfeld/health v0.8.1
|
github.com/alexliesenfeld/health v0.8.1
|
||||||
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d
|
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d // pinned with chromedp v0.14.2, see below
|
||||||
github.com/chromedp/chromedp v0.14.2
|
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/cucumber/godog v0.16.0
|
||||||
github.com/dlclark/regexp2 v1.12.0
|
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/google/uuid v1.6.0
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.8
|
github.com/hashicorp/go-retryablehttp v0.7.8
|
||||||
github.com/labstack/echo/v4 v4.15.2
|
github.com/labstack/echo/v4 v4.15.4
|
||||||
github.com/labstack/gommon v0.5.0
|
github.com/labstack/gommon v0.5.0
|
||||||
github.com/mholt/archives v0.1.5
|
github.com/mholt/archives v0.1.5
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27
|
github.com/microcosm-cc/bluemonday v1.0.27
|
||||||
github.com/moby/moby/api v1.54.2
|
github.com/moby/moby/api v1.55.0
|
||||||
github.com/moby/moby/client v0.4.1
|
github.com/moby/moby/client v0.5.1
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.24.1
|
||||||
github.com/shirou/gopsutil/v4 v4.26.5
|
github.com/shirou/gopsutil/v4 v4.26.7
|
||||||
github.com/spf13/pflag v1.0.10
|
github.com/spf13/pflag v1.0.10
|
||||||
github.com/stretchr/testify v1.11.1
|
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/bridges/otelslog v0.19.0
|
||||||
go.opentelemetry.io/contrib/exporters/autoexport v0.69.0
|
go.opentelemetry.io/contrib/exporters/autoexport v0.69.0
|
||||||
go.opentelemetry.io/otel v1.44.0
|
go.opentelemetry.io/otel v1.45.0
|
||||||
go.opentelemetry.io/otel/log v0.20.0
|
go.opentelemetry.io/otel/log v0.20.0
|
||||||
go.opentelemetry.io/otel/metric v1.44.0
|
go.opentelemetry.io/otel/metric v1.45.0
|
||||||
go.opentelemetry.io/otel/sdk v1.44.0
|
go.opentelemetry.io/otel/sdk v1.45.0
|
||||||
go.opentelemetry.io/otel/sdk/log v0.20.0
|
go.opentelemetry.io/otel/sdk/log v0.20.0
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.44.0
|
go.opentelemetry.io/otel/sdk/metric v1.45.0
|
||||||
go.opentelemetry.io/otel/trace v1.44.0
|
go.opentelemetry.io/otel/trace v1.45.0
|
||||||
golang.org/x/net v0.55.0
|
golang.org/x/net v0.57.0
|
||||||
golang.org/x/sync v0.20.0
|
golang.org/x/sync v0.22.0
|
||||||
golang.org/x/term v0.43.0
|
golang.org/x/term v0.45.0
|
||||||
golang.org/x/text v0.37.0
|
golang.org/x/text v0.40.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -57,23 +57,22 @@ require (
|
|||||||
github.com/containerd/log v0.1.0 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
github.com/containerd/platforms v0.2.1 // indirect
|
github.com/containerd/platforms v0.2.1 // indirect
|
||||||
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||||
github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect
|
github.com/cucumber/gherkin/go/v42 v42.0.0 // indirect
|
||||||
github.com/cucumber/messages/go/v21 v21.0.1 // indirect
|
github.com/cucumber/messages/go/v34 v34.2.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/docker/go-connections v0.7.0 // indirect
|
github.com/docker/go-connections v0.7.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||||
github.com/ebitengine/purego v0.10.1 // indirect
|
github.com/ebitengine/purego v0.10.2 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.1.0 // indirect
|
||||||
github.com/go-json-experiment/json v0.0.0-20260520185125-572e7c383686 // 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/logr v1.4.4 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/gobwas/httphead v0.1.0 // indirect
|
github.com/gobwas/httphead v0.1.0 // indirect
|
||||||
github.com/gobwas/pool v0.2.1 // indirect
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
github.com/gobwas/ws v1.4.0 // indirect
|
github.com/gobwas/ws v1.4.0 // indirect
|
||||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
|
||||||
github.com/gorilla/css v1.0.1 // indirect
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
@@ -81,32 +80,32 @@ require (
|
|||||||
github.com/hashicorp/go-memdb v1.3.5 // indirect
|
github.com/hashicorp/go-memdb v1.3.5 // indirect
|
||||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // 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/klauspost/pgzip v1.2.6 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect
|
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect
|
||||||
github.com/magiconair/properties v1.8.10 // 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/mattn/go-isatty v0.0.22 // indirect
|
||||||
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
||||||
github.com/minio/minlz v1.1.1 // indirect
|
github.com/minio/minlz v1.1.1 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/moby/go-archive v0.2.0 // indirect
|
github.com/moby/go-archive v0.2.0 // indirect
|
||||||
github.com/moby/patternmatcher v0.6.1 // 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/user v0.4.0 // indirect
|
||||||
github.com/moby/sys/userns v0.1.0 // indirect
|
github.com/moby/sys/userns v0.1.0 // indirect
|
||||||
github.com/moby/term v0.5.2 // indirect
|
github.com/moby/term v0.5.2 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.1 // 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/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // 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/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/sirupsen/logrus v1.9.4 // indirect
|
||||||
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
@@ -132,14 +131,13 @@ require (
|
|||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0 // indirect
|
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/otel/exporters/stdout/stdouttrace v1.44.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.10.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
|
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
|
||||||
golang.org/x/crypto v0.52.0 // indirect
|
golang.org/x/crypto v0.54.0 // indirect
|
||||||
golang.org/x/sys v0.45.0 // indirect
|
golang.org/x/sys v0.47.0 // indirect
|
||||||
golang.org/x/time v0.15.0 // indirect
|
golang.org/x/time v0.15.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260615183401-62b3387ff324 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260615183401-62b3387ff324 // indirect
|
||||||
google.golang.org/grpc v1.81.1 // indirect
|
google.golang.org/grpc v1.82.1 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
161
go.sum
161
go.sum
@@ -44,16 +44,14 @@ github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpS
|
|||||||
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
||||||
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
||||||
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
|
||||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI=
|
github.com/cucumber/gherkin/go/v42 v42.0.0 h1:Ulh3E2awUUSSja+wonP/IOQ+ycmiZwZbgmzqk5H8JNI=
|
||||||
github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0=
|
github.com/cucumber/gherkin/go/v42 v42.0.0/go.mod h1:CsaumaO2dR9XvBc6ZyiGLMhWCKtTRDxgoxqJigSjSSg=
|
||||||
github.com/cucumber/godog v0.15.1 h1:rb/6oHDdvVZKS66hrhpjFQFHjthFSrQBCOI1LwshNTI=
|
github.com/cucumber/godog v0.16.0 h1:ezQbgItuWqZrjPUQwLJ3muwIlvzXBOfZso5QZfG7efE=
|
||||||
github.com/cucumber/godog v0.15.1/go.mod h1:qju+SQDewOljHuq9NSM66s0xEhogx0q30flfxL4WUk8=
|
github.com/cucumber/godog v0.16.0/go.mod h1:EDUX9yCqANK+GpbftMDeu61sUDtdLuo1JJgXD2n3bbM=
|
||||||
github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI=
|
github.com/cucumber/messages/go/v34 v34.2.0 h1:VCbcNOMz+f8ccjjOOx1NLBNhwvE7/X49Atc8klJa+i8=
|
||||||
github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s=
|
github.com/cucumber/messages/go/v34 v34.2.0/go.mod h1:LYUPjqlTS1kS0pdkdf6sS5uirnjwiIzEGyXPezXNhL8=
|
||||||
github.com/cucumber/messages/go/v22 v22.0.0/go.mod h1:aZipXTKc0JnjCsXrJnuZpWhtay93k7Rn3Dee7iyPJjs=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -68,17 +66,17 @@ github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
|
|||||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
|
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
|
||||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
||||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||||
github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
|
github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
|
||||||
github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
github.com/ebitengine/purego v0.10.2/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 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
|
||||||
github.com/go-json-experiment/json v0.0.0-20260520185125-572e7c383686 h1:NZBJxCpbHS1gzS6xAmyxbJznosZIIPk9IB42v62UvKA=
|
github.com/go-json-experiment/json v0.0.0-20260601182631-00ed12fed2a6 h1:nxP4pPoyqOAgX8lYDFCfl3DyKeXErCvSvhcyzwGV9CE=
|
||||||
github.com/go-json-experiment/json v0.0.0-20260520185125-572e7c383686/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
|
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.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.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
@@ -90,14 +88,10 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
|||||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||||
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
|
||||||
github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
|
||||||
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
|
|
||||||
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 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
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-20260614204949-e08cff860f76 h1:Ltt9ldIaSYEsjA7sPY2c8r9dOmnKM1vlzhh3dxlhBHM=
|
||||||
github.com/gomarkdown/markdown v0.0.0-20260417124207-7d523f7318df/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
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.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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
@@ -111,10 +105,8 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n
|
|||||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||||
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
|
||||||
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
|
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
|
||||||
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||||
github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg=
|
|
||||||
github.com/hashicorp/go-memdb v1.3.5 h1:b3taDMxCBCBVgyRrS1AZVHO14ubMYZB++QpNhBg+Nyo=
|
github.com/hashicorp/go-memdb v1.3.5 h1:b3taDMxCBCBVgyRrS1AZVHO14ubMYZB++QpNhBg+Nyo=
|
||||||
github.com/hashicorp/go-memdb v1.3.5/go.mod h1:8IVKKBkVe+fxFgdFOYxzQQNjz+sWCyHCdIC/+5+Vy1Y=
|
github.com/hashicorp/go-memdb v1.3.5/go.mod h1:8IVKKBkVe+fxFgdFOYxzQQNjz+sWCyHCdIC/+5+Vy1Y=
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
|
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
|
||||||
@@ -128,24 +120,20 @@ github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iP
|
|||||||
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
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.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
|
||||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
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/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 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/labstack/echo/v4 v4.15.2 h1:nnh2sCzGCVYnU+wCisMPiYapEg/QVo/gcI9ePKg5/T4=
|
github.com/labstack/echo/v4 v4.15.4 h1:DL45vVYa+BWE+XuW+zZNd9H0YEdZ80UAWJGcTVW4EVs=
|
||||||
github.com/labstack/echo/v4 v4.15.2/go.mod h1:Xzp1Ns1RA2c9fY7nSgUJkpkUZGNbEIVHZbtbOMPktBI=
|
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 h1:6VSQ2NOzsnEJ5W6+84E0RbcaDDmgB6NIAzWCczTEe6c=
|
||||||
github.com/labstack/gommon v0.5.0/go.mod h1:Rzlg7HHy1maLfzBYGg9NZcVuz1sA68HHhLjhcEllYE0=
|
github.com/labstack/gommon v0.5.0/go.mod h1:Rzlg7HHy1maLfzBYGg9NZcVuz1sA68HHhLjhcEllYE0=
|
||||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||||
@@ -154,8 +142,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/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 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
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.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
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 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||||
github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=
|
github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=
|
||||||
@@ -170,14 +158,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/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 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
|
||||||
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
|
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.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
|
||||||
github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||||
github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY=
|
github.com/moby/moby/client v0.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw=
|
||||||
github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ=
|
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 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
|
||||||
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
|
||||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
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 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||||
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
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=
|
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||||
@@ -186,44 +174,40 @@ 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/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 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
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.5 h1:L5doqgGfQwI7qADJMqnkrSB86rpPsqQDrHeO0HWa5JY=
|
||||||
github.com/nwaples/rardecode/v2 v2.2.3/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
|
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 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
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 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
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 h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
||||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||||
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
|
github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk=
|
||||||
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
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/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.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
|
||||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
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 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
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.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY=
|
||||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
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 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
|
||||||
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
|
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
|
||||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
|
||||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
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 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
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.7 h1:IXzpHz/dkMRYAhKkOXr1HB6SuzWU3eoyyeWe7g3bNZc=
|
||||||
github.com/shirou/gopsutil/v4 v4.26.5 h1:RPcBXkpz7kOj9PqGFQOlBPZHsyaPvPVQc098y9RmCNM=
|
github.com/shirou/gopsutil/v4 v4.26.7/go.mod h1:5O9FjBiXoTDFatIWjZZosqj4pV0DRtLx598xGbBehzM=
|
||||||
github.com/shirou/gopsutil/v4 v4.26.5/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
|
||||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||||
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
|
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
|
||||||
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
|
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
|
||||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
|
||||||
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
|
||||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stangelandcl/ppmd v0.1.1 h1:c25QazhlWUn5nmR1QOzafKhQxBicAr7GGCKER2aJ8H8=
|
github.com/stangelandcl/ppmd v0.1.1 h1:c25QazhlWUn5nmR1QOzafKhQxBicAr7GGCKER2aJ8H8=
|
||||||
@@ -236,11 +220,10 @@ github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+Q
|
|||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
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.43.0 h1:oEQx5MW2DGd9z3AeEQfB2lPM0eLs7ztyaGRu75bFo5A=
|
||||||
github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30=
|
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 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
|
||||||
github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
|
github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
|
||||||
github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4=
|
github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4=
|
||||||
@@ -266,8 +249,8 @@ go.opentelemetry.io/contrib/exporters/autoexport v0.69.0 h1:R3jsCoTIzv0BiYNhW0ax
|
|||||||
go.opentelemetry.io/contrib/exporters/autoexport v0.69.0/go.mod h1:m07gqyr2QhQxKOKb5vqKCCBtLH3uqlNYR7PU/FISXVU=
|
go.opentelemetry.io/contrib/exporters/autoexport v0.69.0/go.mod h1:m07gqyr2QhQxKOKb5vqKCCBtLH3uqlNYR7PU/FISXVU=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
|
||||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
|
||||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.20.0 h1:rydZ9sxbcFdm/oWrVyfLTjHIygMgv0bEeMd+3B/BvoM=
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.20.0 h1:rydZ9sxbcFdm/oWrVyfLTjHIygMgv0bEeMd+3B/BvoM=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.20.0/go.mod h1:earQ25dooT0Hhspq59DZ8YCC50jWfOlFEeWoxy/P444=
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.20.0/go.mod h1:earQ25dooT0Hhspq59DZ8YCC50jWfOlFEeWoxy/P444=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0 h1:owlhcJ3QO3X0YTDTCcDZ4V+6aVDkWbNmBoQ5NUp7Oww=
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0 h1:owlhcJ3QO3X0YTDTCcDZ4V+6aVDkWbNmBoQ5NUp7Oww=
|
||||||
@@ -292,20 +275,20 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0 h1:bl2S7Ubua0Nms+D
|
|||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0/go.mod h1:L0hRV50XdVIODHUfWEqGRCXQvj2rV82STVo12FMFBU0=
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0/go.mod h1:L0hRV50XdVIODHUfWEqGRCXQvj2rV82STVo12FMFBU0=
|
||||||
go.opentelemetry.io/otel/log v0.20.0 h1:/5i0vuHxCLWUfChWG41K9wkM0jafruPw9NU1/RCJirs=
|
go.opentelemetry.io/otel/log v0.20.0 h1:/5i0vuHxCLWUfChWG41K9wkM0jafruPw9NU1/RCJirs=
|
||||||
go.opentelemetry.io/otel/log v0.20.0/go.mod h1:wOcMcjsZpG8x7Bak7IhSi/lg8wscV2C1VdrKCLPlt0E=
|
go.opentelemetry.io/otel/log v0.20.0/go.mod h1:wOcMcjsZpG8x7Bak7IhSi/lg8wscV2C1VdrKCLPlt0E=
|
||||||
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
|
||||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
|
||||||
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
|
go.opentelemetry.io/otel/metric/x v0.67.0 h1:PcicCNZFkZ4bXfSooXdo3WN7RBOVOtjVdo1wD358Uns=
|
||||||
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
|
go.opentelemetry.io/otel/metric/x v0.67.0/go.mod h1:FBjCWZe6wgcqxcMtjdGiClDKXb2YxxXii0CXftE4QtI=
|
||||||
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
|
||||||
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
|
||||||
go.opentelemetry.io/otel/sdk/log v0.20.0 h1:vM3xI7TQgKPiSghe6urZtAkyFY7SodrSpC83CffDFuY=
|
go.opentelemetry.io/otel/sdk/log v0.20.0 h1:vM3xI7TQgKPiSghe6urZtAkyFY7SodrSpC83CffDFuY=
|
||||||
go.opentelemetry.io/otel/sdk/log v0.20.0/go.mod h1:Knej2nmsTUzN79T2eeXdRsjjPcoxoq2pUyUHz9TFyyU=
|
go.opentelemetry.io/otel/sdk/log v0.20.0/go.mod h1:Knej2nmsTUzN79T2eeXdRsjjPcoxoq2pUyUHz9TFyyU=
|
||||||
go.opentelemetry.io/otel/sdk/log/logtest v0.20.0 h1:OqdRZ1guyzamK3M6LlRsmGqRrjkHWw6WZOKKli5ELpg=
|
go.opentelemetry.io/otel/sdk/log/logtest v0.20.0 h1:OqdRZ1guyzamK3M6LlRsmGqRrjkHWw6WZOKKli5ELpg=
|
||||||
go.opentelemetry.io/otel/sdk/log/logtest v0.20.0/go.mod h1:PuMIlm7zAt7c3z8zfOI5ox4iT1Z87We+PF6YoINux/M=
|
go.opentelemetry.io/otel/sdk/log/logtest v0.20.0/go.mod h1:PuMIlm7zAt7c3z8zfOI5ox4iT1Z87We+PF6YoINux/M=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA=
|
||||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
|
||||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
|
||||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
@@ -314,34 +297,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=
|
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 h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
|
||||||
go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0=
|
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.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
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 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
|
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-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
|
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-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260615183401-62b3387ff324 h1:9HZDLIdYBJXAnaFOr9WHrKVycfpY+75s9HGadC0305A=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260615183401-62b3387ff324/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
|
||||||
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
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 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
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=
|
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": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "3.8.3",
|
"prettier": "3.9.6",
|
||||||
"prettier-plugin-gherkin": "^3.1.3",
|
"prettier-plugin-gherkin": "^3.1.3",
|
||||||
"prettier-plugin-sh": "^0.18.0"
|
"prettier-plugin-sh": "^0.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cucumber/gherkin": {
|
"node_modules/@cucumber/gherkin": {
|
||||||
@@ -34,15 +34,80 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@reteps/dockerfmt": {
|
"node_modules/@reteps/dockerfmt": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/@reteps/dockerfmt/-/dockerfmt-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/@reteps/dockerfmt/-/dockerfmt-0.5.4.tgz",
|
||||||
"integrity": "sha512-Hbr7yen4fP5TxGM54ucXa4o5NwWXatJ6Bd9I8gp0PValYbI4Rug2Gu+rVv7K7o/efQc3F5ctqWJz47rYaa8zBw==",
|
"integrity": "sha512-HEGgXVVOb+JtGUSSzXl/XPKFIZjMDTUoHarCjaQdkY+cb5M9K/O3b5xm+x0IPIk3SfHurbc0bSgcFsQlzjitxA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"dockerfmt": "dist/launcher.js"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^v12.20.0 || ^14.13.0 || >=16.0.0"
|
"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": {
|
"node_modules/@types/uuid": {
|
||||||
"version": "10.0.0",
|
"version": "10.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||||
@@ -58,9 +123,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
"node_modules/prettier": {
|
||||||
"version": "3.8.3",
|
"version": "3.9.6",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz",
|
||||||
"integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
|
"integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -86,14 +151,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier-plugin-sh": {
|
"node_modules/prettier-plugin-sh": {
|
||||||
"version": "0.18.1",
|
"version": "0.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/prettier-plugin-sh/-/prettier-plugin-sh-0.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/prettier-plugin-sh/-/prettier-plugin-sh-0.19.0.tgz",
|
||||||
"integrity": "sha512-uZmU22wBMevjh3rmCatNQqiEer2+5KLa0xYCBX6zQQUQkcNzVL+s6FbPKK6ZSUNUbQk6jMAcQHrYPvuL2W6ihQ==",
|
"integrity": "sha512-39VXFZH/cOGtcuu8aeSvqp/hhwomOR4QroZUj+jBz2cNb3os9s0sqFZSNlYts6jdtLLDU7D2YT3Z1+abtb7adQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@reteps/dockerfmt": "^0.5.1",
|
"@reteps/dockerfmt": "^0.5.4",
|
||||||
"sh-syntax": "^0.5.8"
|
"sh-syntax": "^0.6.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
@@ -113,14 +178,11 @@
|
|||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/sh-syntax": {
|
"node_modules/sh-syntax": {
|
||||||
"version": "0.5.8",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/sh-syntax/-/sh-syntax-0.5.8.tgz",
|
"resolved": "https://registry.npmjs.org/sh-syntax/-/sh-syntax-0.6.0.tgz",
|
||||||
"integrity": "sha512-JfVoxf4FxQI5qpsPbkHhZo+n6N9YMJobyl4oGEUBb/31oQYlgTjkXQD8PBiafS2UbWoxrTO0Z5PJUBXEPAG1Zw==",
|
"integrity": "sha512-52VK6z/cdZHv7UURjIcwfBUQZrAhIEEe0bY4lrkfypjnFIKsDZdD3Uaz/dBiw/sF8BeX0Mssv140s8EnrsJ9dQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.8.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
},
|
},
|
||||||
@@ -128,13 +190,6 @@
|
|||||||
"url": "https://opencollective.com/sh-syntax"
|
"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": {
|
"node_modules/uuid": {
|
||||||
"version": "11.0.5",
|
"version": "11.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "3.8.3",
|
"prettier": "3.9.6",
|
||||||
"prettier-plugin-gherkin": "^3.1.3",
|
"prettier-plugin-gherkin": "^3.1.3",
|
||||||
"prettier-plugin-sh": "^0.18.0"
|
"prettier-plugin-sh": "^0.19.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package gotenberg
|
package gotenberg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@@ -12,6 +15,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dlclark/regexp2"
|
"github.com/dlclark/regexp2"
|
||||||
|
"golang.org/x/net/http/httpproxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrNonPublicIP indicates that an outbound URL targets an IP address that
|
// 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]
|
// The default posture is permissive; callers pass [WithDenyPrivateIPs]
|
||||||
// or [WithDenyPublicIPs] to opt into IP-class rejection.
|
// 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 := 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{
|
return &http.Client{
|
||||||
Timeout: timeout,
|
Timeout: timeout,
|
||||||
Transport: &outboundRoundTripper{
|
Transport: &outboundRoundTripper{
|
||||||
@@ -452,3 +484,99 @@ func DialPinned(ctx context.Context, network string, addrs []netip.Addr, port st
|
|||||||
}
|
}
|
||||||
return nil, lastErr
|
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 {
|
type downloadFromConfig struct {
|
||||||
allowList []*regexp2.Regexp
|
allowList []*regexp2.Regexp
|
||||||
denyList []*regexp2.Regexp
|
denyList []*regexp2.Regexp
|
||||||
denyPrivateIPs bool
|
denyPrivateIPs bool
|
||||||
denyPublicIPs bool
|
denyPublicIPs bool
|
||||||
maxRetry int
|
enableEnvironmentProxy bool
|
||||||
disable bool
|
maxRetry int
|
||||||
|
disable bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Router is a module interface that adds routes to the [Api].
|
// 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.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-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-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.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-download-from", false, "Disable the download from feature")
|
||||||
fs.Bool("api-disable-health-check-route-telemetry", true, "Disable telemetry for health check route")
|
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.rootPath = flags.MustString("api-root-path")
|
||||||
a.correlationIdHeader = flags.MustDeprecatedString("api-trace-header", "api-correlation-id-header")
|
a.correlationIdHeader = flags.MustDeprecatedString("api-trace-header", "api-correlation-id-header")
|
||||||
a.downloadFromCfg = downloadFromConfig{
|
a.downloadFromCfg = downloadFromConfig{
|
||||||
allowList: flags.MustRegexpSlice("api-download-from-allow-list"),
|
allowList: flags.MustRegexpSlice("api-download-from-allow-list"),
|
||||||
denyList: flags.MustRegexpSlice("api-download-from-deny-list"),
|
denyList: flags.MustRegexpSlice("api-download-from-deny-list"),
|
||||||
denyPrivateIPs: flags.MustBool("api-download-from-deny-private-ips"),
|
denyPrivateIPs: flags.MustBool("api-download-from-deny-private-ips"),
|
||||||
denyPublicIPs: flags.MustBool("api-download-from-deny-public-ips"),
|
denyPublicIPs: flags.MustBool("api-download-from-deny-public-ips"),
|
||||||
maxRetry: flags.MustInt("api-download-from-max-retry"),
|
enableEnvironmentProxy: flags.MustBool("api-download-from-enable-environment-proxy"),
|
||||||
disable: flags.MustBool("api-disable-download-from"),
|
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.disableHealthCheckRouteTelemetry = flags.MustDeprecatedBool("api-disable-health-check-logging", "api-disable-health-check-route-telemetry")
|
||||||
a.disableRootRouteTelemetry = flags.MustBool("api-disable-root-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{
|
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,
|
RetryMax: downloadFromCfg.maxRetry,
|
||||||
RetryWaitMin: time.Duration(1) * time.Second,
|
RetryWaitMin: time.Duration(1) * time.Second,
|
||||||
RetryWaitMax: time.Until(deadline),
|
RetryWaitMax: time.Until(deadline),
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ type browserArguments struct {
|
|||||||
allowFileAccessFromFiles bool
|
allowFileAccessFromFiles bool
|
||||||
hostResolverRules string
|
hostResolverRules string
|
||||||
proxyServer string
|
proxyServer string
|
||||||
|
enableEnvironmentProxy bool
|
||||||
wsUrlReadTimeout time.Duration
|
wsUrlReadTimeout time.Duration
|
||||||
hyphenDataDirPath string
|
hyphenDataDirPath string
|
||||||
|
|
||||||
@@ -58,6 +59,14 @@ type chromiumBrowser struct {
|
|||||||
userProfileDirPath string
|
userProfileDirPath string
|
||||||
ctxMu sync.RWMutex
|
ctxMu sync.RWMutex
|
||||||
isStarted atomic.Bool
|
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
|
arguments browserArguments
|
||||||
fs *gotenberg.FileSystem
|
fs *gotenberg.FileSystem
|
||||||
@@ -69,7 +78,7 @@ func newChromiumBrowser(arguments browserArguments) browser {
|
|||||||
initialCtx: context.Background(),
|
initialCtx: context.Background(),
|
||||||
arguments: arguments,
|
arguments: arguments,
|
||||||
fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)),
|
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)
|
b.isStarted.Store(false)
|
||||||
|
|
||||||
@@ -77,6 +86,19 @@ func newChromiumBrowser(arguments browserArguments) browser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *chromiumBrowser) Start(logger *slog.Logger) error {
|
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() {
|
if b.isStarted.Load() {
|
||||||
return errors.New("browser is already started")
|
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
|
PreferCssPageSize bool
|
||||||
|
|
||||||
// GenerateDocumentOutline defines whether the document outline should be
|
// 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
|
GenerateDocumentOutline bool
|
||||||
|
|
||||||
// GenerateTaggedPdf defines whether to generate tagged (accessible)
|
// 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.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-host-resolver-rules", "", "Set custom mappings to the host resolver")
|
||||||
fs.String("chromium-proxy-server", "", "Set the outbound proxy server; this switch only affects HTTP and HTTPS requests")
|
fs.String("chromium-proxy-server", "", "Set the outbound proxy server; this switch only affects HTTP and HTTPS requests")
|
||||||
|
fs.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-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.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")
|
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"),
|
allowFileAccessFromFiles: flags.MustBool("chromium-allow-file-access-from-files"),
|
||||||
hostResolverRules: flags.MustString("chromium-host-resolver-rules"),
|
hostResolverRules: flags.MustString("chromium-host-resolver-rules"),
|
||||||
proxyServer: flags.MustString("chromium-proxy-server"),
|
proxyServer: flags.MustString("chromium-proxy-server"),
|
||||||
|
enableEnvironmentProxy: flags.MustBool("chromium-enable-environment-proxy"),
|
||||||
wsUrlReadTimeout: flags.MustDuration("chromium-start-timeout"),
|
wsUrlReadTimeout: flags.MustDuration("chromium-start-timeout"),
|
||||||
hyphenDataDirPath: hyphenDataDirPath,
|
hyphenDataDirPath: hyphenDataDirPath,
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dlclark/regexp2"
|
"github.com/dlclark/regexp2"
|
||||||
|
"golang.org/x/net/http/httpproxy"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
@@ -43,6 +45,14 @@ type pinningProxy struct {
|
|||||||
// allow-list opt-in). Tests may override it.
|
// allow-list opt-in). Tests may override it.
|
||||||
dialBypass func(ctx context.Context, network, addr string) (net.Conn, error)
|
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
|
listener net.Listener
|
||||||
server *http.Server
|
server *http.Server
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
@@ -57,8 +67,8 @@ type pinningProxy struct {
|
|||||||
// [gotenberg.DecideOutbound] on every request the proxy sees, so
|
// [gotenberg.DecideOutbound] on every request the proxy sees, so
|
||||||
// Chromium inherits whatever posture the operator selected. The
|
// Chromium inherits whatever posture the operator selected. The
|
||||||
// returned proxy is not yet listening; call Start.
|
// returned proxy is not yet listening; call Start.
|
||||||
func newPinningProxy(allowList, denyList []*regexp2.Regexp, denyPrivateIPs, denyPublicIPs bool) *pinningProxy {
|
func newPinningProxy(allowList, denyList []*regexp2.Regexp, denyPrivateIPs, denyPublicIPs, enableEnvironmentProxy bool) *pinningProxy {
|
||||||
return &pinningProxy{
|
p := &pinningProxy{
|
||||||
allowList: allowList,
|
allowList: allowList,
|
||||||
denyList: denyList,
|
denyList: denyList,
|
||||||
decide: func(ctx context.Context, rawURL string, allow, deny []*regexp2.Regexp, deadline time.Time) (gotenberg.OutboundDecision, error) {
|
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)
|
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
|
// 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
|
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
|
var upstream net.Conn
|
||||||
switch {
|
switch {
|
||||||
|
case proxyURL != nil:
|
||||||
|
upstream, err = p.dialThroughUpstreamProxy(req.Context(), proxyURL, req.Host)
|
||||||
case decision.Bypass:
|
case decision.Bypass:
|
||||||
upstream, err = p.dialBypass(req.Context(), "tcp", req.Host)
|
upstream, err = p.dialBypass(req.Context(), "tcp", req.Host)
|
||||||
case len(decision.Pinned) > 0:
|
case len(decision.Pinned) > 0:
|
||||||
@@ -275,17 +309,35 @@ func (p *pinningProxy) handleForward(w http.ResponseWriter, req *http.Request) {
|
|||||||
return
|
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 := req.Clone(req.Context())
|
||||||
outReq.RequestURI = ""
|
outReq.RequestURI = ""
|
||||||
stripHopByHopHeaders(outReq.Header)
|
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{
|
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,
|
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)
|
_, port, splitErr := net.SplitHostPort(addr)
|
||||||
if splitErr != nil {
|
if splitErr != nil {
|
||||||
return nil, fmt.Errorf("split forward addr %q: %w", addr, splitErr)
|
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:
|
default:
|
||||||
return nil, errors.New("no pinned addresses and not bypassed")
|
return nil, errors.New("no pinned addresses and not bypassed")
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
defer transport.CloseIdleConnections()
|
defer transport.CloseIdleConnections()
|
||||||
|
|
||||||
@@ -364,3 +416,11 @@ func isClientCancellation(ctx context.Context, err error) bool {
|
|||||||
}
|
}
|
||||||
return ctx.Err() != nil
|
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)
|
upstreamURL := mustParseURL(t, upstream.URL)
|
||||||
|
|
||||||
var decideCalls atomic.Int32
|
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) {
|
p.decide = func(_ context.Context, _ string, _, _ []*regexp2.Regexp, _ time.Time) (gotenberg.OutboundDecision, error) {
|
||||||
decideCalls.Add(1)
|
decideCalls.Add(1)
|
||||||
return gotenberg.OutboundDecision{Pinned: []netip.Addr{netip.MustParseAddr("127.0.0.1")}}, nil
|
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) {
|
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) {
|
p.decide = func(_ context.Context, _ string, _, _ []*regexp2.Regexp, _ time.Time) (gotenberg.OutboundDecision, error) {
|
||||||
return gotenberg.OutboundDecision{}, fmt.Errorf("nope: %w", gotenberg.ErrFiltered)
|
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)
|
upstreamURL := mustParseURL(t, upstream.URL)
|
||||||
|
|
||||||
var bypassCalls atomic.Int32
|
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) {
|
p.decide = func(_ context.Context, _ string, _, _ []*regexp2.Regexp, _ time.Time) (gotenberg.OutboundDecision, error) {
|
||||||
return gotenberg.OutboundDecision{Bypass: true}, nil
|
return gotenberg.OutboundDecision{Bypass: true}, nil
|
||||||
}
|
}
|
||||||
@@ -236,7 +236,7 @@ func TestPinningProxy_Forward_StripsHopByHopHeaders(t *testing.T) {
|
|||||||
t.Cleanup(upstream.Close)
|
t.Cleanup(upstream.Close)
|
||||||
upstreamURL := mustParseURL(t, upstream.URL)
|
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) {
|
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
|
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) {
|
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) {
|
p.decide = func(_ context.Context, _ string, _, _ []*regexp2.Regexp, _ time.Time) (gotenberg.OutboundDecision, error) {
|
||||||
t.Fatal("decide must not be called for malformed proxy request")
|
t.Fatal("decide must not be called for malformed proxy request")
|
||||||
return gotenberg.OutboundDecision{}, nil
|
return gotenberg.OutboundDecision{}, nil
|
||||||
@@ -316,7 +316,7 @@ func TestPinningProxy_CONNECT_Pinned_Success(t *testing.T) {
|
|||||||
t.Cleanup(stop)
|
t.Cleanup(stop)
|
||||||
|
|
||||||
var decideCalls atomic.Int32
|
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) {
|
p.decide = func(_ context.Context, _ string, _, _ []*regexp2.Regexp, _ time.Time) (gotenberg.OutboundDecision, error) {
|
||||||
decideCalls.Add(1)
|
decideCalls.Add(1)
|
||||||
return gotenberg.OutboundDecision{Pinned: []netip.Addr{netip.MustParseAddr("127.0.0.1")}}, nil
|
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) {
|
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) {
|
p.decide = func(_ context.Context, _ string, _, _ []*regexp2.Regexp, _ time.Time) (gotenberg.OutboundDecision, error) {
|
||||||
return gotenberg.OutboundDecision{}, fmt.Errorf("nope: %w", gotenberg.ErrFiltered)
|
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)
|
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.decide = stubDecide
|
||||||
p.dialPinned = func(_ context.Context, network string, addrs []netip.Addr, _ string) (net.Conn, error) {
|
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" {
|
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].
|
// [TestPinningProxy_CONNECT_BlockedByDecide].
|
||||||
func TestPinningProxy_CONNECT_ClientCancellation_LoggedAtDebug(t *testing.T) {
|
func TestPinningProxy_CONNECT_ClientCancellation_LoggedAtDebug(t *testing.T) {
|
||||||
rec := &recordingHandler{}
|
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) {
|
p.decide = func(_ context.Context, _ string, _, _ []*regexp2.Regexp, _ time.Time) (gotenberg.OutboundDecision, error) {
|
||||||
// Mimic the wrap chain produced by outbound.resolveHost when the
|
// Mimic the wrap chain produced by outbound.resolveHost when the
|
||||||
// DNS lookup is canceled mid-flight by Chromium hanging up.
|
// 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.
|
// HTTP forward requests aborted by the client must also log at debug.
|
||||||
func TestPinningProxy_Forward_ClientCancellation_LoggedAtDebug(t *testing.T) {
|
func TestPinningProxy_Forward_ClientCancellation_LoggedAtDebug(t *testing.T) {
|
||||||
rec := &recordingHandler{}
|
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) {
|
p.decide = func(_ context.Context, _ string, _, _ []*regexp2.Regexp, _ time.Time) (gotenberg.OutboundDecision, error) {
|
||||||
return gotenberg.OutboundDecision{}, fmt.Errorf("validate host: %w", context.DeadlineExceeded)
|
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.
|
// still surface at warn level so operators see real refusals.
|
||||||
func TestPinningProxy_PolicyDenial_LoggedAtWarn(t *testing.T) {
|
func TestPinningProxy_PolicyDenial_LoggedAtWarn(t *testing.T) {
|
||||||
rec := &recordingHandler{}
|
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) {
|
p.decide = func(_ context.Context, _ string, _, _ []*regexp2.Regexp, _ time.Time) (gotenberg.OutboundDecision, error) {
|
||||||
return gotenberg.OutboundDecision{}, fmt.Errorf("denied: %w", gotenberg.ErrFiltered)
|
return gotenberg.OutboundDecision{}, fmt.Errorf("denied: %w", gotenberg.ErrFiltered)
|
||||||
}
|
}
|
||||||
@@ -652,7 +652,7 @@ func TestPinningProxy_PolicyDenial_LoggedAtWarn(t *testing.T) {
|
|||||||
// [TestPinningProxy_CONNECT_DialFailure_LoggedAtWarn].
|
// [TestPinningProxy_CONNECT_DialFailure_LoggedAtWarn].
|
||||||
func TestPinningProxy_CONNECT_DialCancellation_LoggedAtDebug(t *testing.T) {
|
func TestPinningProxy_CONNECT_DialCancellation_LoggedAtDebug(t *testing.T) {
|
||||||
rec := &recordingHandler{}
|
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) {
|
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
|
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.
|
// must still warn so operators see real problems.
|
||||||
func TestPinningProxy_CONNECT_DialFailure_LoggedAtWarn(t *testing.T) {
|
func TestPinningProxy_CONNECT_DialFailure_LoggedAtWarn(t *testing.T) {
|
||||||
rec := &recordingHandler{}
|
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) {
|
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
|
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.
|
// logs at debug, not warn. Genuine RoundTrip failures still warn.
|
||||||
func TestPinningProxy_Forward_RoundTripCancellation_LoggedAtDebug(t *testing.T) {
|
func TestPinningProxy_Forward_RoundTripCancellation_LoggedAtDebug(t *testing.T) {
|
||||||
rec := &recordingHandler{}
|
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) {
|
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
|
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) {
|
func TestPinningProxy_StartTwice(t *testing.T) {
|
||||||
p := newPinningProxy(nil, nil, false, false)
|
p := newPinningProxy(nil, nil, false, false, false)
|
||||||
err := p.Start(testLogger())
|
err := p.Start(testLogger())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("first Start: %v", err)
|
t.Fatalf("first Start: %v", err)
|
||||||
@@ -882,7 +882,7 @@ func TestPinningProxy_StartTwice(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPinningProxy_StopIdempotent(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.
|
// Stop on a never-started proxy is a no-op.
|
||||||
if err := p.Stop(testLogger()); err != nil {
|
if err := p.Stop(testLogger()); err != nil {
|
||||||
t.Fatalf("Stop on never-started proxy: %v", err)
|
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"
|
"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 {
|
func printToPdfActionFunc(reqCtx context.Context, logger *slog.Logger, outputPath string, options PdfOptions) chromedp.ActionFunc {
|
||||||
return func(ctx context.Context) error {
|
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(),
|
// ctx is the chromedp task context, derived from context.Background(),
|
||||||
// so the span is started under reqCtx to keep print_to_pdf in the
|
// so the span is started under reqCtx to keep print_to_pdf in the
|
||||||
// conversion trace instead of orphaning it into a new one.
|
// 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.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-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-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
|
return fs
|
||||||
}(),
|
}(),
|
||||||
@@ -363,10 +364,11 @@ func (a *Api) Provision(ctx *gotenberg.Context) error {
|
|||||||
unoBinPath: unoBinPath,
|
unoBinPath: unoBinPath,
|
||||||
startTimeout: flags.MustDuration("libreoffice-start-timeout"),
|
startTimeout: flags.MustDuration("libreoffice-start-timeout"),
|
||||||
proxyOptions: outboundProxyOptions{
|
proxyOptions: outboundProxyOptions{
|
||||||
allowList: flags.MustRegexpSlice("libreoffice-allow-list"),
|
allowList: flags.MustRegexpSlice("libreoffice-allow-list"),
|
||||||
denyList: flags.MustRegexpSlice("libreoffice-deny-list"),
|
denyList: flags.MustRegexpSlice("libreoffice-deny-list"),
|
||||||
denyPrivateIPs: flags.MustBool("libreoffice-deny-private-ips"),
|
denyPrivateIPs: flags.MustBool("libreoffice-deny-private-ips"),
|
||||||
denyPublicIPs: flags.MustBool("libreoffice-deny-public-ips"),
|
denyPublicIPs: flags.MustBool("libreoffice-deny-public-ips"),
|
||||||
|
enableEnvironmentProxy: flags.MustBool("libreoffice-enable-environment-proxy"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,16 +15,18 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dlclark/regexp2"
|
"github.com/dlclark/regexp2"
|
||||||
|
"golang.org/x/net/http/httpproxy"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
// outboundProxyOptions configures a [libreOfficeProxy].
|
// outboundProxyOptions configures a [libreOfficeProxy].
|
||||||
type outboundProxyOptions struct {
|
type outboundProxyOptions struct {
|
||||||
allowList []*regexp2.Regexp
|
allowList []*regexp2.Regexp
|
||||||
denyList []*regexp2.Regexp
|
denyList []*regexp2.Regexp
|
||||||
denyPrivateIPs bool
|
denyPrivateIPs bool
|
||||||
denyPublicIPs bool
|
denyPublicIPs bool
|
||||||
|
enableEnvironmentProxy bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// libreOfficeProxy is an HTTP/HTTPS forward proxy that LibreOffice routes
|
// libreOfficeProxy is an HTTP/HTTPS forward proxy that LibreOffice routes
|
||||||
@@ -45,6 +47,12 @@ type libreOfficeProxy struct {
|
|||||||
opts outboundProxyOptions
|
opts outboundProxyOptions
|
||||||
logger *slog.Logger
|
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
|
stopOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,10 +72,15 @@ func newLibreOfficeProxy(logger *slog.Logger, opts outboundProxyOptions) (*libre
|
|||||||
|
|
||||||
p := &libreOfficeProxy{
|
p := &libreOfficeProxy{
|
||||||
listener: listener,
|
listener: listener,
|
||||||
client: gotenberg.NewOutboundHttpClient(0, opts.allowList, opts.denyList, decideOpts...),
|
client: gotenberg.NewOutboundHttpClient(0, opts.allowList, opts.denyList, opts.enableEnvironmentProxy, decideOpts...),
|
||||||
opts: opts,
|
opts: opts,
|
||||||
logger: logger.With(slog.String("logger", "libreoffice-proxy")),
|
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{
|
p.server = &http.Server{
|
||||||
Handler: p,
|
Handler: p,
|
||||||
ReadHeaderTimeout: 10 * time.Second,
|
ReadHeaderTimeout: 10 * time.Second,
|
||||||
@@ -182,8 +195,25 @@ func (p *libreOfficeProxy) handleConnect(w http.ResponseWriter, r *http.Request)
|
|||||||
return
|
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
|
var dest net.Conn
|
||||||
switch {
|
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:
|
case len(decision.Pinned) > 0:
|
||||||
dest, err = gotenberg.DialPinned(r.Context(), "tcp", decision.Pinned, port)
|
dest, err = gotenberg.DialPinned(r.Context(), "tcp", decision.Pinned, port)
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ func webhookMiddleware(w *Webhook) api.Middleware {
|
|||||||
startTime: startTime,
|
startTime: startTime,
|
||||||
|
|
||||||
client: &retryablehttp.Client{
|
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,
|
RetryMax: w.maxRetry,
|
||||||
RetryWaitMin: w.retryMinWait,
|
RetryWaitMin: w.retryMinWait,
|
||||||
RetryWaitMax: w.retryMaxWait,
|
RetryWaitMax: w.retryMaxWait,
|
||||||
|
|||||||
@@ -18,19 +18,20 @@ func init() {
|
|||||||
// Webhook is a module that provides a middleware for uploading output files
|
// Webhook is a module that provides a middleware for uploading output files
|
||||||
// to any destinations in an asynchronous fashion.
|
// to any destinations in an asynchronous fashion.
|
||||||
type Webhook struct {
|
type Webhook struct {
|
||||||
enableSyncMode bool
|
enableSyncMode bool
|
||||||
allowList []*regexp2.Regexp
|
allowList []*regexp2.Regexp
|
||||||
denyList []*regexp2.Regexp
|
denyList []*regexp2.Regexp
|
||||||
errorAllowList []*regexp2.Regexp
|
errorAllowList []*regexp2.Regexp
|
||||||
errorDenyList []*regexp2.Regexp
|
errorDenyList []*regexp2.Regexp
|
||||||
denyPrivateIPs bool
|
denyPrivateIPs bool
|
||||||
denyPublicIPs bool
|
denyPublicIPs bool
|
||||||
maxRetry int
|
enableEnvironmentProxy bool
|
||||||
retryMinWait time.Duration
|
maxRetry int
|
||||||
retryMaxWait time.Duration
|
retryMinWait time.Duration
|
||||||
clientTimeout time.Duration
|
retryMaxWait time.Duration
|
||||||
asyncCount atomic.Int64
|
clientTimeout time.Duration
|
||||||
disable bool
|
asyncCount atomic.Int64
|
||||||
|
disable bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Descriptor returns an [Webhook]'s module descriptor.
|
// 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.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-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-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")
|
fs.Int("webhook-max-retry", 4, "Set the maximum number of retries for the webhook feature")
|
||||||
|
|
||||||
// Deprecated flags.
|
// 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.errorDenyList = flags.MustDeprecatedRegexpSlice("webhook-error-deny-list", "webhook-deny-list")
|
||||||
w.denyPrivateIPs = flags.MustBool("webhook-deny-private-ips")
|
w.denyPrivateIPs = flags.MustBool("webhook-deny-private-ips")
|
||||||
w.denyPublicIPs = flags.MustBool("webhook-deny-public-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.maxRetry = flags.MustInt("webhook-max-retry")
|
||||||
w.retryMinWait = flags.MustDuration("webhook-retry-min-wait")
|
w.retryMinWait = flags.MustDuration("webhook-retry-min-wait")
|
||||||
w.retryMaxWait = flags.MustDuration("webhook-retry-max-wait")
|
w.retryMaxWait = flags.MustDuration("webhook-retry-max-wait")
|
||||||
|
|||||||
@@ -548,6 +548,25 @@ Feature: /forms/chromium/convert/html
|
|||||||
Error: Exception 2
|
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)
|
Scenario: POST /forms/chromium/convert/html (Bad Request)
|
||||||
Given I have a default Gotenberg container
|
Given I have a default Gotenberg container
|
||||||
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
|
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
|
||||||
|
|||||||
@@ -1077,6 +1077,60 @@ func (s *scenario) thePdfShouldBeSetToLandscapeOrientation(ctx context.Context,
|
|||||||
return nil
|
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
|
// 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
|
// pdftotext. name is either a literal filename or a "*_" glob resolved against
|
||||||
// the test store.
|
// 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 (response|webhook request) PDF\(s\) should declare Factur-X XMP with conformance level "([^"]*)"$`, s.thePdfsShouldDeclareFacturXConformanceLevel)
|
||||||
ctx.Then(`^the "([^"]*)" PDF should have (\d+) page\(s\)$`, s.thePdfShouldHavePages)
|
ctx.Then(`^the "([^"]*)" PDF should have (\d+) page\(s\)$`, s.thePdfShouldHavePages)
|
||||||
ctx.Then(`^the "([^"]*)" PDF (should|should NOT) be set to landscape orientation$`, s.thePdfShouldBeSetToLandscapeOrientation)
|
ctx.Then(`^the "([^"]*)" PDF (should|should NOT) be set to landscape orientation$`, s.thePdfShouldBeSetToLandscapeOrientation)
|
||||||
|
ctx.Then(`^the "([^"]*)" PDF (should|should NOT) have a document outline$`, s.thePdfShouldHaveADocumentOutline)
|
||||||
ctx.Then(`^the "([^"]*)" PDF (should|should NOT) have the following content at page (\d+):$`, s.thePdfShouldHaveTheFollowingContentAtPage)
|
ctx.Then(`^the "([^"]*)" PDF (should|should NOT) have the following content at page (\d+):$`, s.thePdfShouldHaveTheFollowingContentAtPage)
|
||||||
ctx.Then(`^the "([^"]*)" PDF (should|should NOT) have content matching "([^"]*)" at page (\d+)$`, s.thePdfShouldHaveContentMatchingAtPage)
|
ctx.Then(`^the "([^"]*)" PDF (should|should NOT) have content matching "([^"]*)" at page (\d+)$`, s.thePdfShouldHaveContentMatchingAtPage)
|
||||||
ctx.Then(`^the "([^"]*)" PDF should have (\d+) image\(s\)$`, s.thePdfShouldHaveImages)
|
ctx.Then(`^the "([^"]*)" PDF should have (\d+) image\(s\)$`, s.thePdfShouldHaveImages)
|
||||||
|
|||||||
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