From 0ad6623e24e4c0ca2693e6f68cf571438ebea8e0 Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Mon, 1 Sep 2025 20:26:34 +0200 Subject: [PATCH] fix(pdgengines): encrypt behavior accross different engines --- Makefile | 2 + pkg/gotenberg/pdfengine.go | 4 + pkg/modules/pdfengines/routes.go | 10 +++ pkg/modules/pdftk/pdftk.go | 14 +++- pkg/modules/qpdf/qpdf.go | 4 +- .../features/pdfengines_encrypt.feature | 45 ++++++++--- .../features/pdfengines_merge.feature | 24 +++++- .../features/pdfengines_split.feature | 81 ++++++++++++++++++- 8 files changed, 165 insertions(+), 19 deletions(-) diff --git a/Makefile b/Makefile index f726cddb..44ed3d9d 100644 --- a/Makefile +++ b/Makefile @@ -62,6 +62,7 @@ PDFENGINES_FLATTEN_ENGINES=qpdf PDFENGINES_CONVERT_ENGINES=libreoffice-pdfengine PDFENGINES_READ_METADATA_ENGINES=exiftool PDFENGINES_WRITE_METADATA_ENGINES=exiftool +PDFENGINES_ENCRYPT_ENGINES=qpdf,pdfcpu,pdftk PDFENGINES_DISABLE_ROUTES=false PROMETHEUS_NAMESPACE=gotenberg PROMETHEUS_COLLECT_INTERVAL=1s @@ -136,6 +137,7 @@ run: ## Start a Gotenberg container --pdfengines-convert-engines=$(PDFENGINES_CONVERT_ENGINES) \ --pdfengines-read-metadata-engines=$(PDFENGINES_READ_METADATA_ENGINES) \ --pdfengines-write-metadata-engines=$(PDFENGINES_WRITE_METADATA_ENGINES) \ + --pdfengines-encrypt-engines=$(PDFENGINES_ENCRYPT_ENGINES) \ --pdfengines-disable-routes=$(PDFENGINES_DISABLE_ROUTES) \ --prometheus-namespace=$(PROMETHEUS_NAMESPACE) \ --prometheus-collect-interval=$(PROMETHEUS_COLLECT_INTERVAL) \ diff --git a/pkg/gotenberg/pdfengine.go b/pkg/gotenberg/pdfengine.go index 604cc185..78e6e53f 100644 --- a/pkg/gotenberg/pdfengine.go +++ b/pkg/gotenberg/pdfengine.go @@ -27,6 +27,10 @@ var ( // ErrPdfEncryptionNotSupported is returned when encryption // is not supported by the PDF engine. ErrPdfEncryptionNotSupported = errors.New("encryption not supported") + + // ErrPdfEngineEncryptionPasswordsNotSupported is returned when provided + // passwords are not supported by the PDF engine. + ErrPdfEngineEncryptionPasswordsNotSupported = errors.New("passwords not supported") ) const ( diff --git a/pkg/modules/pdfengines/routes.go b/pkg/modules/pdfengines/routes.go index ce9135cb..63f8003c 100644 --- a/pkg/modules/pdfengines/routes.go +++ b/pkg/modules/pdfengines/routes.go @@ -270,6 +270,16 @@ func EncryptPdfStub(ctx *api.Context, engine gotenberg.PdfEngine, userPassword, for _, inputPath := range inputPaths { err := engine.Encrypt(ctx, ctx.Log(), inputPath, userPassword, ownerPassword) if err != nil { + if errors.Is(err, gotenberg.ErrPdfEngineEncryptionPasswordsNotSupported) { + return api.WrapError( + err, + api.NewSentinelHttpError( + http.StatusBadRequest, + "Invalid form data: both 'userPassword' and 'ownerPassword' form fields must be provided and different", + ), + ) + } + return fmt.Errorf("encrypt PDF '%s': %w", inputPath, err) } } diff --git a/pkg/modules/pdftk/pdftk.go b/pkg/modules/pdftk/pdftk.go index c5b76076..5a853e39 100644 --- a/pkg/modules/pdftk/pdftk.go +++ b/pkg/modules/pdftk/pdftk.go @@ -151,13 +151,16 @@ func (engine *PdfTk) Encrypt(ctx context.Context, logger *zap.Logger, inputPath, return errors.New("user password cannot be empty") } - if ownerPassword == "" { - ownerPassword = userPassword + if ownerPassword == userPassword || ownerPassword == "" { + return gotenberg.ErrPdfEngineEncryptionPasswordsNotSupported } + // Create a temp output file in the same directory. + tmpPath := inputPath + ".tmp" + var args []string args = append(args, inputPath) - args = append(args, "output", inputPath) + args = append(args, "output", tmpPath) args = append(args, "encrypt_128bit") args = append(args, "user_pw", userPassword) args = append(args, "owner_pw", ownerPassword) @@ -172,6 +175,11 @@ func (engine *PdfTk) Encrypt(ctx context.Context, logger *zap.Logger, inputPath, return fmt.Errorf("encrypt PDF with PDFtk: %w", err) } + err = os.Rename(tmpPath, inputPath) + if err != nil { + return fmt.Errorf("rename temporary output file with input file: %w", err) + } + return nil } diff --git a/pkg/modules/qpdf/qpdf.go b/pkg/modules/qpdf/qpdf.go index bba05c6b..f2aa2e39 100644 --- a/pkg/modules/qpdf/qpdf.go +++ b/pkg/modules/qpdf/qpdf.go @@ -185,8 +185,8 @@ func (engine *QPdf) Encrypt(ctx context.Context, logger *zap.Logger, inputPath, var args []string args = append(args, inputPath) args = append(args, engine.globalArgs...) - args = append(args, "--encrypt", userPassword, ownerPassword, "256", "--use-aes=y", "--") - args = append(args, inputPath) + args = append(args, "--replace-input") + args = append(args, "--encrypt", userPassword, ownerPassword, "256", "--") cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) if err != nil { diff --git a/test/integration/features/pdfengines_encrypt.feature b/test/integration/features/pdfengines_encrypt.feature index bd81cb17..471e28a3 100644 --- a/test/integration/features/pdfengines_encrypt.feature +++ b/test/integration/features/pdfengines_encrypt.feature @@ -1,6 +1,6 @@ Feature: /forms/pdfengines/encrypt - Scenario: POST /forms/pdfengines/encrypt (default - QPDF - user password only) + Scenario: POST /forms/pdfengines/encrypt (default - user password only) Given I have a default Gotenberg container When I make a "POST" request to Gotenberg at the "/forms/pdfengines/encrypt" endpoint with the following form data and header(s): | files | testdata/page_1.pdf | file | @@ -10,7 +10,31 @@ Feature: /forms/pdfengines/encrypt Then there should be 1 PDF(s) in the response Then the response PDF(s) should be encrypted - Scenario: POST /forms/pdfengines/encrypt (default - QPDF - both user and owner passwords) + Scenario: POST /forms/pdfengines/encrypt (default - both user and owner passwords) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/encrypt" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | userPassword | foo | field | + | ownerPassword | bar | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then the response PDF(s) should be encrypted + + Scenario: POST /forms/pdfengines/encrypt (QPDF - user password only) + Given I have a Gotenberg container with the following environment variable(s): + | PDFENGINES_ENCRYPT_ENGINES | qpdf | + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/encrypt" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | userPassword | foo | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then the response PDF(s) should be encrypted + + Scenario: POST /forms/pdfengines/encrypt (QPDF - both user and owner passwords) + Given I have a Gotenberg container with the following environment variable(s): + | PDFENGINES_ENCRYPT_ENGINES | qpdf | Given I have a default Gotenberg container When I make a "POST" request to Gotenberg at the "/forms/pdfengines/encrypt" endpoint with the following form data and header(s): | files | testdata/page_1.pdf | file | @@ -23,18 +47,19 @@ Feature: /forms/pdfengines/encrypt Scenario: POST /forms/pdfengines/encrypt (PDFtk - user password only) Given I have a Gotenberg container with the following environment variable(s): - | PDFENGINES_PASSWORD_ENGINES | pdftk | + | PDFENGINES_ENCRYPT_ENGINES | pdftk | When I make a "POST" request to Gotenberg at the "/forms/pdfengines/encrypt" endpoint with the following form data and header(s): | files | testdata/page_1.pdf | file | | userPassword | foo | field | - Then the response status code should be 200 - Then the response header "Content-Type" should be "application/pdf" - Then there should be 1 PDF(s) in the response - Then the response PDF(s) should be encrypted + Then the response status code should be 400 + Then the response body should match string: + """ + Invalid form data: both 'userPassword' and 'ownerPassword' form fields must be provided and different + """ Scenario: POST /forms/pdfengines/encrypt (PDFtk - both user and owner passwords) Given I have a Gotenberg container with the following environment variable(s): - | PDFENGINES_PASSWORD_ENGINES | pdftk | + | PDFENGINES_ENCRYPT_ENGINES | pdftk | When I make a "POST" request to Gotenberg at the "/forms/pdfengines/encrypt" endpoint with the following form data and header(s): | files | testdata/page_1.pdf | file | | userPassword | foo | field | @@ -46,7 +71,7 @@ Feature: /forms/pdfengines/encrypt Scenario: POST /forms/pdfengines/encrypt (pdfcpu - user password only) Given I have a Gotenberg container with the following environment variable(s): - | PDFENGINES_PASSWORD_ENGINES | pdfcpu | + | PDFENGINES_ENCRYPT_ENGINES | pdfcpu | When I make a "POST" request to Gotenberg at the "/forms/pdfengines/encrypt" endpoint with the following form data and header(s): | files | testdata/page_1.pdf | file | | userPassword | foo | field | @@ -57,7 +82,7 @@ Feature: /forms/pdfengines/encrypt Scenario: POST /forms/pdfengines/encrypt (pdfcpu - both user and owner passwords) Given I have a Gotenberg container with the following environment variable(s): - | PDFENGINES_PASSWORD_ENGINES | pdfcpu | + | PDFENGINES_ENCRYPT_ENGINES | pdfcpu | When I make a "POST" request to Gotenberg at the "/forms/pdfengines/encrypt" endpoint with the following form data and header(s): | files | testdata/page_1.pdf | file | | userPassword | foo | field | diff --git a/test/integration/features/pdfengines_merge.feature b/test/integration/features/pdfengines_merge.feature index 57cf2890..200e5035 100644 --- a/test/integration/features/pdfengines_merge.feature +++ b/test/integration/features/pdfengines_merge.feature @@ -1,6 +1,6 @@ Feature: /forms/pdfengines/merge - Scenario: POST /forms/pdfengines/merge (default - QPDF) + Scenario: POST /forms/pdfengines/merge (default) Given I have a default Gotenberg container When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s): | files | testdata/page_1.pdf | file | @@ -21,6 +21,28 @@ Feature: /forms/pdfengines/merge Page 2 """ + Scenario: POST /forms/pdfengines/merge (QPDF) + Given I have a Gotenberg container with the following environment variable(s): + | PDFENGINES_MERGE_ENGINES | qpdf | + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | files | testdata/page_2.pdf | file | + | 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 + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 2 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "foo.pdf" PDF should have the following content at page 2: + """ + Page 2 + """ + Scenario: POST /forms/pdfengines/merge (pdfcpu) Given I have a Gotenberg container with the following environment variable(s): | PDFENGINES_MERGE_ENGINES | pdfcpu | diff --git a/test/integration/features/pdfengines_split.feature b/test/integration/features/pdfengines_split.feature index 2cbbae5c..387ef4ad 100644 --- a/test/integration/features/pdfengines_split.feature +++ b/test/integration/features/pdfengines_split.feature @@ -1,6 +1,6 @@ Feature: /forms/pdfengines/split - Scenario: POST /forms/pdfengines/split (Intervals - Default - pdfcpu) + Scenario: POST /forms/pdfengines/split (Intervals - Default) Given I have a default Gotenberg container When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): | files | testdata/pages_3.pdf | file | @@ -27,7 +27,7 @@ Feature: /forms/pdfengines/split Page 3 """ - Scenario: POST /forms/pdfengines/split (Pages - Default - pdfcpu) + Scenario: POST /forms/pdfengines/split (Pages - Default) Given I have a default Gotenberg container When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): | files | testdata/pages_3.pdf | file | @@ -50,7 +50,7 @@ Feature: /forms/pdfengines/split Page 3 """ - Scenario: POST /forms/pdfengines/split (Pages & Unify - Default - pdfcpu) + Scenario: POST /forms/pdfengines/split (Pages & Unify - Default) Given I have a default Gotenberg container When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): | files | testdata/pages_3.pdf | file | @@ -72,6 +72,81 @@ Feature: /forms/pdfengines/split Page 3 """ + Scenario: POST /forms/pdfengines/split (Intervals - pdfcpu) + Given I have a Gotenberg container with the following environment variable(s): + | PDFENGINES_SPLIT_ENGINES | pdfcpu | + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_3.pdf | file | + | splitMode | intervals | field | + | splitSpan | 2 | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + Then there should be the following file(s) in the response: + | pages_3_0.pdf | + | pages_3_1.pdf | + Then the "pages_3_0.pdf" PDF should have 2 page(s) + Then the "pages_3_1.pdf" PDF should have 1 page(s) + Then the "pages_3_0.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "pages_3_0.pdf" PDF should have the following content at page 2: + """ + Page 2 + """ + Then the "pages_3_1.pdf" PDF should have the following content at page 1: + """ + Page 3 + """ + + Scenario: POST /forms/pdfengines/split (Pages - pdfcpu) + Given I have a Gotenberg container with the following environment variable(s): + | PDFENGINES_SPLIT_ENGINES | pdfcpu | + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_3.pdf | file | + | splitMode | pages | field | + | splitSpan | 2- | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + Then there should be the following file(s) in the response: + | pages_3_0.pdf | + | pages_3_1.pdf | + Then the "pages_3_0.pdf" PDF should have 1 page(s) + Then the "pages_3_1.pdf" PDF should have 1 page(s) + Then the "pages_3_0.pdf" PDF should have the following content at page 1: + """ + Page 2 + """ + Then the "pages_3_1.pdf" PDF should have the following content at page 1: + """ + Page 3 + """ + + Scenario: POST /forms/pdfengines/split (Pages & Unify - pdfcpu) + Given I have a Gotenberg container with the following environment variable(s): + | PDFENGINES_SPLIT_ENGINES | pdfcpu | + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_3.pdf | file | + | splitMode | pages | field | + | splitSpan | 2- | field | + | splitUnify | true | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | pages_3.pdf | + Then the "pages_3.pdf" PDF should have 2 page(s) + Then the "pages_3.pdf" PDF should have the following content at page 1: + """ + Page 2 + """ + Then the "pages_3.pdf" PDF should have the following content at page 2: + """ + Page 3 + """ + Scenario: POST /forms/pdfengines/split (Pages & Unify - QPDF) Given I have a Gotenberg container with the following environment variable(s): | PDFENGINES_SPLIT_ENGINES | qpdf |