diff --git a/.gitignore b/.gitignore index 1fb3e993..291b0fd4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ /coverage.html /coverage.txt -/TODO.txt \ No newline at end of file +/TODO.txt +.idea +.vscode +.DS_Store diff --git a/pkg/modules/api/api.go b/pkg/modules/api/api.go index 76fd362f..9320400d 100644 --- a/pkg/modules/api/api.go +++ b/pkg/modules/api/api.go @@ -28,6 +28,8 @@ func init() { // middlewares or health checks. type Api struct { port int + tlsCertFile string + tlsKeyFile string startTimeout time.Duration timeout time.Duration rootPath string @@ -159,6 +161,8 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor { fs := flag.NewFlagSet("api", flag.ExitOnError) fs.Int("api-port", 3000, "Set the port on which the API should listen") fs.String("api-port-from-env", "", "Set the environment variable with the port on which the API should listen - override the default port") + fs.String("api-tls-cert-file", "", "Path to the TLS/SSL certificate file - for HTTPS support") + fs.String("api-tls-key-file", "", "Path to the TLS/SSL key file - for HTTPS support") fs.Duration("api-start-timeout", time.Duration(30)*time.Second, "Set the time limit for the API to start") fs.Duration("api-timeout", time.Duration(30)*time.Second, "Set the time limit for requests") fs.String("api-root-path", "/", "Set the root path of the API - for service discovery via URL paths") @@ -175,6 +179,8 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor { func (a *Api) Provision(ctx *gotenberg.Context) error { flags := ctx.ParsedFlags() a.port = flags.MustInt("api-port") + a.tlsCertFile = flags.MustString("api-tls-cert-file") + a.tlsKeyFile = flags.MustString("api-tls-key-file") a.startTimeout = flags.MustDuration("api-start-timeout") a.timeout = flags.MustDuration("api-timeout") a.rootPath = flags.MustString("api-root-path") @@ -301,6 +307,12 @@ func (a *Api) Validate() error { ) } + if (a.tlsCertFile != "" && a.tlsKeyFile == "") || (a.tlsCertFile == "" && a.tlsKeyFile != "") { + err = multierr.Append(err, + errors.New("tls certificate and key file must both be set"), + ) + } + if !strings.HasPrefix(a.rootPath, "/") { err = multierr.Append(err, errors.New("root path must start with /"), @@ -478,10 +490,19 @@ func (a *Api) Start() error { // As the following code is blocking, run it in a goroutine. go func() { - server := &http2.Server{} - err := a.srv.StartH2CServer(fmt.Sprintf(":%d", a.port), server) - if !errors.Is(err, http.ErrServerClosed) { - a.logger.Fatal(err.Error()) + if a.tlsCertFile != "" && a.tlsKeyFile != "" { + // Start an HTTPS server (supports HTTP/2). + err := a.srv.StartTLS(fmt.Sprintf(":%d", a.port), a.tlsCertFile, a.tlsKeyFile) + if !errors.Is(err, http.ErrServerClosed) { + a.logger.Fatal(err.Error()) + } + } else { + // Start an HTTP/2 Cleartext (non-HTTPS) server. + server := &http2.Server{} + err := a.srv.StartH2CServer(fmt.Sprintf(":%d", a.port), server) + if !errors.Is(err, http.ErrServerClosed) { + a.logger.Fatal(err.Error()) + } } }() diff --git a/pkg/modules/api/api_test.go b/pkg/modules/api/api_test.go index 1e23fd07..6a6e90db 100644 --- a/pkg/modules/api/api_test.go +++ b/pkg/modules/api/api_test.go @@ -462,6 +462,8 @@ func TestApi_Validate(t *testing.T) { for _, tc := range []struct { scenario string port int + tlsCertFile string + tlsKeyFile string rootPath string traceHeader string routes []Route @@ -486,6 +488,26 @@ func TestApi_Validate(t *testing.T) { middlewares: nil, expectError: true, }, + { + scenario: "invalid tls files: only cert file provided", + port: 10, + tlsCertFile: "cert.pem", + rootPath: "/foo/", + traceHeader: "foo", + routes: nil, + middlewares: nil, + expectError: true, + }, + { + scenario: "invalid tls files: only key file provided", + port: 10, + tlsKeyFile: "key.pem", + rootPath: "/foo/", + traceHeader: "foo", + routes: nil, + middlewares: nil, + expectError: true, + }, { scenario: "invalid root path: missing / prefix", port: 10, @@ -647,10 +669,45 @@ func TestApi_Validate(t *testing.T) { }, }, }, + { + scenario: "success with tls", + port: 10, + tlsCertFile: "cert.pem", + tlsKeyFile: "key.pem", + rootPath: "/foo/", + traceHeader: "foo", + routes: []Route{ + { + Method: http.MethodGet, + Path: "/foo", + Handler: func(_ echo.Context) error { return nil }, + }, + { + Method: http.MethodGet, + Path: "/forms/foo", + Handler: func(_ echo.Context) error { return nil }, + IsMultipart: true, + }, + }, + middlewares: []Middleware{ + { + Priority: HighPriority, + Handler: func() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + return next(c) + } + } + }(), + }, + }, + }, } { t.Run(tc.scenario, func(t *testing.T) { mod := Api{ port: tc.port, + tlsCertFile: tc.tlsCertFile, + tlsKeyFile: tc.tlsKeyFile, rootPath: tc.rootPath, traceHeader: tc.traceHeader, routes: tc.routes, @@ -673,6 +730,8 @@ func TestApi_Start(t *testing.T) { for _, tc := range []struct { scenario string readyFn []func() error + tlsCertFile string + tlsKeyFile string expectError bool }{ { @@ -691,10 +750,22 @@ func TestApi_Start(t *testing.T) { }, expectError: false, }, + { + scenario: "success with tls", + readyFn: []func() error{ + func() error { return nil }, + func() error { return nil }, + }, + tlsCertFile: "/tests/test/testdata/api/cert.pem", + tlsKeyFile: "/tests/test/testdata/api/key.pem", + expectError: false, + }, } { t.Run(tc.scenario, func(t *testing.T) { mod := new(Api) mod.port = 3000 + mod.tlsCertFile = tc.tlsCertFile + mod.tlsKeyFile = tc.tlsKeyFile mod.startTimeout = time.Duration(30) * time.Second mod.rootPath = "/" mod.basicAuthUsername = "foo" diff --git a/test/docker-entrypoint.sh b/test/docker-entrypoint.sh index 3ff928a3..4862f516 100755 --- a/test/docker-entrypoint.sh +++ b/test/docker-entrypoint.sh @@ -5,19 +5,19 @@ # Credits: https://github.com/thecodingmachine/docker-images-php. set +e -mkdir testing_file_system_rights.foo +mkdir -p testing_file_system_rights.foo chmod 700 testing_file_system_rights.foo su gotenberg -c "touch testing_file_system_rights.foo/foo > /dev/null 2>&1" HAS_CONSISTENT_RIGHTS=$? if [[ "$HAS_CONSISTENT_RIGHTS" != "0" ]]; then # If not specified, the DOCKER_USER is the owner of the current working directory (heuristic!). - DOCKER_USER=$(stat -c '%U' "$(pwd)") + DOCKER_USER=$(stat -c '%u' "$(pwd)") else # macOs or Windows. # Note: in most cases, we don't care about the rights (they are not respected). - FILE_OWNER=$(stat -c '%U' "testing_file_system_rights.foo/foo") - if [[ "$FILE_OWNER" == "root" ]]; then + FILE_OWNER=$(stat -c '%u' "testing_file_system_rights.foo/foo") + if [[ "$FILE_OWNER" == "0" ]]; then # If root, we are likely on a Windows host. # All files will belong to root, but it does not matter as everybody can write/delete # those (0777 access rights). @@ -56,4 +56,4 @@ go mod tidy set +x # Run the command with the correct user. -exec "sudo" "-E" "-H" "-u" "#$DOCKER_USER_ID" "$@" \ No newline at end of file +exec "sudo" "-E" "-H" "-u" "#$DOCKER_USER_ID" "$@" diff --git a/test/testdata/api/README.md b/test/testdata/api/README.md new file mode 100644 index 00000000..e59b632c --- /dev/null +++ b/test/testdata/api/README.md @@ -0,0 +1,14 @@ +To generate a valid certificate and private key use the following command: + +```bash +# In OpenSSL ≥ 1.1.1 +openssl req -x509 -newkey rsa:4096 -sha256 -days 9999 -nodes \ + -keyout key.pem -out cert.pem -subj "/CN=localhost" \ + -addext "subjectAltName=DNS:localhost,IP:127.0.0.1,IP:::1" +``` + +To check a certificate use the following command: + +```bash +openssl x509 -in cert.pem -text +``` diff --git a/test/testdata/api/cert.pem b/test/testdata/api/cert.pem new file mode 100644 index 00000000..ebb4bb8d --- /dev/null +++ b/test/testdata/api/cert.pem @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFOjCCAyKgAwIBAgIUZaht2QoZwWDK8wEuwm+wmip9sqwwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI0MDUyNjA1MDExNFoYDzIwNTEx +MDExMDUwMTE0WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCm3TNckD55ae6pxXS03iV2P3X/edZT4vRa8D15elsi +CSRf6mdU2TVAQyg22GEcMXgsbba7sotJVg7559LkWAgb3Dj9rbPputdS29ebncat +x1nzo1QHMCS/v7nWoPe9fQVlDEf3ebM198f+5vhccSYFxFa+YvsUmSw9yMF8z1nA +6POZTpm6ErByAY9EGvC8EcNOQd9xLOfBZc0e+mY/rMRxmL+0N+sSRE/ACU+2zxHN +KcCiFX5WQdLDYW1ZjX5xAVatCPNlxSQpj97/ktGsnqwGnJWSTeMUOPv7L0LrQasf +k1VXBvfTW2/ZqhI9WA8qfdC5XOuzVwfU14gv+6vv9AGZnUFmOjeo9XG4xWBxlIKj +AQAzeNwAGFltPkp5a3yPczxMjKY4xLsMGcDfpaNtv1B24+7fV77Nyty/gLLT9pUa +FtXLuRfIPSaUK3kFhAuYzGUL/sO9O9jJkNlxxygxQ+VyIgmdw0zWrUi3q+Lfmdya +FFjHdM7LnLrLCjSv3dEoMINngR3+IejsY+98Ehd25O3oW+ZnGIQX0VwykV8sOQWq +rpnnZ+2iHMtF6Vsj40z/Y9y7yVE18AIJFC+Yr/dD/nQyg6SE70wdNcOx0Ay1xHbP +WuV3fqKOfhONR7NEBSO2UbHi6GZAiYMMwwul+4yKAQ4+lHOEXG4JQcKvtZasxq9v +GwIDAQABo4GBMH8wHQYDVR0OBBYEFPPMaH3C0rAkD/E0gtBZwM0x8N9MMB8GA1Ud +IwQYMBaAFPPMaH3C0rAkD/E0gtBZwM0x8N9MMA8GA1UdEwEB/wQFMAMBAf8wLAYD +VR0RBCUwI4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqG +SIb3DQEBCwUAA4ICAQB0DXz59JfQgfJvRJfraa+Cv/3nG4NNmHYtNGhrid5sagH3 +LM8jkbA+JKRAn34SmsqUZb1f2XFI64TwrScgMfAvfi+5ibRqBjM/YL06bTlG61Ua +t2IQchV2kcX4zQnaMCIYG0dUj6JoXIspiFULMHyGDs7G9BKVcZxlaLrEvPXBqQjd +nwF80KHS/rCConmICU9uWHPgHxS1VmhQiwYGbrakETilau1ppRS0bpH2k/stVyjc +plttIr1DUD6xaRV4mZ0NZVlIXl2nDf78iQvreJeMrGl6AcNHzV+/rGHpY/gOrziP +PQkB1VtSiHtBe/VMKXcVB69SEzsoGEknqs4GPXuVYm47POo3/U5HmGU+FadSATNP +qtorRECUMbVn0IfDzJX2jBLUjZlhPI9gqO9DhiAT8FWHRV2yPsZKuiBBg5/hUvyb +Qsswtmi/YV5e6AnXEuvylwE3wYzlqDzxwlZM08bWFyZYnSf38d3hAjVwUHAugiaX +4sgE6GPAiONW5FkTZ4Z2oXYiN213QDIIcQeFAl04qSEFkGBV8x2hM0uXgFH1S++j +JePdKBbV/CjxegyyYsmnln+eO1kC8pCalB6k6pEtYMqNYOnmimC8UKrJATrH9EVq +eAehGBiIe514Oz06Ws2Vm4PTiY2x0KVbqphwkuz85ohSF9UXOHUcMryusr1zlg== +-----END CERTIFICATE----- diff --git a/test/testdata/api/key.pem b/test/testdata/api/key.pem new file mode 100644 index 00000000..3dc35124 --- /dev/null +++ b/test/testdata/api/key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +lrSOZMPTLX1tN/cWa+Ii+/WALnkI +-----END PRIVATE KEY-----