Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add encrypt route #496

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,75 @@ paths:
'400':
description: >-
Bad Request, e.g. Invalid form data: no form file found for extensions: [.pdf]; form value 'pdfFormat' is required

/forms/pdfengines/encrypt:
post:
tags:
- pdfengines
summary: Encrypts PDFs with given passwords
externalDocs:
url: https://gotenberg.dev/docs/modules/pdf-engines
description: >-
This route accepts PDF files and a form field pdfFormat for converting them into the specified format.
parameters:
- in: header
name: Gotenberg-Output-Filename
description: >-
By default, the API generates a UUID filename.
However, you may also specify the filename per request,
thanks to the Gotenberg-Output-Filename header.
Caution! The API adds the file extension automatically; you don't have to set it.
schema:
type: string
required: false
- in: header
name: Gotenberg-Trace
description: >-
The trace, or request ID, identifies a request in the logs.

By default, the API generates a UUID trace for each request.
However, you may also specify the trace per request, thanks to the Gotenberg-Trace header.
schema:
type: string
required: false
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
files:
type: array
items:
type: string
format: binary
ownerPassword:
type: string
description: The Password used for permissions.
example: secureOwnerPassword
userPassword:
type: string
description: The Password used for access to the PDF
example: secureUserPassword
keyLength:
type: integer
description: Keylength for encryption, defaults to 256
enum:
- 40
- 128
- 256
example: 256
required:
- files
- ownerPassword
- userPassword

responses:
'200':
$ref: '#/components/responses/SuccessfulPDF'
'400':
description: >-
Bad Request, e.g. Invalid form data: no form file found for extensions: [.pdf]; form value 'ownerPassword' is required; form value 'userPassword' is required;

components:
schemas:
Expand Down
5 changes: 5 additions & 0 deletions pkg/gotenberg/mocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func (mod ValidatorMock) Validate() error {
type PDFEngineMock struct {
MergeMock func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
ConvertMock func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error
EncryptMock func(ctx context.Context, logger *zap.Logger, keyLength int, ownerPassword, userPassword, inputPath, outputPath string) error
}

func (engine PDFEngineMock) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
Expand All @@ -38,6 +39,10 @@ func (engine PDFEngineMock) Convert(ctx context.Context, logger *zap.Logger, for
return engine.ConvertMock(ctx, logger, format, inputPath, outputPath)
}

func (engine PDFEngineMock) Encrypt(ctx context.Context, logger *zap.Logger, keyLength int, ownerPassword, userPassword, inputPath, outputPath string) error {
return engine.EncryptMock(ctx, logger, keyLength, ownerPassword, userPassword, inputPath, outputPath)
}

// PDFEngineProviderMock is a mock for the PDFEngineProvider interface.
type PDFEngineProviderMock struct {
PDFEngineMock func() (PDFEngine, error)
Expand Down
3 changes: 3 additions & 0 deletions pkg/gotenberg/pdfengine.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ type PDFEngine interface {

// Convert converts the given PDF to a specific PDF format.
Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error

//Encrypt one or more PDF Files with given passwords.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
//Encrypt one or more PDF Files with given passwords.
// Encrypt encrypts the given PDF.

Encrypt(ctx context.Context, logger *zap.Logger, keyLength int, ownerPassword, userPassword, inputPath, outputPath string) error
}

// PDFEngineProvider is a module interface which exposes a method for creating a
Expand Down
5 changes: 5 additions & 0 deletions pkg/modules/chromium/chromium_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func (mod ProtoPDFEngineProvider) PDFEngine() (gotenberg.PDFEngine, error) {
type ProtoPDFEngine struct {
merge func(_ context.Context, _ *zap.Logger, _ []string, _ string) error
convert func(_ context.Context, _ *zap.Logger, _, _, _ string) error
encrypt func(_ context.Context, _ *zap.Logger, _ int, _, _, _, _ string) error
}

func (mod ProtoPDFEngine) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
Expand All @@ -51,6 +52,10 @@ func (mod ProtoPDFEngine) Convert(ctx context.Context, logger *zap.Logger, forma
return mod.convert(ctx, logger, format, inputPath, outputPath)
}

func (mod ProtoPDFEngine) Encrypt(ctx context.Context, logger *zap.Logger, keyLength int, ownerPassword, userPassword, inputPath, outputPath string) error {
return mod.encrypt(ctx, logger, keyLength, ownerPassword, userPassword, inputPath, outputPath)
}

func TestDefaultOptions(t *testing.T) {
actual := DefaultOptions()
notExpect := Options{}
Expand Down
5 changes: 5 additions & 0 deletions pkg/modules/libreoffice/pdfengine/pdfengine.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ func (engine UNO) Convert(ctx context.Context, logger *zap.Logger, format, input
return fmt.Errorf("convert PDF to '%s' with unoconv: %w", format, err)
}

// Encrypt is not available for this PDF engine.
func (engine UNO) Encrypt(_ context.Context, _ *zap.Logger, _ int, _, _ , _ , _ string) error {
return fmt.Errorf("encrypt PDF with unoconv: %w", gotenberg.ErrPDFEngineMethodNotAvailable)
}

// Interface guards.
var (
_ gotenberg.Module = (*UNO)(nil)
Expand Down
16 changes: 16 additions & 0 deletions pkg/modules/pdfcpu/pdfcpu.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,22 @@ func (engine PDFcpu) Convert(_ context.Context, _ *zap.Logger, format, _, _ stri
return fmt.Errorf("convert PDF to '%s' with PDFcpu: %w", format, gotenberg.ErrPDFEngineMethodNotAvailable)
}

func (engine PDFcpu) Encrypt(_ context.Context, _ *zap.Logger, keyLength int, ownerPassword, userPassword, inputPath, outputPath string) error {
conf := engine.conf

conf.EncryptKeyLength = keyLength
conf.UserPW = userPassword
conf.OwnerPW = ownerPassword

err := pdfcpuAPI.EncryptFile(inputPath, outputPath, conf)

if err == nil {
return nil
}

return fmt.Errorf("encrypt PDF with PDFcpu: %w", err)
}

// Interface guards.
var (
_ gotenberg.Module = (*PDFcpu)(nil)
Expand Down
23 changes: 23 additions & 0 deletions pkg/modules/pdfengines/multi.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,29 @@ func (multi multiPDFEngines) Convert(ctx context.Context, logger *zap.Logger, fo
return fmt.Errorf("convert PDF to '%s' with multi PDF engines: %w", format, err)
}

func (multi multiPDFEngines) Encrypt(ctx context.Context, logger *zap.Logger, keyLength int, ownerPassword, userPassword, inputPath, outputPath string) error {
var err error
errChan := make(chan error, 1)

for _, engine := range multi.engines {
go func(engine gotenberg.PDFEngine) {
errChan <- engine.Encrypt(ctx, logger, keyLength, ownerPassword, userPassword, inputPath, outputPath)
}(engine)

select {
case mergeErr := <-errChan:
errored := multierr.AppendInto(&err, mergeErr)
if !errored {
return nil
}
case <-ctx.Done():
return ctx.Err()
}
}

return fmt.Errorf("encrypt PDF with multi PDF engines: %w", err)
}

// Interface guards.
var (
_ gotenberg.PDFEngine = (*multiPDFEngines)(nil)
Expand Down
1 change: 1 addition & 0 deletions pkg/modules/pdfengines/pdfengines.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ func (mod PDFEngines) Routes() ([]api.Route, error) {
return []api.Route{
mergeRoute(engine),
convertRoute(engine),
encryptRoute(engine),
}, nil
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/modules/pdfengines/pdfengines_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ func TestPDFEngines_Routes(t *testing.T) {
gotenberg.PDFEngineMock{},
},
},
expectRoutesCount: 2,
expectRoutesCount: 3,
},
{
name: "route disabled",
Expand Down
54 changes: 54 additions & 0 deletions pkg/modules/pdfengines/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,57 @@ func convertRoute(engine gotenberg.PDFEngine) api.Route {
},
}
}

func encryptRoute(engine gotenberg.PDFEngine) api.Route {
return api.Route{
Method: http.MethodPost,
Path: "/forms/pdfengines/encrypt",
IsMultipart: true,
Handler: func(c echo.Context) error {
ctx := c.Get("context").(*api.Context)

// Let's get the data from the form and validate them.
var (
inputPaths []string
ownerPassword string
userPassword string
keyLength int
)

err := ctx.FormData().
MandatoryPaths([]string{".pdf"}, &inputPaths).
MandatoryString("ownerPassword", &ownerPassword).
MandatoryString("userPassword", &userPassword).
Int("keyLength", &keyLength, 256).
vidschofelix marked this conversation as resolved.
Show resolved Hide resolved
Validate()

if err != nil {
return fmt.Errorf("validate form data: %w", err)
}

// Alright, let's encrypt the PDFs.

outputPaths := make([]string, len(inputPaths))

for i, inputPath := range inputPaths {
outputPaths[i] = ctx.GeneratePath(".pdf")

err = engine.Encrypt(ctx, ctx.Log(), keyLength, ownerPassword, userPassword, inputPath, outputPaths[i])

if err != nil {
return fmt.Errorf("convert PDF: %w", err)
}
}

// Last but not least, add the output paths to the context so that
// the API is able to send them as a response to the client.

err = ctx.AddOutputPaths(outputPaths...)
if err != nil {
return fmt.Errorf("add output paths: %w", err)
}

return nil
},
}
}
4 changes: 4 additions & 0 deletions pkg/modules/pdftk/pdftk.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ func (engine PDFtk) Convert(_ context.Context, _ *zap.Logger, format, _, _ strin
return fmt.Errorf("convert PDF to '%s' with PDFtk: %w", format, gotenberg.ErrPDFEngineMethodNotAvailable)
}

func (engine PDFtk) Encrypt(ctx context.Context, logger *zap.Logger, keyLength int, ownerPassword, userPassword, inputPath, outputPath string) error {
return fmt.Errorf("encrypt PDF with PDFtk: %w", gotenberg.ErrPDFEngineMethodNotAvailable)
}

var (
activeInstancesCount float64
activeInstancesCountMu sync.RWMutex
Expand Down
29 changes: 29 additions & 0 deletions pkg/modules/qpdf/qpdf.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"os"
"strconv"
"sync"

"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
Expand Down Expand Up @@ -103,6 +104,34 @@ func (engine QPDF) Convert(_ context.Context, _ *zap.Logger, format, _, _ string
return fmt.Errorf("convert PDF to '%s' with QPDF: %w", format, gotenberg.ErrPDFEngineMethodNotAvailable)
}

func (engine QPDF) Encrypt(ctx context.Context, logger *zap.Logger, keyLength int, ownerPassword, userPassword, inputPath, outputPath string) error {
var args []string
args = append(args, "--encrypt", userPassword, ownerPassword, strconv.Itoa(keyLength))
args = append(args, "--", inputPath)
args = append(args, outputPath)

cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
if err != nil {
return fmt.Errorf("create command: %w", err)
}

activeInstancesCountMu.Lock()
activeInstancesCount += 1
activeInstancesCountMu.Unlock()

_, err = cmd.Exec()

activeInstancesCountMu.Lock()
activeInstancesCount -= 1
activeInstancesCountMu.Unlock()

if err == nil {
return nil
}

return fmt.Errorf("encrypt PDF with QPDF: %w", err)
}

var (
activeInstancesCount float64
activeInstancesCountMu sync.RWMutex
Expand Down