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 all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
954 changes: 480 additions & 474 deletions docs/openapi.yaml

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions pkg/gotenberg/encryption.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package gotenberg

import (
"fmt"
)

// EncryptionOptions represents options for encryption.
type EncryptionOptions struct {
// Password for rights of PDF file.
// Required, but can be empty.
OwnerPassword string

// Password for opening PDF file.
// Required, but can be empty.
UserPassword string

// Encryption key length.
// Required.
KeyLength int
}

func NewEncryptionOptions(keyLength int, ownerPassword, userPassword string) *EncryptionOptions {
//check for valid KeyLength
if !isValidKeyLength(keyLength) {
panic(fmt.Sprintf("Invalid keyLength specified: %d", keyLength))
}
//Both Passwords can be empty, but not a single one
if (len(ownerPassword) == 0 || len(userPassword) == 0) && (len(ownerPassword)+len(userPassword) != 0) {
panic("Can't have one single empty password for encryption")
}
settings := EncryptionOptions{KeyLength: keyLength, OwnerPassword: ownerPassword, UserPassword: userPassword}

return &settings
}

func isValidKeyLength(keyLength int) bool {
switch keyLength {
case 40, 128, 256:
return true
default:
return false
}
}

func (e *EncryptionOptions) AreValidForEncryption() bool {
return (len(e.OwnerPassword) != 0 && len(e.UserPassword) != 0)
}
87 changes: 87 additions & 0 deletions pkg/gotenberg/encryption_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package gotenberg

import "testing"

func TestNewEncryptionOptions(t *testing.T) {
for i, tc := range []struct {
OwnerPassword string
UserPassword string
KeyLength int
expectPanic bool
AreValidForEncryption bool
}{
{
OwnerPassword: "a",
UserPassword: "a",
KeyLength: 256,
expectPanic: false,
AreValidForEncryption: true,
},
{
OwnerPassword: "a",
UserPassword: "a",
KeyLength: 128,
expectPanic: false,
AreValidForEncryption: true,
},
{
OwnerPassword: "a",
UserPassword: "a",
KeyLength: 40,
expectPanic: false,
AreValidForEncryption: true,
},
{
OwnerPassword: "a",
UserPassword: "a",
KeyLength: 1337,
expectPanic: true,
AreValidForEncryption: false,
},
{
OwnerPassword: "",
UserPassword: "",
KeyLength: 256,
expectPanic: false,
AreValidForEncryption: false,
},
{
OwnerPassword: "1",
UserPassword: "",
KeyLength: 256,
expectPanic: true,
AreValidForEncryption: false,
},
{
OwnerPassword: "",
UserPassword: "1",
KeyLength: 256,
expectPanic: true,
AreValidForEncryption: false,
},
} {
func() {
if tc.expectPanic {
defer func() {
if r := recover(); r == nil {
t.Errorf("test %d: expected panic but got none", i)
}
}()
}

if !tc.expectPanic {
defer func() {
if r := recover(); r != nil {
t.Errorf("test %d: expected no panic but got: %v", i, r)
}
}()
}

options := NewEncryptionOptions(tc.KeyLength, tc.OwnerPassword, tc.UserPassword)
passValid := options.AreValidForEncryption()
if tc.AreValidForEncryption != passValid {
t.Errorf("test %d: expected encryptionoptions to be: %t, got: %t", i, tc.AreValidForEncryption, passValid)
}
}()
}
}
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, encryptionOptions EncryptionOptions, 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, encryptionOptions EncryptionOptions, inputPath, outputPath string) error {
return engine.EncryptMock(ctx, logger, encryptionOptions, inputPath, outputPath)
}

// PDFEngineProviderMock is a mock for the PDFEngineProvider interface.
type PDFEngineProviderMock struct {
PDFEngineMock func() (PDFEngine, error)
Expand Down
8 changes: 8 additions & 0 deletions pkg/gotenberg/mocks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ func TestPDFEngineMock(t *testing.T) {
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil
},
EncryptMock: func(ctx context.Context, logger *zap.Logger, encryptionOptions EncryptionOptions, inputPath, outputPath string) error {
return nil
},
}

err := mock.Merge(context.Background(), zap.NewNop(), nil, "")
Expand All @@ -53,6 +56,11 @@ func TestPDFEngineMock(t *testing.T) {
if err != nil {
t.Errorf("expected no error from mock.Convert(), but got: %v", err)
}

err = mock.Encrypt(context.Background(), zap.NewNop(), EncryptionOptions{}, "", "")
if err != nil {
t.Errorf("expected no error from mock.Convert(), but got: %v", err)
}
}

func TestPDFEngineProvider(t *testing.T) {
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 encrypts the given PDF.
Encrypt(ctx context.Context, logger *zap.Logger, encryptionOptions EncryptionOptions, 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, _ gotenberg.EncryptionOptions, _, _ 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, encryptionOptions gotenberg.EncryptionOptions, inputPath, outputPath string) error {
return mod.encrypt(ctx, logger, encryptionOptions, inputPath, outputPath)
}

func TestDefaultOptions(t *testing.T) {
actual := DefaultOptions()
notExpect := Options{}
Expand Down
62 changes: 51 additions & 11 deletions pkg/modules/chromium/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,12 @@ func convertURLRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
form, options := FormDataChromiumPDFOptions(ctx)

var (
URL string
PDFformat string
URL string
PDFformat string
ownerPassword string
userPassword string
keyLength int
encryptionOptions gotenberg.EncryptionOptions
)

err := form.
Expand Down Expand Up @@ -160,13 +164,18 @@ func convertURLRoute(chromium API, engine gotenberg.PDFEngine) api.Route {

return nil
}).
String("ownerPassword", &ownerPassword, "").
String("userPassword", &userPassword, "").
Int("keyLength", &keyLength, 256).
Validate()

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

err = convertURL(ctx, chromium, engine, URL, PDFformat, options)
encryptionOptions = *gotenberg.NewEncryptionOptions(keyLength, ownerPassword, userPassword)

err = convertURL(ctx, chromium, engine, URL, PDFformat, options, encryptionOptions)
if err != nil {
return fmt.Errorf("convert URL to PDF: %w", err)
}
Expand All @@ -187,22 +196,31 @@ func convertHTMLRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
form, options := FormDataChromiumPDFOptions(ctx)

var (
inputPath string
PDFformat string
inputPath string
PDFformat string
ownerPassword string
userPassword string
keyLength int
encryptionOptions gotenberg.EncryptionOptions
)

err := form.
MandatoryPath("index.html", &inputPath).
String("pdfFormat", &PDFformat, "").
String("ownerPassword", &ownerPassword, "").
String("userPassword", &userPassword, "").
Int("keyLength", &keyLength, 256).
Validate()

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

encryptionOptions = *gotenberg.NewEncryptionOptions(keyLength, ownerPassword, userPassword)

URL := fmt.Sprintf("file://%s", inputPath)

err = convertURL(ctx, chromium, engine, URL, PDFformat, options)
err = convertURL(ctx, chromium, engine, URL, PDFformat, options, encryptionOptions)
if err != nil {
return fmt.Errorf("convert HTML to PDF: %w", err)
}
Expand All @@ -224,21 +242,30 @@ func convertMarkdownRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
form, options := FormDataChromiumPDFOptions(ctx)

var (
inputPath string
markdownPaths []string
PDFformat string
inputPath string
markdownPaths []string
PDFformat string
ownerPassword string
userPassword string
keyLength int
encryptionOptions gotenberg.EncryptionOptions
)

err := form.
MandatoryPath("index.html", &inputPath).
MandatoryPaths([]string{".md"}, &markdownPaths).
String("pdfFormat", &PDFformat, "").
String("ownerPassword", &ownerPassword, "").
String("userPassword", &userPassword, "").
Int("keyLength", &keyLength, 256).
Validate()

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

encryptionOptions = *gotenberg.NewEncryptionOptions(keyLength, ownerPassword, userPassword)

// We have to convert each markdown file referenced in the HTML
// file to... HTML. Thanks to the "html/template" package, we are
// able to provide the "toHTML" function which the user may call
Expand Down Expand Up @@ -313,7 +340,7 @@ func convertMarkdownRoute(chromium API, engine gotenberg.PDFEngine) api.Route {

URL := fmt.Sprintf("file://%s", inputPath)

err = convertURL(ctx, chromium, engine, URL, PDFformat, options)
err = convertURL(ctx, chromium, engine, URL, PDFformat, options, encryptionOptions)
if err != nil {
return fmt.Errorf("convert markdown to PDF: %w", err)
}
Expand All @@ -324,7 +351,7 @@ func convertMarkdownRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
}

// convertURL is a stub which is called by the other methods of this file.
func convertURL(ctx *api.Context, chromium API, engine gotenberg.PDFEngine, URL, PDFformat string, options Options) error {
func convertURL(ctx *api.Context, chromium API, engine gotenberg.PDFEngine, URL, PDFformat string, options Options, encryptionOptions gotenberg.EncryptionOptions) error {
outputPath := ctx.GeneratePath(".pdf")

err := chromium.PDF(ctx, ctx.Log(), URL, outputPath, options)
Expand Down Expand Up @@ -418,6 +445,19 @@ func convertURL(ctx *api.Context, chromium API, engine gotenberg.PDFEngine, URL,
outputPath = convertOutputPath
}

if encryptionOptions.AreValidForEncryption() {
convertInputPath := outputPath
convertOutputPath := ctx.GeneratePath(".pdf")

err = engine.Encrypt(ctx, ctx.Log(), encryptionOptions, convertInputPath, convertOutputPath)

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

outputPath = convertOutputPath
}

err = ctx.AddOutputPaths(outputPath)
if err != nil {
return fmt.Errorf("add output path: %w", err)
Expand Down
3 changes: 2 additions & 1 deletion pkg/modules/chromium/routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,7 @@ func TestConvertURL(t *testing.T) {
engine gotenberg.PDFEngine
PDFformat string
options Options
encryptionOptions gotenberg.EncryptionOptions
expectErr bool
expectHTTPErr bool
expectHTTPStatus int
Expand Down Expand Up @@ -855,7 +856,7 @@ func TestConvertURL(t *testing.T) {
expectOutputPathsCount: 1,
},
} {
err := convertURL(tc.ctx.Context, tc.api, tc.engine, "", tc.PDFformat, tc.options)
err := convertURL(tc.ctx.Context, tc.api, tc.engine, "", tc.PDFformat, tc.options, tc.encryptionOptions)

if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)
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, _ gotenberg.EncryptionOptions, _, _ string) error {
return fmt.Errorf("encrypt PDF with unoconv: %w", gotenberg.ErrPDFEngineMethodNotAvailable)
}

// Interface guards.
var (
_ gotenberg.Module = (*UNO)(nil)
Expand Down
9 changes: 9 additions & 0 deletions pkg/modules/libreoffice/pdfengine/pdfengine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,12 @@ func TestUNO_Convert(t *testing.T) {
})
}
}

func TestUNO_Encrypt(t *testing.T) {
mod := new(UNO)
err := mod.Encrypt(context.Background(), zap.NewNop(), gotenberg.EncryptionOptions{}, "", "")

if !errors.Is(err, gotenberg.ErrPDFEngineMethodNotAvailable) {
t.Errorf("expected error %v from mod.Merge(), but got: %v", gotenberg.ErrPDFEngineMethodNotAvailable, err)
}
}