mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-17 20:52:14 +01:00
feat(api): add basic auth support
This commit is contained in:
6
Makefile
6
Makefile
@@ -33,6 +33,9 @@ API_START_TIMEOUT=30s
|
|||||||
API_TIMEOUT=30s
|
API_TIMEOUT=30s
|
||||||
API_ROOT_PATH=/
|
API_ROOT_PATH=/
|
||||||
API_TRACE_HEADER=Gotenberg-Trace
|
API_TRACE_HEADER=Gotenberg-Trace
|
||||||
|
API_ENABLE_BASIC_AUTH=false
|
||||||
|
GOTENBERG_API_BASIC_AUTH_USERNAME=
|
||||||
|
GOTENBERG_API_BASIC_AUTH_PASSWORD=
|
||||||
API_DISABLE_HEALTH_CHECK_LOGGING=false
|
API_DISABLE_HEALTH_CHECK_LOGGING=false
|
||||||
CHROMIUM_RESTART_AFTER=0
|
CHROMIUM_RESTART_AFTER=0
|
||||||
CHROMIUM_MAX_QUEUE_SIZE=0
|
CHROMIUM_MAX_QUEUE_SIZE=0
|
||||||
@@ -79,6 +82,8 @@ WEBHOOK_DISABLE=false
|
|||||||
run: ## Start a Gotenberg container
|
run: ## Start a Gotenberg container
|
||||||
docker run --rm -it \
|
docker run --rm -it \
|
||||||
-p $(API_PORT):$(API_PORT) \
|
-p $(API_PORT):$(API_PORT) \
|
||||||
|
-e GOTENBERG_API_BASIC_AUTH_USERNAME=$(GOTENBERG_API_BASIC_AUTH_USERNAME) \
|
||||||
|
-e GOTENBERG_API_BASIC_AUTH_PASSWORD=$(GOTENBERG_API_BASIC_AUTH_PASSWORD) \
|
||||||
$(DOCKER_REPOSITORY)/gotenberg:$(GOTENBERG_VERSION) \
|
$(DOCKER_REPOSITORY)/gotenberg:$(GOTENBERG_VERSION) \
|
||||||
gotenberg \
|
gotenberg \
|
||||||
--gotenberg-graceful-shutdown-duration=$(GOTENBERG_GRACEFUL_SHUTDOWN_DURATION) \
|
--gotenberg-graceful-shutdown-duration=$(GOTENBERG_GRACEFUL_SHUTDOWN_DURATION) \
|
||||||
@@ -88,6 +93,7 @@ run: ## Start a Gotenberg container
|
|||||||
--api-timeout=$(API_TIMEOUT) \
|
--api-timeout=$(API_TIMEOUT) \
|
||||||
--api-root-path=$(API_ROOT_PATH) \
|
--api-root-path=$(API_ROOT_PATH) \
|
||||||
--api-trace-header=$(API_TRACE_HEADER) \
|
--api-trace-header=$(API_TRACE_HEADER) \
|
||||||
|
--api-enable-basic-auth=$(API_ENABLE_BASIC_AUTH) \
|
||||||
--api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \
|
--api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \
|
||||||
--chromium-restart-after=$(CHROMIUM_RESTART_AFTER) \
|
--chromium-restart-after=$(CHROMIUM_RESTART_AFTER) \
|
||||||
--chromium-auto-start=$(CHROMIUM_AUTO_START) \
|
--chromium-auto-start=$(CHROMIUM_AUTO_START) \
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -48,6 +48,7 @@ require (
|
|||||||
github.com/gobwas/httphead v0.1.0 // indirect
|
github.com/gobwas/httphead v0.1.0 // indirect
|
||||||
github.com/gobwas/pool v0.2.1 // indirect
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
github.com/gobwas/ws v1.3.2 // indirect
|
github.com/gobwas/ws v1.3.2 // indirect
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
github.com/gorilla/css v1.0.1 // indirect
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
github.com/hhrutter/lzw v1.0.0 // indirect
|
github.com/hhrutter/lzw v1.0.0 // indirect
|
||||||
github.com/hhrutter/tiff v1.0.1 // indirect
|
github.com/hhrutter/tiff v1.0.1 // indirect
|
||||||
@@ -63,6 +64,7 @@ require (
|
|||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||||
|
golang.org/x/time v0.5.0 // indirect
|
||||||
google.golang.org/protobuf v1.33.0 // indirect
|
google.golang.org/protobuf v1.33.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -32,6 +32,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
|||||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q=
|
github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q=
|
||||||
github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
@@ -156,6 +158,8 @@ golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
|
|||||||
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
|
|||||||
35
pkg/gotenberg/env.go
Normal file
35
pkg/gotenberg/env.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StringEnv retrieves the value of the environment variable named by the key.
|
||||||
|
// If the variable is present in the environment and not empty, the value is
|
||||||
|
// returned.
|
||||||
|
func StringEnv(key string) (string, error) {
|
||||||
|
val, ok := os.LookupEnv(key)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("environment variable '%s' does not exist", key)
|
||||||
|
}
|
||||||
|
if val == "" {
|
||||||
|
return "", fmt.Errorf("environment variable '%s' is empty", key)
|
||||||
|
}
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntEnv relies on [StringEnv] and converts the values if it exists and is not
|
||||||
|
// empty.
|
||||||
|
func IntEnv(key string) (int, error) {
|
||||||
|
val, err := StringEnv(key)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
intVal, err := strconv.Atoi(val)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("get int value of environment variable '%s': %w", key, err)
|
||||||
|
}
|
||||||
|
return intVal, nil
|
||||||
|
}
|
||||||
134
pkg/gotenberg/env_test.go
Normal file
134
pkg/gotenberg/env_test.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStringEnv(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
key string
|
||||||
|
setEnv func()
|
||||||
|
expectVal string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "non-existing environment variable",
|
||||||
|
key: "NON_EXISTING",
|
||||||
|
expectVal: "",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "empty environment variable",
|
||||||
|
key: "EMPTY_STRING",
|
||||||
|
setEnv: func() {
|
||||||
|
err := os.Setenv("EMPTY_STRING", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectVal: "",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success",
|
||||||
|
key: "EXISTING_STRING_VALUE",
|
||||||
|
setEnv: func() {
|
||||||
|
err := os.Setenv("EXISTING_STRING_VALUE", "foo")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectVal: "foo",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
if tc.setEnv != nil {
|
||||||
|
tc.setEnv()
|
||||||
|
}
|
||||||
|
|
||||||
|
val, err := StringEnv(tc.key)
|
||||||
|
|
||||||
|
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.expectVal != val {
|
||||||
|
t.Errorf("expected value '%s' but got '%s'", tc.expectVal, val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntEnv(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
key string
|
||||||
|
setEnv func()
|
||||||
|
expectVal int
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "empty environment variable",
|
||||||
|
key: "EMPTY_INT",
|
||||||
|
setEnv: func() {
|
||||||
|
err := os.Setenv("EMPTY_INT", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectVal: 0,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "non-integer value",
|
||||||
|
key: "NON_INTEGER",
|
||||||
|
setEnv: func() {
|
||||||
|
err := os.Setenv("NON_INTEGER", "foo")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectVal: 0,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success",
|
||||||
|
key: "EXISTING_INT_VALUE",
|
||||||
|
setEnv: func() {
|
||||||
|
err := os.Setenv("EXISTING_INT_VALUE", "123")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectVal: 123,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
if tc.setEnv != nil {
|
||||||
|
tc.setEnv()
|
||||||
|
}
|
||||||
|
|
||||||
|
val, err := IntEnv(tc.key)
|
||||||
|
|
||||||
|
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.expectVal != val {
|
||||||
|
t.Errorf("expected value %d but got %d", tc.expectVal, val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,9 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -34,6 +32,8 @@ type Api struct {
|
|||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
rootPath string
|
rootPath string
|
||||||
traceHeader string
|
traceHeader string
|
||||||
|
basicAuthUsername string
|
||||||
|
basicAuthPassword string
|
||||||
disableHealthCheckLogging bool
|
disableHealthCheckLogging bool
|
||||||
|
|
||||||
routes []Route
|
routes []Route
|
||||||
@@ -163,8 +163,8 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
|
|||||||
fs.Duration("api-timeout", time.Duration(30)*time.Second, "Set the time limit for requests")
|
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")
|
fs.String("api-root-path", "/", "Set the root path of the API - for service discovery via URL paths")
|
||||||
fs.String("api-trace-header", "Gotenberg-Trace", "Set the header name to use for identifying requests")
|
fs.String("api-trace-header", "Gotenberg-Trace", "Set the header name to use for identifying requests")
|
||||||
|
fs.Bool("api-enable-basic-auth", false, "Enable basic authentication - will look for the GOTENBERG_API_BASIC_AUTH_USERNAME and GOTENBERG_API_BASIC_AUTH_PASSWORD environment variables")
|
||||||
fs.Bool("api-disable-health-check-logging", false, "Disable health check logging")
|
fs.Bool("api-disable-health-check-logging", false, "Disable health check logging")
|
||||||
|
|
||||||
return fs
|
return fs
|
||||||
}(),
|
}(),
|
||||||
New: func() gotenberg.Module { return new(Api) },
|
New: func() gotenberg.Module { return new(Api) },
|
||||||
@@ -184,24 +184,28 @@ func (a *Api) Provision(ctx *gotenberg.Context) error {
|
|||||||
// Port from env?
|
// Port from env?
|
||||||
portEnvVar := flags.MustString("api-port-from-env")
|
portEnvVar := flags.MustString("api-port-from-env")
|
||||||
if portEnvVar != "" {
|
if portEnvVar != "" {
|
||||||
val, ok := os.LookupEnv(portEnvVar)
|
port, err := gotenberg.IntEnv(portEnvVar)
|
||||||
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("environment variable '%s' does not exist", portEnvVar)
|
|
||||||
}
|
|
||||||
|
|
||||||
if val == "" {
|
|
||||||
return fmt.Errorf("environment variable '%s' is empty", portEnvVar)
|
|
||||||
}
|
|
||||||
|
|
||||||
port, err := strconv.Atoi(val)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get int value of environment variable '%s': %w", portEnvVar, err)
|
return fmt.Errorf("get API port from env: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
a.port = port
|
a.port = port
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enable basic auth?
|
||||||
|
enableBasicAuth := flags.MustBool("api-enable-basic-auth")
|
||||||
|
if enableBasicAuth {
|
||||||
|
basicAuthUsername, err := gotenberg.StringEnv("GOTENBERG_API_BASIC_AUTH_USERNAME")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get basic auth username from env: %w", err)
|
||||||
|
}
|
||||||
|
basicAuthPassword, err := gotenberg.StringEnv("GOTENBERG_API_BASIC_AUTH_PASSWORD")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get basic auth password from env: %w", err)
|
||||||
|
}
|
||||||
|
a.basicAuthUsername = basicAuthUsername
|
||||||
|
a.basicAuthPassword = basicAuthPassword
|
||||||
|
}
|
||||||
|
|
||||||
// Get routes from modules.
|
// Get routes from modules.
|
||||||
mods, err := ctx.Modules(new(Router))
|
mods, err := ctx.Modules(new(Router))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -394,6 +398,13 @@ func (a *Api) Start() error {
|
|||||||
loggerMiddleware(a.logger, disableLoggingForPaths),
|
loggerMiddleware(a.logger, disableLoggingForPaths),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Basic auth?
|
||||||
|
if a.basicAuthUsername != "" {
|
||||||
|
a.srv.Pre(
|
||||||
|
basicAuthMiddleware(a.basicAuthUsername, a.basicAuthPassword),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Add the modules' middlewares in their respective stacks.
|
// Add the modules' middlewares in their respective stacks.
|
||||||
var externalMultipartMiddlewares []Middleware
|
var externalMultipartMiddlewares []Middleware
|
||||||
for _, externalMiddleware := range a.externalMiddlewares {
|
for _, externalMiddleware := range a.externalMiddlewares {
|
||||||
|
|||||||
@@ -58,10 +58,28 @@ func TestApi_Provision(t *testing.T) {
|
|||||||
expectError: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "port from env: empty environment variable",
|
scenario: "basic auth: non-existing GOTENBERG_API_BASIC_AUTH_USERNAME environment variable",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
fs := new(Api).Descriptor().FlagSet
|
fs := new(Api).Descriptor().FlagSet
|
||||||
err := fs.Parse([]string{"--api-port-from-env=PORT"})
|
err := fs.Parse([]string{"--api-enable-basic-auth=true"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return gotenberg.NewContext(
|
||||||
|
gotenberg.ParsedFlags{
|
||||||
|
FlagSet: fs,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "basic auth: non-existing GOTENBERG_API_BASIC_AUTH_PASSWORD environment variable",
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
fs := new(Api).Descriptor().FlagSet
|
||||||
|
err := fs.Parse([]string{"--api-enable-basic-auth=true"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
@@ -74,7 +92,7 @@ func TestApi_Provision(t *testing.T) {
|
|||||||
)
|
)
|
||||||
}(),
|
}(),
|
||||||
setEnv: func() {
|
setEnv: func() {
|
||||||
err := os.Setenv("PORT", "")
|
err := os.Setenv("GOTENBERG_API_BASIC_AUTH_USERNAME", "foo")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
@@ -361,7 +379,7 @@ func TestApi_Provision(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fs := new(Api).Descriptor().FlagSet
|
fs := new(Api).Descriptor().FlagSet
|
||||||
err := fs.Parse([]string{"--api-port-from-env=PORT"})
|
err := fs.Parse([]string{"--api-port-from-env=PORT", "--api-enable-basic-auth=true"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
@@ -383,6 +401,14 @@ func TestApi_Provision(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
err = os.Setenv("GOTENBERG_API_BASIC_AUTH_USERNAME", "foo")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
err = os.Setenv("GOTENBERG_API_BASIC_AUTH_PASSWORD", "bar")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
expectPort: 1337,
|
expectPort: 1337,
|
||||||
expectMiddlewares: []Middleware{
|
expectMiddlewares: []Middleware{
|
||||||
@@ -671,6 +697,8 @@ func TestApi_Start(t *testing.T) {
|
|||||||
mod.port = 3000
|
mod.port = 3000
|
||||||
mod.startTimeout = time.Duration(30) * time.Second
|
mod.startTimeout = time.Duration(30) * time.Second
|
||||||
mod.rootPath = "/"
|
mod.rootPath = "/"
|
||||||
|
mod.basicAuthUsername = "foo"
|
||||||
|
mod.basicAuthPassword = "bar"
|
||||||
mod.disableHealthCheckLogging = true
|
mod.disableHealthCheckLogging = true
|
||||||
mod.routes = []Route{
|
mod.routes = []Route{
|
||||||
{
|
{
|
||||||
@@ -755,6 +783,7 @@ func TestApi_Start(t *testing.T) {
|
|||||||
// health request.
|
// health request.
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
healthRequest := httptest.NewRequest(http.MethodGet, "/health", nil)
|
healthRequest := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||||
|
healthRequest.SetBasicAuth(mod.basicAuthUsername, mod.basicAuthPassword)
|
||||||
|
|
||||||
mod.srv.ServeHTTP(recorder, healthRequest)
|
mod.srv.ServeHTTP(recorder, healthRequest)
|
||||||
if recorder.Code != http.StatusOK {
|
if recorder.Code != http.StatusOK {
|
||||||
@@ -791,6 +820,7 @@ func TestApi_Start(t *testing.T) {
|
|||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, url, body)
|
req := httptest.NewRequest(http.MethodPost, url, body)
|
||||||
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
||||||
|
req.SetBasicAuth(mod.basicAuthUsername, mod.basicAuthPassword)
|
||||||
|
|
||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/subtle"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -10,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/labstack/echo/v4/middleware"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
@@ -112,7 +114,6 @@ func rootPathMiddleware(rootPath string) echo.MiddlewareFunc {
|
|||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
c.Set("rootPath", rootPath)
|
c.Set("rootPath", rootPath)
|
||||||
|
|
||||||
// Call the next middleware in the chain.
|
// Call the next middleware in the chain.
|
||||||
return next(c)
|
return next(c)
|
||||||
}
|
}
|
||||||
@@ -217,6 +218,17 @@ func loggerMiddleware(logger *zap.Logger, disableLoggingForPaths []string) echo.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// basicAuthMiddleware manages basic authentication.
|
||||||
|
func basicAuthMiddleware(username, password string) echo.MiddlewareFunc {
|
||||||
|
return middleware.BasicAuth(func(u string, p string, e echo.Context) (bool, error) {
|
||||||
|
if subtle.ConstantTimeCompare([]byte(u), []byte(username)) == 1 &&
|
||||||
|
subtle.ConstantTimeCompare([]byte(p), []byte(password)) == 1 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// contextMiddleware, a middleware for "multipart/form-data" requests, sets the
|
// contextMiddleware, a middleware for "multipart/form-data" requests, sets the
|
||||||
// [Context] and related context.CancelFunc in the [echo.Context] under
|
// [Context] and related context.CancelFunc in the [echo.Context] under
|
||||||
// "context" and "cancel". If the process is synchronous, it also handles the
|
// "context" and "cancel". If the process is synchronous, it also handles the
|
||||||
|
|||||||
@@ -236,6 +236,56 @@ func TestTraceMiddleware(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBasicAuthMiddleware(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
request *http.Request
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "invalid basic auth",
|
||||||
|
request: func() *http.Request {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.SetBasicAuth("invalid", "invalid")
|
||||||
|
return req
|
||||||
|
}(),
|
||||||
|
username: "foo",
|
||||||
|
password: "bar",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "valid basic auth",
|
||||||
|
request: func() *http.Request {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.SetBasicAuth("foo", "bar")
|
||||||
|
return req
|
||||||
|
}(),
|
||||||
|
username: "foo",
|
||||||
|
password: "bar",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
srv := echo.New()
|
||||||
|
srv.HideBanner = true
|
||||||
|
srv.HidePort = true
|
||||||
|
c := srv.NewContext(tc.request, recorder)
|
||||||
|
err := basicAuthMiddleware(tc.username, tc.password)(func(c echo.Context) error {
|
||||||
|
return nil
|
||||||
|
})(c)
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoggerMiddleware(t *testing.T) {
|
func TestLoggerMiddleware(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for i, tc := range []struct {
|
||||||
request *http.Request
|
request *http.Request
|
||||||
|
|||||||
Reference in New Issue
Block a user