mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-13 02:37:18 +03:00
* Use the RealIP middleware only behind a reverse proxy * Fix proxy ip source in tests * Fix test for PR#2087 The PR did not update the test after changing the behavior, but the test still passed because another condition was preventing the user from being created in the test. * Use RealIP even without a trusted reverse proxy * Use own type for context key * Fix casing to follow go's conventions * Do not apply RealIP middleware twice * Fix IP source in logs The most interesting data point in the log message is the proxy's IP, but having the client IP too can help identify integration issues.
297 lines
9.3 KiB
Go
297 lines
9.3 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/cors"
|
|
"github.com/navidrome/navidrome/conf"
|
|
. "github.com/navidrome/navidrome/utils/gg"
|
|
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
"github.com/navidrome/navidrome/consts"
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model/request"
|
|
"github.com/unrolled/secure"
|
|
)
|
|
|
|
func requestLogger(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
scheme := "http"
|
|
if r.TLS != nil {
|
|
scheme = "https"
|
|
}
|
|
|
|
start := time.Now()
|
|
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
|
|
next.ServeHTTP(ww, r)
|
|
status := ww.Status()
|
|
|
|
message := fmt.Sprintf("HTTP: %s %s://%s%s", r.Method, scheme, r.Host, r.RequestURI)
|
|
logArgs := []interface{}{
|
|
r.Context(),
|
|
message,
|
|
"remoteAddr", r.RemoteAddr,
|
|
"elapsedTime", time.Since(start),
|
|
"httpStatus", ww.Status(),
|
|
"responseSize", ww.BytesWritten(),
|
|
}
|
|
if log.IsGreaterOrEqualTo(log.LevelDebug) {
|
|
logArgs = append(logArgs, "userAgent", r.UserAgent())
|
|
}
|
|
|
|
switch {
|
|
case status >= 500:
|
|
log.Error(logArgs...)
|
|
case status >= 400:
|
|
log.Warn(logArgs...)
|
|
default:
|
|
log.Debug(logArgs...)
|
|
}
|
|
})
|
|
}
|
|
|
|
func loggerInjector(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
ctx = log.NewContext(r.Context(), "requestId", middleware.GetReqID(ctx))
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
|
|
func robotsTXT(fs fs.FS) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasSuffix(r.URL.Path, "/robots.txt") {
|
|
r.URL.Path = "/robots.txt"
|
|
http.FileServer(http.FS(fs)).ServeHTTP(w, r)
|
|
} else {
|
|
next.ServeHTTP(w, r)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func corsHandler() func(http.Handler) http.Handler {
|
|
return cors.Handler(cors.Options{
|
|
AllowedOrigins: []string{"*"},
|
|
AllowedMethods: []string{
|
|
http.MethodHead,
|
|
http.MethodGet,
|
|
http.MethodPost,
|
|
http.MethodPut,
|
|
http.MethodPatch,
|
|
http.MethodDelete,
|
|
},
|
|
AllowedHeaders: []string{"*"},
|
|
AllowCredentials: false,
|
|
ExposedHeaders: []string{"x-content-duration", "x-total-count", "x-nd-authorization"},
|
|
})
|
|
}
|
|
|
|
func secureMiddleware() func(http.Handler) http.Handler {
|
|
sec := secure.New(secure.Options{
|
|
ContentTypeNosniff: true,
|
|
FrameDeny: true,
|
|
ReferrerPolicy: "same-origin",
|
|
PermissionsPolicy: "autoplay=(), camera=(), microphone=(), usb=()",
|
|
//ContentSecurityPolicy: "script-src 'self' 'unsafe-inline'",
|
|
})
|
|
return sec.Handler
|
|
}
|
|
|
|
func compressMiddleware() func(http.Handler) http.Handler {
|
|
return middleware.Compress(
|
|
5,
|
|
"application/xml",
|
|
"application/json",
|
|
"application/javascript",
|
|
"text/html",
|
|
"text/plain",
|
|
"text/css",
|
|
"text/javascript",
|
|
"text/event-stream",
|
|
)
|
|
}
|
|
|
|
// clientUniqueIDMiddleware is a middleware that sets a unique client ID as a cookie if it's provided in the request header.
|
|
// If the unique client ID is not in the header but present as a cookie, it adds the ID to the request context.
|
|
func clientUniqueIDMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
clientUniqueId := r.Header.Get(consts.UIClientUniqueIDHeader)
|
|
|
|
// If clientUniqueId is found in the header, set it as a cookie
|
|
if clientUniqueId != "" {
|
|
c := &http.Cookie{
|
|
Name: consts.UIClientUniqueIDHeader,
|
|
Value: clientUniqueId,
|
|
MaxAge: consts.CookieExpiry,
|
|
HttpOnly: true,
|
|
Secure: true,
|
|
SameSite: http.SameSiteStrictMode,
|
|
Path: If(conf.Server.BasePath, "/"),
|
|
}
|
|
http.SetCookie(w, c)
|
|
} else {
|
|
// If clientUniqueId is not found in the header, check if it's present as a cookie
|
|
c, err := r.Cookie(consts.UIClientUniqueIDHeader)
|
|
if !errors.Is(err, http.ErrNoCookie) {
|
|
clientUniqueId = c.Value
|
|
}
|
|
}
|
|
|
|
// If a valid clientUniqueId is found, add it to the request context
|
|
if clientUniqueId != "" {
|
|
ctx = request.WithClientUniqueId(ctx, clientUniqueId)
|
|
r = r.WithContext(ctx)
|
|
}
|
|
|
|
// Call the next middleware or handler in the chain
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// realIPMiddleware applies middleware.RealIP, and additionally saves the request's original RemoteAddr to the request's
|
|
// context if navidrome is behind a trusted reverse proxy.
|
|
func realIPMiddleware(next http.Handler) http.Handler {
|
|
if conf.Server.ReverseProxyWhitelist != "" {
|
|
return chi.Chain(
|
|
reqToCtx(request.ReverseProxyIp, func(r *http.Request) any { return r.RemoteAddr }),
|
|
middleware.RealIP,
|
|
).Handler(next)
|
|
}
|
|
|
|
// The middleware is applied without a trusted reverse proxy to support other use-cases such as multiple clients
|
|
// behind a caching proxy. In this case, navidrome only uses the request's RemoteAddr for logging, so the security
|
|
// impact of reading the headers from untrusted sources is limited.
|
|
return middleware.RealIP(next)
|
|
}
|
|
|
|
// reqToCtx creates a middleware that updates the request's context with a value computed from the request. A given key
|
|
// can only be set once.
|
|
func reqToCtx(key any, fn func(req *http.Request) any) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Context().Value(key) == nil {
|
|
ctx := context.WithValue(r.Context(), key, fn(r))
|
|
r = r.WithContext(ctx)
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
// serverAddressMiddleware is a middleware function that modifies the request object
|
|
// to reflect the address of the server handling the request, as determined by the
|
|
// presence of X-Forwarded-* headers or the scheme and host of the request URL.
|
|
func serverAddressMiddleware(h http.Handler) http.Handler {
|
|
// Define a new handler function that will be returned by this middleware function.
|
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
|
// Call the serverAddress function to get the scheme and host of the server
|
|
// handling the request. If a host is found, modify the request object to use
|
|
// that host and scheme instead of the original ones.
|
|
if rScheme, rHost := serverAddress(r); rHost != "" {
|
|
r.Host = rHost
|
|
r.URL.Scheme = rScheme
|
|
}
|
|
|
|
// Call the next handler in the chain with the modified request and response.
|
|
h.ServeHTTP(w, r)
|
|
}
|
|
|
|
// Return the new handler function as an http.Handler object.
|
|
return http.HandlerFunc(fn)
|
|
}
|
|
|
|
// Define constants for the X-Forwarded-* header keys.
|
|
var (
|
|
xForwardedHost = http.CanonicalHeaderKey("X-Forwarded-Host")
|
|
xForwardedProto = http.CanonicalHeaderKey("X-Forwarded-Proto")
|
|
xForwardedScheme = http.CanonicalHeaderKey("X-Forwarded-Scheme")
|
|
)
|
|
|
|
// serverAddress is a helper function that returns the scheme and host of the server
|
|
// handling the given request, as determined by the presence of X-Forwarded-* headers
|
|
// or the scheme and host of the request URL.
|
|
func serverAddress(r *http.Request) (scheme, host string) {
|
|
// Save the original request host for later comparison.
|
|
origHost := r.Host
|
|
|
|
// Determine the protocol of the request based on the presence of a TLS connection.
|
|
protocol := "http"
|
|
if r.TLS != nil {
|
|
protocol = "https"
|
|
}
|
|
|
|
// Get the X-Forwarded-Host header and extract the first host name if there are
|
|
// multiple hosts listed. If there is no X-Forwarded-Host header, use the original
|
|
// request host as the default.
|
|
xfh := r.Header.Get(xForwardedHost)
|
|
if xfh != "" {
|
|
i := strings.Index(xfh, ",")
|
|
if i == -1 {
|
|
i = len(xfh)
|
|
}
|
|
xfh = xfh[:i]
|
|
}
|
|
host = FirstOr(r.Host, xfh)
|
|
|
|
// Determine the protocol and scheme of the request based on the presence of
|
|
// X-Forwarded-* headers or the scheme of the request URL.
|
|
scheme = FirstOr(
|
|
protocol,
|
|
r.Header.Get(xForwardedProto),
|
|
r.Header.Get(xForwardedScheme),
|
|
r.URL.Scheme,
|
|
)
|
|
|
|
// If the request host has changed due to the X-Forwarded-Host header, log a trace
|
|
// message with the original and new host values, as well as the scheme and URL.
|
|
if host != origHost {
|
|
log.Trace(r.Context(), "Request host has changed", "origHost", origHost, "host", host, "scheme", scheme, "url", r.URL)
|
|
}
|
|
|
|
// Return the scheme and host of the server handling the request.
|
|
return scheme, host
|
|
}
|
|
|
|
// URLParamsMiddleware is a middleware function that decodes the query string of
|
|
// the incoming HTTP request, adds the URL parameters from the routing context,
|
|
// and re-encodes the modified query string.
|
|
func URLParamsMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Retrieve the routing context from the request context.
|
|
ctx := chi.RouteContext(r.Context())
|
|
|
|
// Parse the existing query string into a URL values map.
|
|
params, _ := url.ParseQuery(r.URL.RawQuery)
|
|
|
|
// Loop through each URL parameter in the routing context.
|
|
for i, key := range ctx.URLParams.Keys {
|
|
// Skip any wildcard URL parameter keys.
|
|
if strings.Contains(key, "*") {
|
|
continue
|
|
}
|
|
|
|
// Add the URL parameter key-value pair to the URL values map.
|
|
params.Add(":"+key, ctx.URLParams.Values[i])
|
|
}
|
|
|
|
// Re-encode the URL values map as a query string and replace the
|
|
// existing query string in the request.
|
|
r.URL.RawQuery = params.Encode()
|
|
|
|
// Call the next handler in the chain with the modified request and response.
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|