mirror of
https://github.com/photoprism/photoprism.git
synced 2026-07-17 16:49:04 +00:00
Server: Add HTTP security hardening config options #5471
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
80338bdebc
commit
ecdc49ad33
14 changed files with 285 additions and 20 deletions
|
|
@ -12,4 +12,7 @@ func TestShowConfigOptionsCommand(t *testing.T) {
|
|||
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, output, "PHOTOPRISM_IMPORT_PATH")
|
||||
assert.Contains(t, output, "PHOTOPRISM_HTTP_HEADER_TIMEOUT")
|
||||
assert.Contains(t, output, "PHOTOPRISM_HTTP_HEADER_BYTES")
|
||||
assert.Contains(t, output, "PHOTOPRISM_HTTP_IDLE_TIMEOUT")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -240,6 +240,82 @@ func TestShowConfigYaml_MarkdownServicesCIDRSectionOrder(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestShowConfigOptions_MarkdownHttpHeaderSettingsOrder(t *testing.T) {
|
||||
out, err := RunWithTestContext(ShowConfigOptionsCommand, []string{"config-options", "--md"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
webServerHeading := strings.Index(out, "### Web Server")
|
||||
httpCompression := strings.Index(out, "PHOTOPRISM_HTTP_COMPRESSION")
|
||||
httpHeaderTimeout := strings.Index(out, "PHOTOPRISM_HTTP_HEADER_TIMEOUT")
|
||||
httpHeaderBytes := strings.Index(out, "PHOTOPRISM_HTTP_HEADER_BYTES")
|
||||
httpIdleTimeout := strings.Index(out, "PHOTOPRISM_HTTP_IDLE_TIMEOUT")
|
||||
httpCachePublic := strings.Index(out, "PHOTOPRISM_HTTP_CACHE_PUBLIC")
|
||||
|
||||
if webServerHeading < 0 || httpCompression < 0 || httpHeaderTimeout < 0 || httpHeaderBytes < 0 || httpIdleTimeout < 0 || httpCachePublic < 0 {
|
||||
t.Fatalf("expected web server heading and related rows in output")
|
||||
}
|
||||
|
||||
if httpHeaderTimeout < webServerHeading {
|
||||
t.Fatalf("expected http-header-timeout row to appear in Web Server section")
|
||||
}
|
||||
|
||||
if httpHeaderTimeout < httpCompression {
|
||||
t.Fatalf("expected http-header-timeout row to appear after http-compression")
|
||||
}
|
||||
|
||||
if httpHeaderBytes < httpHeaderTimeout {
|
||||
t.Fatalf("expected http-header-bytes row to appear after http-header-timeout")
|
||||
}
|
||||
|
||||
if httpIdleTimeout < httpHeaderBytes {
|
||||
t.Fatalf("expected http-idle-timeout row to appear after http-header-bytes")
|
||||
}
|
||||
|
||||
if httpIdleTimeout > httpCachePublic {
|
||||
t.Fatalf("expected http-idle-timeout row to appear before http-cache-public")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowConfigYaml_MarkdownHttpHeaderSettingsOrder(t *testing.T) {
|
||||
out, err := RunWithTestContext(ShowConfigYamlCommand, []string{"config-yaml", "--md"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
webServerHeading := strings.Index(out, "### Web Server")
|
||||
httpCompression := strings.Index(out, "HttpCompression")
|
||||
httpHeaderTimeout := strings.Index(out, "HttpHeaderTimeout")
|
||||
httpHeaderBytes := strings.Index(out, "HttpHeaderBytes")
|
||||
httpIdleTimeout := strings.Index(out, "HttpIdleTimeout")
|
||||
httpCachePublic := strings.Index(out, "HttpCachePublic")
|
||||
|
||||
if webServerHeading < 0 || httpCompression < 0 || httpHeaderTimeout < 0 || httpHeaderBytes < 0 || httpIdleTimeout < 0 || httpCachePublic < 0 {
|
||||
t.Fatalf("expected web server heading and related rows in output")
|
||||
}
|
||||
|
||||
if httpHeaderTimeout < webServerHeading {
|
||||
t.Fatalf("expected HttpHeaderTimeout row to appear in Web Server section")
|
||||
}
|
||||
|
||||
if httpHeaderTimeout < httpCompression {
|
||||
t.Fatalf("expected HttpHeaderTimeout row to appear after HttpCompression")
|
||||
}
|
||||
|
||||
if httpHeaderBytes < httpHeaderTimeout {
|
||||
t.Fatalf("expected HttpHeaderBytes row to appear after HttpHeaderTimeout")
|
||||
}
|
||||
|
||||
if httpIdleTimeout < httpHeaderBytes {
|
||||
t.Fatalf("expected HttpIdleTimeout row to appear after HttpHeaderBytes")
|
||||
}
|
||||
|
||||
if httpIdleTimeout > httpCachePublic {
|
||||
t.Fatalf("expected HttpIdleTimeout row to appear before HttpCachePublic")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowFileFormats_JSON(t *testing.T) {
|
||||
out, err := RunWithTestContext(ShowFileFormatsCommand, []string{"file-formats", "--json"})
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,9 @@ func startAction(ctx *cli.Context) error {
|
|||
{"detach-server", fmt.Sprintf("%t", conf.DetachServer())},
|
||||
{"http-mode", conf.HttpMode()},
|
||||
{"http-compression", conf.HttpCompression()},
|
||||
{"http-header-timeout", conf.HttpHeaderTimeout().String()},
|
||||
{"http-header-bytes", fmt.Sprintf("%d", conf.HttpHeaderBytes())},
|
||||
{"http-idle-timeout", conf.HttpIdleTimeout().String()},
|
||||
{"http-cache-public", fmt.Sprintf("%t", conf.HttpCachePublic())},
|
||||
{"http-cache-maxage", fmt.Sprintf("%d", conf.HttpCacheMaxAge())},
|
||||
{"http-video-maxage", fmt.Sprintf("%d", conf.HttpVideoMaxAge())},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
## PhotoPrism — Config Package
|
||||
|
||||
**Last Updated:** March 2, 2026
|
||||
**Last Updated:** March 4, 2026
|
||||
|
||||
### Overview
|
||||
|
||||
|
|
@ -30,6 +30,13 @@ The `PHOTOPRISM_CONFIG_PATH` variable controls where PhotoPrism looks for YAML f
|
|||
|
||||
> Any change to configuration (flags, env vars, YAML files) requires a restart. The Go process reads options during startup and does not watch for changes.
|
||||
|
||||
### HTTP Hardening Defaults
|
||||
|
||||
- `PHOTOPRISM_HTTP_HEADER_TIMEOUT` / `--http-header-timeout` configures `http.Server.ReadHeaderTimeout` and defaults to `15s`.
|
||||
- `PHOTOPRISM_HTTP_HEADER_BYTES` / `--http-header-bytes` configures `http.Server.MaxHeaderBytes` and defaults to `1048576` (1 MiB).
|
||||
- `PHOTOPRISM_HTTP_IDLE_TIMEOUT` / `--http-idle-timeout` configures `http.Server.IdleTimeout` and defaults to `180s`.
|
||||
- `ReadTimeout` and `WriteTimeout` remain disabled globally so large uploads/downloads are not interrupted by a one-size-fits-all timeout.
|
||||
|
||||
### Inspect Before Editing
|
||||
|
||||
Before changing environment variables or YAML files, run `photoprism config | grep -i <flag>` to confirm the current value of a flag, such as `site-url`, or `site` to show all related values:
|
||||
|
|
|
|||
|
|
@ -44,6 +44,15 @@ const DefaultWakeupIntervalSeconds = int(15 * 60) // 15 Minutes
|
|||
// DefaultWakeupInterval is the default worker interval as a duration.
|
||||
const DefaultWakeupInterval = time.Second * time.Duration(DefaultWakeupIntervalSeconds)
|
||||
|
||||
// DefaultHttpHeaderTimeout is the default timeout for reading request headers.
|
||||
const DefaultHttpHeaderTimeout = 15 * time.Second
|
||||
|
||||
// DefaultHttpHeaderBytes is the default limit for HTTP request header size.
|
||||
const DefaultHttpHeaderBytes = 1 << 20 // 1 MiB
|
||||
|
||||
// DefaultHttpIdleTimeout is the default timeout for idle keep-alive connections.
|
||||
const DefaultHttpIdleTimeout = 180 * time.Second
|
||||
|
||||
// MegaByte defines a megabyte in bytes.
|
||||
const MegaByte = 1000 * 1000 // 1,000,000 Bytes
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config/ttl"
|
||||
"github.com/photoprism/photoprism/internal/server/limiter"
|
||||
|
|
@ -103,6 +104,33 @@ func (c *Config) HttpCompression() string {
|
|||
return strings.ToLower(strings.TrimSpace(c.options.HttpCompression))
|
||||
}
|
||||
|
||||
// HttpHeaderTimeout returns the timeout for reading HTTP request headers.
|
||||
func (c *Config) HttpHeaderTimeout() time.Duration {
|
||||
if c.options.HttpHeaderTimeout <= 0 {
|
||||
return DefaultHttpHeaderTimeout
|
||||
}
|
||||
|
||||
return c.options.HttpHeaderTimeout
|
||||
}
|
||||
|
||||
// HttpHeaderBytes returns the maximum size of HTTP request headers in bytes.
|
||||
func (c *Config) HttpHeaderBytes() int {
|
||||
if c.options.HttpHeaderBytes <= 0 {
|
||||
return DefaultHttpHeaderBytes
|
||||
}
|
||||
|
||||
return c.options.HttpHeaderBytes
|
||||
}
|
||||
|
||||
// HttpIdleTimeout returns the timeout for idle keep-alive connections.
|
||||
func (c *Config) HttpIdleTimeout() time.Duration {
|
||||
if c.options.HttpIdleTimeout <= 0 {
|
||||
return DefaultHttpIdleTimeout
|
||||
}
|
||||
|
||||
return c.options.HttpIdleTimeout
|
||||
}
|
||||
|
||||
// HttpCachePublic checks whether static content may be cached by a CDN or caching proxy.
|
||||
func (c *Config) HttpCachePublic() bool {
|
||||
if c.options.HttpCachePublic {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package config
|
|||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
|
|
@ -118,6 +119,42 @@ func TestConfig_HttpCompression(t *testing.T) {
|
|||
assert.Equal(t, "", c.HttpCompression())
|
||||
}
|
||||
|
||||
func TestConfig_HttpHeaderTimeout(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, DefaultHttpHeaderTimeout, c.HttpHeaderTimeout())
|
||||
|
||||
c.Options().HttpHeaderTimeout = 17 * time.Second
|
||||
assert.Equal(t, 17*time.Second, c.HttpHeaderTimeout())
|
||||
|
||||
c.Options().HttpHeaderTimeout = -1 * time.Second
|
||||
assert.Equal(t, DefaultHttpHeaderTimeout, c.HttpHeaderTimeout())
|
||||
}
|
||||
|
||||
func TestConfig_HttpHeaderBytes(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, DefaultHttpHeaderBytes, c.HttpHeaderBytes())
|
||||
|
||||
c.Options().HttpHeaderBytes = 2048
|
||||
assert.Equal(t, 2048, c.HttpHeaderBytes())
|
||||
|
||||
c.Options().HttpHeaderBytes = 0
|
||||
assert.Equal(t, DefaultHttpHeaderBytes, c.HttpHeaderBytes())
|
||||
}
|
||||
|
||||
func TestConfig_HttpIdleTimeout(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, DefaultHttpIdleTimeout, c.HttpIdleTimeout())
|
||||
|
||||
c.Options().HttpIdleTimeout = 2 * time.Minute
|
||||
assert.Equal(t, 2*time.Minute, c.HttpIdleTimeout())
|
||||
|
||||
c.Options().HttpIdleTimeout = -1 * time.Second
|
||||
assert.Equal(t, DefaultHttpIdleTimeout, c.HttpIdleTimeout())
|
||||
}
|
||||
|
||||
func TestConfig_HttpCachePublic(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
|
|
|
|||
|
|
@ -863,6 +863,24 @@ var Flags = CliFlags{
|
|||
Usage: "Web server compression `METHOD` (gzip, none)",
|
||||
EnvVars: EnvVars("HTTP_COMPRESSION"),
|
||||
}}, {
|
||||
Flag: &cli.StringFlag{
|
||||
Name: "http-header-timeout",
|
||||
Usage: "timeout for reading request headers as `DURATION`",
|
||||
Value: DefaultHttpHeaderTimeout.String(),
|
||||
EnvVars: EnvVars("HTTP_HEADER_TIMEOUT"),
|
||||
}}, {
|
||||
Flag: &cli.IntFlag{
|
||||
Name: "http-header-bytes",
|
||||
Usage: "maximum request header size in `BYTES`",
|
||||
Value: DefaultHttpHeaderBytes,
|
||||
EnvVars: EnvVars("HTTP_HEADER_BYTES"),
|
||||
}}, {
|
||||
Flag: &cli.StringFlag{
|
||||
Name: "http-idle-timeout",
|
||||
Usage: "timeout for idle keep-alive connections as `DURATION`",
|
||||
Value: DefaultHttpIdleTimeout.String(),
|
||||
EnvVars: EnvVars("HTTP_IDLE_TIMEOUT"),
|
||||
}}, {
|
||||
Flag: &cli.BoolFlag{
|
||||
Name: "http-cache-public",
|
||||
Usage: "allows static content to be cached by a CDN or caching proxy",
|
||||
|
|
|
|||
|
|
@ -180,6 +180,9 @@ type Options struct {
|
|||
TLSKey string `yaml:"TLSKey" json:"TLSKey" flag:"tls-key"`
|
||||
HttpMode string `yaml:"HttpMode" json:"-" flag:"http-mode"`
|
||||
HttpCompression string `yaml:"HttpCompression" json:"-" flag:"http-compression"`
|
||||
HttpHeaderTimeout time.Duration `yaml:"HttpHeaderTimeout" json:"-" flag:"http-header-timeout"`
|
||||
HttpHeaderBytes int `yaml:"HttpHeaderBytes" json:"-" flag:"http-header-bytes"`
|
||||
HttpIdleTimeout time.Duration `yaml:"HttpIdleTimeout" json:"-" flag:"http-idle-timeout"`
|
||||
HttpCachePublic bool `yaml:"HttpCachePublic" json:"HttpCachePublic" flag:"http-cache-public"`
|
||||
HttpCacheMaxAge int `yaml:"HttpCacheMaxAge" json:"HttpCacheMaxAge" flag:"http-cache-maxage"`
|
||||
HttpVideoMaxAge int `yaml:"HttpVideoMaxAge" json:"HttpVideoMaxAge" flag:"http-video-maxage"`
|
||||
|
|
|
|||
|
|
@ -233,6 +233,9 @@ func (c *Config) Report() (rows [][]string, cols []string) {
|
|||
{"tls-key", c.TLSKey()},
|
||||
{"http-mode", c.HttpMode()},
|
||||
{"http-compression", c.HttpCompression()},
|
||||
{"http-header-timeout", c.HttpHeaderTimeout().String()},
|
||||
{"http-header-bytes", fmt.Sprintf("%d", c.HttpHeaderBytes())},
|
||||
{"http-idle-timeout", c.HttpIdleTimeout().String()},
|
||||
{"http-cache-public", fmt.Sprintf("%t", c.HttpCachePublic())},
|
||||
{"http-cache-maxage", fmt.Sprintf("%d", c.HttpCacheMaxAge())},
|
||||
{"http-video-maxage", fmt.Sprintf("%d", c.HttpVideoMaxAge())},
|
||||
|
|
|
|||
|
|
@ -52,6 +52,37 @@ func TestConfig_ReportServicesCIDROrder(t *testing.T) {
|
|||
assert.Less(t, servicesCIDR, disableTLS)
|
||||
}
|
||||
|
||||
func TestConfig_ReportHttpHeaderSettingsOrder(t *testing.T) {
|
||||
conf := NewConfig(CliTestContext())
|
||||
rows, _ := conf.Report()
|
||||
|
||||
indexOf := func(name string) int {
|
||||
for i := range rows {
|
||||
if len(rows[i]) > 0 && rows[i][0] == name {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
httpCompression := indexOf("http-compression")
|
||||
httpHeaderTimeout := indexOf("http-header-timeout")
|
||||
httpHeaderBytes := indexOf("http-header-bytes")
|
||||
httpIdleTimeout := indexOf("http-idle-timeout")
|
||||
httpCachePublic := indexOf("http-cache-public")
|
||||
|
||||
assert.Greater(t, httpCompression, -1)
|
||||
assert.Greater(t, httpHeaderTimeout, -1)
|
||||
assert.Greater(t, httpHeaderBytes, -1)
|
||||
assert.Greater(t, httpIdleTimeout, -1)
|
||||
assert.Greater(t, httpCachePublic, -1)
|
||||
assert.Greater(t, httpHeaderTimeout, httpCompression)
|
||||
assert.Greater(t, httpHeaderBytes, httpHeaderTimeout)
|
||||
assert.Greater(t, httpIdleTimeout, httpHeaderBytes)
|
||||
assert.Less(t, httpIdleTimeout, httpCachePublic)
|
||||
}
|
||||
|
||||
func TestConfig_ReportDatabaseSection(t *testing.T) {
|
||||
collect := func(rows [][]string) map[string]string {
|
||||
result := make(map[string]string, len(rows))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
## PhotoPrism — HTTP Server
|
||||
|
||||
**Last Updated:** February 26, 2026
|
||||
**Last Updated:** March 4, 2026
|
||||
|
||||
### Overview
|
||||
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
- Provide a single entrypoint (`Start`) that configures listeners, middleware, and routes consistently.
|
||||
- Keep health/readiness endpoints lightweight and cache-safe.
|
||||
- Ensure redirect and TLS listeners include sensible timeouts.
|
||||
- Ensure redirect and TLS listeners include sensible header and idle limits.
|
||||
|
||||
#### Non-Goals
|
||||
|
||||
|
|
@ -46,7 +46,12 @@
|
|||
- Compression: only gzip is enabled; brotli requests log a notice.
|
||||
- Trusted proxies/platform headers are read from config; keep the list tight.
|
||||
- If no trusted proxy ranges are configured (or the configured ranges are invalid), proxy trust is disabled and client IP resolution falls back to the TCP peer address.
|
||||
- AutoTLS: uses `autocert` and spins up a redirect listener with explicit read/write timeouts; ensure ports 80/443 are reachable.
|
||||
- HTTP hardening defaults:
|
||||
- `ReadHeaderTimeout` is configured via `PHOTOPRISM_HTTP_HEADER_TIMEOUT` / `--http-header-timeout` (default `15s`).
|
||||
- `MaxHeaderBytes` is configured via `PHOTOPRISM_HTTP_HEADER_BYTES` / `--http-header-bytes` (default `1 MiB`).
|
||||
- `IdleTimeout` is configured via `PHOTOPRISM_HTTP_IDLE_TIMEOUT` / `--http-idle-timeout` (default `180s`).
|
||||
- Global `ReadTimeout` / `WriteTimeout` remain disabled to avoid breaking large transfers.
|
||||
- AutoTLS: uses `autocert` and spins up a redirect listener; ensure ports 80/443 are reachable.
|
||||
- Unix sockets: optional `force` query removes stale sockets; permissions can be set via `mode` query.
|
||||
- Health endpoints (`/livez`, `/health`, `/healthz`, `/readyz`) return `Cache-Control: no-store` and `Access-Control-Allow-Origin: *`.
|
||||
|
||||
|
|
|
|||
|
|
@ -117,17 +117,9 @@ func Start(ctx context.Context, conf *config.Config) {
|
|||
}
|
||||
router.Any(conf.BaseUri("/readyz"), isReady)
|
||||
|
||||
// Create a new HTTP server instance with no read or write timeout, except for reading the headers:
|
||||
// https://pkg.go.dev/net/http#Server
|
||||
server := &http.Server{
|
||||
ReadHeaderTimeout: time.Minute,
|
||||
ReadTimeout: -1,
|
||||
WriteTimeout: -1,
|
||||
Handler: router,
|
||||
}
|
||||
|
||||
var tlsErr error
|
||||
var tlsManager *autocert.Manager
|
||||
var server *http.Server
|
||||
|
||||
// Listen on a Unix domain socket instead of a TCP port?
|
||||
if unixSocket := conf.HttpSocket(); unixSocket != nil {
|
||||
|
|
@ -168,6 +160,7 @@ func Start(ctx context.Context, conf *config.Config) {
|
|||
|
||||
// Listen on Unix socket, which should be automatically closed and removed after use:
|
||||
// https://pkg.go.dev/net#UnixListener.SetUnlinkOnClose.
|
||||
server = newHTTPServer(router, conf)
|
||||
server.Addr = listener.Addr().String()
|
||||
|
||||
log.Infof("server: listening on %s [%s]", unixSocket.Path, time.Since(start))
|
||||
|
|
@ -182,6 +175,8 @@ func Start(ctx context.Context, conf *config.Config) {
|
|||
tlsConfig := tlsManager.TLSConfig()
|
||||
tlsConfig.MinVersion = tls.VersionTLS12
|
||||
|
||||
server = newHTTPServer(router, conf)
|
||||
|
||||
// Listen on HTTPS socket.
|
||||
server.Addr = tlsSocket
|
||||
server.TLSConfig = tlsConfig
|
||||
|
|
@ -198,6 +193,8 @@ func Start(ctx context.Context, conf *config.Config) {
|
|||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
|
||||
server = newHTTPServer(router, conf)
|
||||
|
||||
// Listen on HTTPS socket.
|
||||
server.Addr = tlsSocket
|
||||
server.TLSConfig = tlsConfig
|
||||
|
|
@ -216,6 +213,7 @@ func Start(ctx context.Context, conf *config.Config) {
|
|||
return
|
||||
} else {
|
||||
// Listen on HTTP socket.
|
||||
server = newHTTPServer(router, conf)
|
||||
server.Addr = tcpSocket
|
||||
|
||||
log.Infof("server: listening on %s [%s]", server.Addr, time.Since(start))
|
||||
|
|
@ -281,13 +279,8 @@ func StartAutoTLS(s *http.Server, m *autocert.Manager, conf *config.Config) {
|
|||
var g errgroup.Group
|
||||
|
||||
g.Go(func() error {
|
||||
redirectSrv := &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", conf.HttpHost(), conf.HttpPort()),
|
||||
Handler: m.HTTPHandler(http.HandlerFunc(redirect)),
|
||||
ReadHeaderTimeout: time.Minute,
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
}
|
||||
redirectSrv := newHTTPServer(m.HTTPHandler(http.HandlerFunc(redirect)), conf)
|
||||
redirectSrv.Addr = fmt.Sprintf("%s:%d", conf.HttpHost(), conf.HttpPort())
|
||||
|
||||
return redirectSrv.ListenAndServe()
|
||||
})
|
||||
|
|
@ -310,3 +303,25 @@ func redirect(w http.ResponseWriter, req *http.Request) {
|
|||
|
||||
http.Redirect(w, req, target, httpsRedirect)
|
||||
}
|
||||
|
||||
// newHTTPServer creates an HTTP server with hardened header and idle settings.
|
||||
func newHTTPServer(handler http.Handler, conf *config.Config) *http.Server {
|
||||
headerTimeout := config.DefaultHttpHeaderTimeout
|
||||
headerBytes := config.DefaultHttpHeaderBytes
|
||||
idleTimeout := config.DefaultHttpIdleTimeout
|
||||
|
||||
if conf != nil {
|
||||
headerTimeout = conf.HttpHeaderTimeout()
|
||||
headerBytes = conf.HttpHeaderBytes()
|
||||
idleTimeout = conf.HttpIdleTimeout()
|
||||
}
|
||||
|
||||
return &http.Server{
|
||||
ReadHeaderTimeout: headerTimeout,
|
||||
ReadTimeout: 0,
|
||||
WriteTimeout: 0,
|
||||
IdleTimeout: idleTimeout,
|
||||
MaxHeaderBytes: headerBytes,
|
||||
Handler: handler,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -77,3 +78,29 @@ func TestConfigureTrustedProxySettings(t *testing.T) {
|
|||
assert.Equal(t, "198.51.100.11", ip)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewHTTPServer(t *testing.T) {
|
||||
t.Run("UsesConfiguredValues", func(t *testing.T) {
|
||||
conf := config.NewConfig(config.CliTestContext())
|
||||
conf.Options().HttpHeaderTimeout = 15 * time.Second
|
||||
conf.Options().HttpHeaderBytes = 2048
|
||||
conf.Options().HttpIdleTimeout = 2 * time.Minute
|
||||
|
||||
server := newHTTPServer(http.NewServeMux(), conf)
|
||||
|
||||
assert.Equal(t, 15*time.Second, server.ReadHeaderTimeout)
|
||||
assert.Equal(t, 0*time.Second, server.ReadTimeout)
|
||||
assert.Equal(t, 0*time.Second, server.WriteTimeout)
|
||||
assert.Equal(t, 2*time.Minute, server.IdleTimeout)
|
||||
assert.Equal(t, 2048, server.MaxHeaderBytes)
|
||||
})
|
||||
t.Run("UsesDefaultsWhenConfigIsNil", func(t *testing.T) {
|
||||
server := newHTTPServer(http.NewServeMux(), nil)
|
||||
|
||||
assert.Equal(t, config.DefaultHttpHeaderTimeout, server.ReadHeaderTimeout)
|
||||
assert.Equal(t, 0*time.Second, server.ReadTimeout)
|
||||
assert.Equal(t, 0*time.Second, server.WriteTimeout)
|
||||
assert.Equal(t, config.DefaultHttpIdleTimeout, server.IdleTimeout)
|
||||
assert.Equal(t, config.DefaultHttpHeaderBytes, server.MaxHeaderBytes)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue