feat(api): add flag --api-bind-ip

This commit is contained in:
Julien Neuhart
2024-10-08 16:23:12 +02:00
parent bf205c579d
commit 119bba4f04
3 changed files with 100 additions and 38 deletions

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"net"
"net/http"
"sort"
"strings"
@@ -29,6 +30,7 @@ func init() {
// middlewares or health checks.
type Api struct {
port int
bindIp string
tlsCertFile string
tlsKeyFile string
startTimeout time.Duration
@@ -171,6 +173,7 @@ 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-bind-ip", "", "Set the IP address the API should bind to for incoming connections")
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")
@@ -194,6 +197,7 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
func (a *Api) Provision(ctx *gotenberg.Context) error {
flags := ctx.ParsedFlags()
a.port = flags.MustInt("api-port")
a.bindIp = flags.MustString("api-bind-ip")
a.tlsCertFile = flags.MustString("api-tls-cert-file")
a.tlsKeyFile = flags.MustString("api-tls-key-file")
a.startTimeout = flags.MustDuration("api-start-timeout")
@@ -329,6 +333,10 @@ func (a *Api) Validate() error {
)
}
if a.bindIp != "" && net.ParseIP(a.bindIp) == nil {
err = multierr.Append(err, errors.New("IP must be a valid IP address"))
}
if (a.tlsCertFile != "" && a.tlsKeyFile == "") || (a.tlsCertFile == "" && a.tlsKeyFile != "") {
err = multierr.Append(err,
errors.New("both TLS certificate and key files must be set"),
@@ -522,11 +530,11 @@ func (a *Api) Start() error {
var 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)
err = a.srv.StartTLS(fmt.Sprintf("%s:%d", a.bindIp, a.port), a.tlsCertFile, a.tlsKeyFile)
} else {
// Start an HTTP/2 Cleartext (non-HTTPS) server.
server := &http2.Server{}
err = a.srv.StartH2CServer(fmt.Sprintf(":%d", a.port), server)
err = a.srv.StartH2CServer(fmt.Sprintf("%s:%d", a.bindIp, a.port), server)
}
if !errors.Is(err, http.ErrServerClosed) {
a.logger.Fatal(err.Error())
@@ -538,7 +546,11 @@ func (a *Api) Start() error {
// StartupMessage returns a custom startup message.
func (a *Api) StartupMessage() string {
return fmt.Sprintf("server listening on port %d", a.port)
ip := a.bindIp
if a.bindIp == "" {
ip = "[::]"
}
return fmt.Sprintf("server started on %s:%d", ip, a.port)
}
// Stop stops the HTTP server.

View File

@@ -57,6 +57,30 @@ func TestApi_Provision(t *testing.T) {
}(),
expectError: true,
},
{
scenario: "port from env: invalid environment variable value",
ctx: func() *gotenberg.Context {
fs := new(Api).Descriptor().FlagSet
err := fs.Parse([]string{"--api-port-from-env=PORT"})
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return gotenberg.NewContext(
gotenberg.ParsedFlags{
FlagSet: fs,
},
nil,
)
}(),
setEnv: func() {
err := os.Setenv("PORT", "foo")
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
},
expectError: true,
},
{
scenario: "basic auth: non-existing GOTENBERG_API_BASIC_AUTH_USERNAME environment variable",
ctx: func() *gotenberg.Context {
@@ -99,30 +123,6 @@ func TestApi_Provision(t *testing.T) {
},
expectError: true,
},
{
scenario: "port from env: invalid environment variable value",
ctx: func() *gotenberg.Context {
fs := new(Api).Descriptor().FlagSet
err := fs.Parse([]string{"--api-port-from-env=PORT"})
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return gotenberg.NewContext(
gotenberg.ParsedFlags{
FlagSet: fs,
},
nil,
)
}(),
setEnv: func() {
err := os.Setenv("PORT", "foo")
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
},
expectError: true,
},
{
scenario: "no valid routers",
ctx: func() *gotenberg.Context {
@@ -462,6 +462,7 @@ func TestApi_Validate(t *testing.T) {
for _, tc := range []struct {
scenario string
port int
bindIp string
tlsCertFile string
tlsKeyFile string
rootPath string
@@ -473,6 +474,7 @@ func TestApi_Validate(t *testing.T) {
{
scenario: "invalid port (< 1)",
port: 0,
bindIp: "127.0.0.1",
rootPath: "/foo/",
traceHeader: "foo",
routes: nil,
@@ -482,6 +484,17 @@ func TestApi_Validate(t *testing.T) {
{
scenario: "invalid port (> 65535)",
port: 65536,
bindIp: "127.0.0.1",
rootPath: "/foo/",
traceHeader: "foo",
routes: nil,
middlewares: nil,
expectError: true,
},
{
scenario: "invalid IP",
port: 10,
bindIp: "foo",
rootPath: "/foo/",
traceHeader: "foo",
routes: nil,
@@ -491,6 +504,7 @@ func TestApi_Validate(t *testing.T) {
{
scenario: "invalid TLS files: only cert file provided",
port: 10,
bindIp: "127.0.0.1",
tlsCertFile: "cert.pem",
rootPath: "/foo/",
traceHeader: "foo",
@@ -501,6 +515,7 @@ func TestApi_Validate(t *testing.T) {
{
scenario: "invalid TLS files: only key file provided",
port: 10,
bindIp: "127.0.0.1",
tlsKeyFile: "key.pem",
rootPath: "/foo/",
traceHeader: "foo",
@@ -511,6 +526,7 @@ func TestApi_Validate(t *testing.T) {
{
scenario: "invalid root path: missing / prefix",
port: 10,
bindIp: "127.0.0.1",
rootPath: "foo/",
traceHeader: "foo",
routes: nil,
@@ -520,6 +536,7 @@ func TestApi_Validate(t *testing.T) {
{
scenario: "invalid root path: missing / suffix",
port: 10,
bindIp: "127.0.0.1",
rootPath: "/foo",
traceHeader: "foo",
routes: nil,
@@ -529,6 +546,7 @@ func TestApi_Validate(t *testing.T) {
{
scenario: "invalid trace header",
port: 10,
bindIp: "127.0.0.1",
rootPath: "/foo/",
traceHeader: "",
routes: nil,
@@ -538,6 +556,7 @@ func TestApi_Validate(t *testing.T) {
{
scenario: "invalid route: empty path",
port: 10,
bindIp: "127.0.0.1",
rootPath: "/foo/",
traceHeader: "foo",
routes: []Route{
@@ -551,6 +570,7 @@ func TestApi_Validate(t *testing.T) {
{
scenario: "invalid route: missing / prefix in path",
port: 10,
bindIp: "127.0.0.1",
rootPath: "/foo/",
traceHeader: "foo",
routes: []Route{
@@ -564,6 +584,7 @@ func TestApi_Validate(t *testing.T) {
{
scenario: "invalid multipart route: no /forms prefix in path",
port: 10,
bindIp: "127.0.0.1",
rootPath: "/foo/",
traceHeader: "foo",
routes: []Route{
@@ -578,6 +599,7 @@ func TestApi_Validate(t *testing.T) {
{
scenario: "invalid route: no method",
port: 10,
bindIp: "127.0.0.1",
rootPath: "/foo/",
traceHeader: "foo",
routes: []Route{
@@ -592,6 +614,7 @@ func TestApi_Validate(t *testing.T) {
{
scenario: "invalid route: nil handler",
port: 10,
bindIp: "127.0.0.1",
rootPath: "/foo/",
traceHeader: "foo",
routes: []Route{
@@ -607,6 +630,7 @@ func TestApi_Validate(t *testing.T) {
{
scenario: "invalid route: path already existing",
port: 10,
bindIp: "127.0.0.1",
rootPath: "/foo/",
traceHeader: "foo",
routes: []Route{
@@ -627,6 +651,7 @@ func TestApi_Validate(t *testing.T) {
{
scenario: "invalid middleware: nil handler",
port: 10,
bindIp: "127.0.0.1",
rootPath: "/foo/",
traceHeader: "foo",
routes: nil,
@@ -641,6 +666,7 @@ func TestApi_Validate(t *testing.T) {
{
scenario: "success",
port: 10,
bindIp: "127.0.0.1",
rootPath: "/foo/",
traceHeader: "foo",
routes: []Route{
@@ -694,6 +720,7 @@ func TestApi_Validate(t *testing.T) {
t.Run(tc.scenario, func(t *testing.T) {
mod := Api{
port: tc.port,
bindIp: tc.bindIp,
tlsCertFile: tc.tlsCertFile,
tlsKeyFile: tc.tlsKeyFile,
rootPath: tc.rootPath,
@@ -918,15 +945,36 @@ func TestApi_Start(t *testing.T) {
}
func TestApi_StartupMessage(t *testing.T) {
mod := Api{
port: 3000,
}
for _, tc := range []struct {
scenario string
port int
bindIp string
expectMessage string
}{
{
scenario: "no custom IP",
port: 3000,
bindIp: "",
expectMessage: "server started on [::]:3000",
},
{
scenario: "custom IP",
port: 3000,
bindIp: "127.0.0.1",
expectMessage: "server started on 127.0.0.1:3000",
},
} {
t.Run(tc.scenario, func(t *testing.T) {
mod := Api{
port: tc.port,
bindIp: tc.bindIp,
}
actual := mod.StartupMessage()
expect := "server listening on port 3000"
if actual != expect {
t.Errorf("expected '%s' but got '%s'", expect, actual)
actual := mod.StartupMessage()
if actual != tc.expectMessage {
t.Errorf("expected '%s' but got '%s'", tc.expectMessage, actual)
}
})
}
}