fix: special characters issues with filenames (#736)

This commit is contained in:
Julien Neuhart
2023-12-05 16:34:35 +01:00
committed by GitHub
parent 21c47f60f2
commit a6ac0a813d
5 changed files with 174 additions and 31 deletions

View File

@@ -12,14 +12,11 @@ import (
"path/filepath"
"strings"
"time"
"unicode"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
"github.com/mholt/archiver/v3"
"go.uber.org/zap"
"golang.org/x/text/runes"
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
@@ -124,15 +121,6 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
ctx.files = make(map[string]string)
copyToDisk := func(fh *multipart.FileHeader) error {
// Avoid directory traversal and normalize filename.
// See https://github.com/gotenberg/gotenberg/issues/104.
t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
filename, _, err := transform.String(t, filepath.Base(fh.Filename))
if err != nil {
return fmt.Errorf("transform filename: %w", err)
}
in, err := fh.Open()
if err != nil {
return fmt.Errorf("open multipart file: %w", err)
@@ -145,6 +133,10 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
}
}()
// Avoid directory traversal and make sure filename characters are
// normalized.
// See: https://github.com/gotenberg/gotenberg/issues/662.
filename := norm.NFC.String(filepath.Base(fh.Filename))
path := fmt.Sprintf("%s/%s", ctx.dirPath, filename)
out, err := os.Create(path)

View File

@@ -4,12 +4,15 @@ import (
"context"
"errors"
"fmt"
"io"
"net"
"os"
"path/filepath"
"sync"
"sync/atomic"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
@@ -290,6 +293,11 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputP
)
}
inputPath, err := nonBasicLatinCharactersGuard(logger, inputPath)
if err != nil {
return fmt.Errorf("non-basic latin characters guard: %w", err)
}
args = append(args, "--output", outputPath, inputPath)
cmd, err := gotenberg.CommandContext(ctx, logger, p.arguments.unoBinPath, args...)
@@ -321,6 +329,65 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputP
return fmt.Errorf("convert to PDF: %w", err)
}
// LibreOffice cannot convert a file with a name containing non-basic Latin
// characters.
// See:
// https://github.com/gotenberg/gotenberg/issues/104
// https://github.com/gotenberg/gotenberg/issues/730
func nonBasicLatinCharactersGuard(logger *zap.Logger, inputPath string) (string, error) {
hasNonBasicLatinChars := func(str string) bool {
for _, r := range str {
// Check if the character is outside basic Latin.
if r != '.' && (r < ' ' || r > '~') {
return true
}
}
return false
}
filename := filepath.Base(inputPath)
if !hasNonBasicLatinChars(filename) {
logger.Debug("no non-basic latin characters in filename, skip copy")
return inputPath, nil
}
logger.Warn("non-basic latin characters in filename, copy to a file with a valid filename")
basePath := filepath.Dir(inputPath)
ext := filepath.Ext(inputPath)
newInputPath := filepath.Join(basePath, fmt.Sprintf("%s%s", uuid.NewString(), ext))
in, err := os.Open(inputPath)
if err != nil {
return "", fmt.Errorf("open file: %w", err)
}
defer func() {
err := in.Close()
if err != nil {
logger.Error(fmt.Sprintf("close file: %s", err))
}
}()
out, err := os.Create(newInputPath)
if err != nil {
return "", fmt.Errorf("create new file: %w", err)
}
defer func() {
err := out.Close()
if err != nil {
logger.Error(fmt.Sprintf("close new file: %s", err))
}
}()
_, err = io.Copy(out, in)
if err != nil {
return "", fmt.Errorf("copy file to new file: %w", err)
}
return newInputPath, nil
}
// Interface guards.
var (
_ gotenberg.Process = (*libreOfficeProcess)(nil)

View File

@@ -545,7 +545,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
err := tc.libreOffice.pdf(
ctx,
logger,
fmt.Sprintf("file://%s/document.txt", tc.fs.WorkingDirPath()),
fmt.Sprintf("%s/document.txt", tc.fs.WorkingDirPath()),
fmt.Sprintf("%s/%s.pdf", tc.fs.WorkingDirPath(), uuid.NewString()),
tc.options,
)
@@ -564,3 +564,87 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
})
}
}
func TestNonBasicLatinCharactersGuard(t *testing.T) {
for _, tc := range []struct {
scenario string
fs *gotenberg.FileSystem
filename string
expectSameInputPath bool
expectError bool
}{
{
scenario: "basic latin characters",
fs: func() *gotenberg.FileSystem {
fs := gotenberg.NewFileSystem()
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
if err != nil {
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
}
err = os.WriteFile(fmt.Sprintf("%s/document.txt", fs.WorkingDirPath()), []byte("Basic latin characters"), 0o755)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return fs
}(),
filename: "document.txt",
expectSameInputPath: true,
expectError: false,
},
{
scenario: "non-basic latin characters",
fs: func() *gotenberg.FileSystem {
fs := gotenberg.NewFileSystem()
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
if err != nil {
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
}
err = os.WriteFile(fmt.Sprintf("%s/éèßàùä.txt", fs.WorkingDirPath()), []byte("Non-basic latin characters"), 0o755)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return fs
}(),
filename: "éèßàùä.txt",
expectSameInputPath: false,
expectError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
defer func() {
err := os.RemoveAll(tc.fs.WorkingDirPath())
if err != nil {
t.Fatalf("expected no error while cleaning up, but got: %v", err)
}
}()
inputPath := fmt.Sprintf("%s/%s", tc.fs.WorkingDirPath(), tc.filename)
newInputPath, err := nonBasicLatinCharactersGuard(
zap.NewNop(),
inputPath,
)
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if tc.expectError && err == nil {
t.Fatal("expected error but got none")
}
if tc.expectSameInputPath && newInputPath != inputPath {
t.Fatalf("expected same input path, but got '%s'", newInputPath)
}
if !tc.expectSameInputPath && newInputPath == inputPath {
t.Fatalf("expected different input path, but got same '%s'", newInputPath)
}
})
}
}