diff --git a/.ci/gotenberg.yml b/.ci/gotenberg.yml index 30b153e9..2980643c 100644 --- a/.ci/gotenberg.yml +++ b/.ci/gotenberg.yml @@ -17,12 +17,15 @@ commands: # Unlike others commands' templates, you have access to FilesPaths instead of FilePath: it gathers all PDF files which should be merged. merge: template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 conversions: # The command template: you have access to FilePath and ResultFilePath variables. - template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}" + # The binary which will call the command. + interpreter: "/bin/sh -c" # Duration in seconds after which the command will be killed if it has not finished. timeout: 30 # Files with the following extensions will be converted by the current command. @@ -30,12 +33,14 @@ commands: - ".md" - template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".html" - ".htm" - template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\"" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".doc" diff --git a/_tests/configurations/broken-gotenberg.yml b/_tests/configurations/broken-gotenberg.yml index 77201b79..93d01111 100644 --- a/_tests/configurations/broken-gotenberg.yml +++ b/_tests/configurations/broken-gotenberg.yml @@ -8,18 +8,22 @@ logs: commands: merge: template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 conversions: - template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".md" - template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".html" - ".htm" - template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\"" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".doc" diff --git a/_tests/configurations/duplicate-command-gotenberg.yml b/_tests/configurations/duplicate-command-gotenberg.yml index ed0f2a65..2b9863c7 100644 --- a/_tests/configurations/duplicate-command-gotenberg.yml +++ b/_tests/configurations/duplicate-command-gotenberg.yml @@ -5,22 +5,27 @@ logs: commands: merge: template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 conversions: - template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".md" - template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".md" - template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".html" - ".htm" - template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\"" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".doc" diff --git a/_tests/configurations/gotenberg.yml b/_tests/configurations/gotenberg.yml index 37d416ac..66b4024b 100644 --- a/_tests/configurations/gotenberg.yml +++ b/_tests/configurations/gotenberg.yml @@ -5,18 +5,22 @@ logs: commands: merge: template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 conversions: - template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".md" - template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".html" - ".htm" - template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\"" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".doc" diff --git a/_tests/configurations/merge-timeout-gotenberg.yml b/_tests/configurations/merge-timeout-gotenberg.yml index 86204982..94c42ccb 100644 --- a/_tests/configurations/merge-timeout-gotenberg.yml +++ b/_tests/configurations/merge-timeout-gotenberg.yml @@ -5,18 +5,22 @@ logs: commands: merge: template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 0 conversions: - template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".md" - template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".html" - ".htm" - template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\"" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".doc" diff --git a/_tests/configurations/timeout-gotenberg.yml b/_tests/configurations/timeout-gotenberg.yml index dfc7e81a..a9668d3a 100644 --- a/_tests/configurations/timeout-gotenberg.yml +++ b/_tests/configurations/timeout-gotenberg.yml @@ -5,18 +5,22 @@ logs: commands: merge: template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 0 conversions: - template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 0 extensions: - ".md" - template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 0 extensions: - ".html" - ".htm" - template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\"" + interpreter: "/bin/sh -c" timeout: 0 extensions: - ".doc" diff --git a/_tests/configurations/wrong-command-template-gotenberg.yml b/_tests/configurations/wrong-command-template-gotenberg.yml index 383d4507..65cbe447 100644 --- a/_tests/configurations/wrong-command-template-gotenberg.yml +++ b/_tests/configurations/wrong-command-template-gotenberg.yml @@ -5,18 +5,22 @@ logs: commands: merge: template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 conversions: - template: "markdown-pdf {{ FilePath }} -o {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".md" - template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".html" - ".htm" - template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\"" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".doc" diff --git a/_tests/configurations/wrong-logging-formatter-gotenberg.yml b/_tests/configurations/wrong-logging-formatter-gotenberg.yml index 2a09f9d5..9bcea896 100644 --- a/_tests/configurations/wrong-logging-formatter-gotenberg.yml +++ b/_tests/configurations/wrong-logging-formatter-gotenberg.yml @@ -5,18 +5,22 @@ logs: commands: merge: template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 conversions: - template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".md" - template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".html" - ".htm" - template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\"" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".doc" diff --git a/_tests/configurations/wrong-logging-level-gotenberg.yml b/_tests/configurations/wrong-logging-level-gotenberg.yml index 1372cd53..68065d53 100644 --- a/_tests/configurations/wrong-logging-level-gotenberg.yml +++ b/_tests/configurations/wrong-logging-level-gotenberg.yml @@ -5,18 +5,22 @@ logs: commands: merge: template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 conversions: - template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".md" - template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".html" - ".htm" - template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\"" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".doc" diff --git a/_tests/configurations/wrong-merge-command-template-gotenberg.yml b/_tests/configurations/wrong-merge-command-template-gotenberg.yml index 987ab88f..c5118c47 100644 --- a/_tests/configurations/wrong-merge-command-template-gotenberg.yml +++ b/_tests/configurations/wrong-merge-command-template-gotenberg.yml @@ -5,18 +5,22 @@ logs: commands: merge: template: "pdftk {{ range $filePath := FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 conversions: - template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".md" - template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".html" - ".htm" - template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\"" + interpreter: "/bin/sh -c" timeout: 30 extensions: - ".doc" diff --git a/app/config/config.go b/app/config/config.go index c82455ba..4081ae12 100644 --- a/app/config/config.go +++ b/app/config/config.go @@ -8,6 +8,7 @@ package config import ( "fmt" + "strings" "text/template" "github.com/sirupsen/logrus" @@ -29,6 +30,8 @@ type ( Command struct { // Template is the data-driven template of the command. Template *template.Template + // The binary which will call the command. + Interpreter []string // Timeout is the duration in seconds after which the command's process will be killed // if it does not finish before. Timeout int @@ -123,15 +126,29 @@ func GetLogsFormatter() logrus.Formatter { return config.logsFormatter } +type interpreterEmptyError struct { + command string +} + +const interpreterEmptyErrorMessage = "the interepreter for command %s should not be empty" + +func (e *interpreterEmptyError) Error() string { + return fmt.Sprintf(interpreterEmptyErrorMessage, e.command) +} + // NewCommand instantiates a Command. If the given command string // is not a valid template, throws an error. -func NewCommand(command string, timeout int) (*Command, error) { +func NewCommand(command string, interpreter string, timeout int) (*Command, error) { t, err := template.New(command).Parse(command) if err != nil { return nil, err } - return &Command{t, timeout}, nil + if interpreter == "" { + return nil, &interpreterEmptyError{command} + } + + return &Command{t, strings.Fields(interpreter), timeout}, nil } type fileExtensionAlreadyUsedError struct { @@ -140,7 +157,7 @@ type fileExtensionAlreadyUsedError struct { existingCommand *Command } -const fileExtensionAlreadyUsedErrorMessage = "file extension '%s' from command '%s' is already used by command '%s'" +const fileExtensionAlreadyUsedErrorMessage = "file extension %s from command %s is already used by command %s" func (e *fileExtensionAlreadyUsedError) Error() string { return fmt.Sprintf(fileExtensionAlreadyUsedErrorMessage, e.extension, e.command.Template.Name(), e.existingCommand.Template.Name()) @@ -167,7 +184,7 @@ type noCommandFoundForFileExtensionError struct { extension string } -const noCommandFoundForFileExtensionErrorMessage = "no command found for file extension '%s'" +const noCommandFoundForFileExtensionErrorMessage = "no command found for file extension %s" func (e *noCommandFoundForFileExtensionError) Error() string { return fmt.Sprintf(noCommandFoundForFileExtensionErrorMessage, e.extension) diff --git a/app/config/config_test.go b/app/config/config_test.go index ff34726c..243ec996 100644 --- a/app/config/config_test.go +++ b/app/config/config_test.go @@ -99,26 +99,39 @@ func TestGetLogsFormatter(t *testing.T) { } } +func TestInterpreterEmptyError(t *testing.T) { + err := &interpreterEmptyError{"echo hello world"} + expected := fmt.Sprintf(interpreterEmptyErrorMessage, err.command) + if err.Error() != expected { + t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), expected) + } +} + func TestNewCommand(t *testing.T) { var cmd string // case 1: uses a wrong command template. cmd = "pdftk {{ range $filePath := FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}" - if _, err := NewCommand(cmd, 0); err == nil { + if _, err := NewCommand(cmd, "/bin/sh -c", 0); err == nil { t.Errorf("Command should not have been instantiated by using '%s' as command template", cmd) } // case 2: uses a correct command template. cmd = "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}" - if _, err := NewCommand(cmd, 0); err != nil { + if _, err := NewCommand(cmd, "/bin/sh -c", 0); err != nil { t.Errorf("Command should have been instantiated by using '%s' as command template", cmd) } + + // case 3: uses an empty interpreter. + if _, err := NewCommand(cmd, "", 0); err == nil { + t.Error("Command should not have been instantiated by using an empty interpreter") + } } func TestFileExtensionAlreadyUsedError(t *testing.T) { ext := ".pdf" - cmd1, _ := NewCommand("echo", 0) - cmd2, _ := NewCommand("echo", 0) + cmd1, _ := NewCommand("echo", "/bin/sh -c", 0) + cmd2, _ := NewCommand("echo", "/bin/sh -c", 0) err := &fileExtensionAlreadyUsedError{ext, cmd1, cmd2} expected := fmt.Sprintf(fileExtensionAlreadyUsedErrorMessage, err.extension, err.command.Template.Name(), err.existingCommand.Template.Name()) @@ -129,7 +142,7 @@ func TestFileExtensionAlreadyUsedError(t *testing.T) { func TestWithCommand(t *testing.T) { ext := ".pdf" - cmd, _ := NewCommand("echo", 0) + cmd, _ := NewCommand("echo", "/bin/sh -c", 0) // case 1: uses a command with a file extension not already referenced. if err := WithCommand(ext, cmd); err != nil { @@ -154,7 +167,7 @@ func TestNoCommandFoundForFileExtensionError(t *testing.T) { func TestGetCommand(t *testing.T) { Reset() ext := ".pdf" - cmd, _ := NewCommand("echo", 0) + cmd, _ := NewCommand("echo", "/bin/sh -c", 0) WithCommand(ext, cmd) // case 1: uses a file extension which has a command associated. diff --git a/app/config/parser.go b/app/config/parser.go index 82089a41..6bd1e0dc 100644 --- a/app/config/parser.go +++ b/app/config/parser.go @@ -24,7 +24,7 @@ func ParseFile(configurationFilePath string) error { } // handles merge command first... - cmd, err := NewCommand(fileConfig.Commands.Merge.Template, fileConfig.Commands.Merge.Timeout) + cmd, err := NewCommand(fileConfig.Commands.Merge.Template, fileConfig.Commands.Merge.Interpreter, fileConfig.Commands.Merge.Timeout) if err != nil { return err } @@ -33,7 +33,7 @@ func ParseFile(configurationFilePath string) error { // ...then conversion commands! for _, command := range fileConfig.Commands.Conversions { - cmd, err := NewCommand(command.Template, command.Timeout) + cmd, err := NewCommand(command.Template, command.Interpreter, command.Timeout) if err != nil { return err } @@ -64,15 +64,17 @@ type ( // mergeCommand gathers all data regarding the... merge command. mergeCommand struct { - Template string `yaml:"template"` - Timeout int `yaml:"timeout"` + Template string `yaml:"template"` + Interpreter string `yaml:"interpreter"` + Timeout int `yaml:"timeout"` } // conversionCommand gathers all data regarding a conversion command. conversionCommand struct { - Template string `yaml:"template"` - Timeout int `yaml:"timeout"` - Extensions []string `yaml:"extensions"` + Template string `yaml:"template"` + Interpreter string `yaml:"interpreter"` + Timeout int `yaml:"timeout"` + Extensions []string `yaml:"extensions"` } ) diff --git a/app/converter/process/process.go b/app/converter/process/process.go index 290b3373..41cf1848 100644 --- a/app/converter/process/process.go +++ b/app/converter/process/process.go @@ -24,7 +24,7 @@ type commandTimeoutError struct { timeout int } -const commandTimeoutErrorMessage = "the command '%s' has reached the %d second(s) timeout" +const commandTimeoutErrorMessage = "the command %s has reached the %d second(s) timeout" func (e *commandTimeoutError) Error() string { return fmt.Sprintf(commandTimeoutErrorMessage, e.command, e.timeout) @@ -32,11 +32,13 @@ func (e *commandTimeoutError) Error() string { // run runs the given command. If timeout is reached or // something bad happened, returns an error. -func (r *runner) run(command string, timeout int) error { +func (r *runner) run(command string, interpreter []string, timeout int) error { r.mu.Lock() defer r.mu.Unlock() - cmd := exec.Command("/bin/sh", "-c", command) + binary := interpreter[0] + parameters := append(interpreter[1:], command) + cmd := exec.Command(binary, parameters...) logger.Debugf("executing command %s", cmd.Args) if err := cmd.Start(); err != nil { @@ -85,7 +87,7 @@ func Unconv(workingDir string, file *gfile.File) (string, error) { return "", err } - err = forest.run(data.String(), cmd.Timeout) + err = forest.run(data.String(), cmd.Interpreter, cmd.Timeout) if err != nil { return "", err } @@ -114,7 +116,7 @@ func Merge(workingDir string, filesPaths []string) (string, error) { return "", err } - err = forest.run(data.String(), cmd.Timeout) + err = forest.run(data.String(), cmd.Interpreter, cmd.Timeout) if err != nil { return "", err } diff --git a/app/converter/process/process_test.go b/app/converter/process/process_test.go index 6824d11e..fa406a8d 100644 --- a/app/converter/process/process_test.go +++ b/app/converter/process/process_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "testing" "github.com/thecodingmachine/gotenberg/app/config" @@ -42,19 +43,19 @@ func TestRun(t *testing.T) { // case 1: uses a simple command. cmd = "echo Hello world" - if err := forest.run(cmd, 30); err != nil { + if err := forest.run(cmd, strings.Fields("/bin/sh -c"), 30); err != nil { t.Errorf("Command '%s' should have worked", cmd) } // case 2: uses a simple command but with an unsuitable timeout. cmd = "sleep 5" - if err := forest.run(cmd, 0); err == nil { + if err := forest.run(cmd, strings.Fields("/bin/sh -c"), 0); err == nil { t.Errorf("Command '%s' should not have worked", cmd) } // case 3: uses a broken command. cmd = "helloworld" - if err := forest.run(cmd, 30); err == nil { + if err := forest.run(cmd, strings.Fields("/bin/sh -c"), 30); err == nil { t.Errorf("Command '%s' should not have worked", cmd) } }