photoprism/internal/server/start_test.go
Michael Mayer e2ff8cd15d Server: Use canonical host for AutoTLS HTTP redirects
Signed-off-by: Michael Mayer <michael@photoprism.app>
2026-03-07 14:31:03 +01:00

174 lines
5.7 KiB
Go

package server
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"golang.org/x/crypto/acme/autocert"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/pkg/http/header"
)
// newProxyTestRouter creates a test router with trusted proxy settings applied.
func newProxyTestRouter(conf *config.Config) *gin.Engine {
r := gin.New()
configureTrustedProxySettings(r, conf)
r.GET("/ip", func(c *gin.Context) {
c.String(http.StatusOK, header.ClientIP(c))
})
return r
}
// requestClientIP performs a test request and returns the resolved client IP.
func requestClientIP(t *testing.T, router *gin.Engine, remoteAddr, forwardedFor string) string {
t.Helper()
req := httptest.NewRequest(http.MethodGet, "/ip", nil)
req.RemoteAddr = remoteAddr
if forwardedFor != "" {
req.Header.Set(header.XForwardedFor, forwardedFor)
}
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
return w.Body.String()
}
func TestConfigureTrustedProxySettings(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Run("UsesForwardedIPForTrustedProxy", func(t *testing.T) {
conf := config.NewConfig(config.CliTestContext())
conf.Options().TrustedProxies = []string{header.CidrDockerInternal}
conf.Options().ProxyClientHeaders = []string{header.XForwardedFor}
router := newProxyTestRouter(conf)
ip := requestClientIP(t, router, "172.16.5.10:12345", "203.0.113.9")
assert.Equal(t, "203.0.113.9", ip)
})
t.Run("DisablesProxyTrustWhenNoTrustedProxiesConfigured", func(t *testing.T) {
conf := config.NewConfig(config.CliTestContext())
conf.Options().TrustedProxies = nil
conf.Options().ProxyClientHeaders = []string{header.XForwardedFor}
router := newProxyTestRouter(conf)
ip := requestClientIP(t, router, "198.51.100.10:12345", "10.0.0.123")
assert.Equal(t, "198.51.100.10", ip)
})
t.Run("FallsBackToDirectIPWhenTrustedProxyIsInvalid", func(t *testing.T) {
conf := config.NewConfig(config.CliTestContext())
conf.Options().TrustedProxies = []string{"invalid"}
conf.Options().ProxyClientHeaders = []string{header.XForwardedFor}
router := newProxyTestRouter(conf)
ip := requestClientIP(t, router, "198.51.100.11:12345", "10.0.0.124")
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)
})
}
func TestCanonicalRedirectTarget(t *testing.T) {
t.Run("UsesConfiguredSiteHostInsteadOfRequestHost", func(t *testing.T) {
conf := config.NewConfig(config.CliTestContext())
conf.Options().SiteUrl = "https://photos.example.com:7443/library/"
req := httptest.NewRequest(http.MethodGet, "http://evil.example.test/library/login?next=%2Flibrary", nil)
req.Host = "evil.example.test"
target := canonicalRedirectTarget(req, conf)
assert.Equal(t, "https://photos.example.com:7443/library/login?next=%2Flibrary", target)
})
t.Run("FallsBackToRequestHostWithoutConfig", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "http://localhost:2342/library", nil)
req.Host = "localhost:2342"
target := canonicalRedirectTarget(req, nil)
assert.Equal(t, "https://localhost:2342/library", target)
})
}
func TestAutoTLSHTTPHandler(t *testing.T) {
t.Run("UsesCanonicalHostForFallbackRedirect", func(t *testing.T) {
conf := config.NewConfig(config.CliTestContext())
conf.Options().SiteUrl = "https://photos.example.com:7443/library/"
handler := (&autocert.Manager{
HostPolicy: autocert.HostWhitelist(conf.SiteDomain()),
}).HTTPHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
redirect(w, r, conf)
}))
req := httptest.NewRequest(http.MethodGet, "http://evil.example.test/library/login?next=%2Flibrary", nil)
req.Host = "evil.example.test"
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
assert.Equal(t, httpsRedirect, w.Code)
assert.Equal(t, "https://photos.example.com:7443/library/login?next=%2Flibrary", w.Header().Get(header.Location))
})
t.Run("HandlesAcmeChallengeWithoutFallbackRedirect", func(t *testing.T) {
conf := config.NewConfig(config.CliTestContext())
conf.Options().SiteUrl = "https://photos.example.com/"
fallbackCalled := false
handler := (&autocert.Manager{
HostPolicy: autocert.HostWhitelist(conf.SiteDomain()),
}).HTTPHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fallbackCalled = true
redirect(w, r, conf)
}))
req := httptest.NewRequest(http.MethodGet, "http://photos.example.com/.well-known/acme-challenge/test-token", nil)
req.Host = conf.SiteDomain()
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
assert.False(t, fallbackCalled)
assert.Equal(t, http.StatusNotFound, w.Code)
assert.Empty(t, w.Header().Get(header.Location))
})
}