Files
rclone/lib/http/middleware.go
T
am-at-enrollvbandGitHub 5dd34275dc serve: pass the client IP address to the auth proxy - fixes #4499
The auth proxy was only given the user and their password or public
key, so a proxy program had no way to restrict logins to particular
networks, or to record where an authentication attempt came from.

The JSON sent to the program now has a client_ip key holding the bare
IP the client connected from, with the port stripped so IPv6 arrives
as 2001:db8::1 rather than [2001:db8::1]:52344. An IPv4-mapped IPv6
address is reported as plain IPv4 so that a client arriving over a
dual-stack listener still matches IPv4 networks. The key is omitted
when the client has no IP address.

The IP is also mixed into the backend cache key. That is needed as the
program is only run on a cache miss, so a client from a
non-allowlisted address presenting valid credentials within the 5
minute cache lifetime would get a cache hit and be let in without the
program being consulted at all.
2026-08-01 12:25:06 +01:00

232 lines
7.5 KiB
Go

package http
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"regexp"
"strings"
"sync"
goauth "github.com/abbot/go-http-auth"
"github.com/rclone/rclone/fs"
)
// parseAuthorization parses the Authorization header into user, pass
// it returns a boolean as to whether the parse was successful
func parseAuthorization(r *http.Request) (user, pass string, ok bool) {
authHeader := r.Header.Get("Authorization")
if authHeader != "" {
s := strings.SplitN(authHeader, " ", 2)
if len(s) == 2 && s[0] == "Basic" {
b, err := base64.StdEncoding.DecodeString(s[1])
if err == nil {
parts := strings.SplitN(string(b), ":", 2)
user = parts[0]
if len(parts) > 1 {
pass = parts[1]
ok = true
}
}
}
}
return
}
// LoggedBasicAuth simply wraps the goauth.BasicAuth struct
type LoggedBasicAuth struct {
goauth.BasicAuth
}
// CheckAuth extends BasicAuth.CheckAuth to emit a log entry for unauthorised requests
func (a *LoggedBasicAuth) CheckAuth(r *http.Request) string {
username := a.BasicAuth.CheckAuth(r)
if username == "" {
user, _, _ := parseAuthorization(r)
fs.Infof(r.URL.Path, "%s: Unauthorized request from %s", r.RemoteAddr, user)
}
return username
}
// NewLoggedBasicAuthenticator instantiates a new instance of LoggedBasicAuthenticator
func NewLoggedBasicAuthenticator(realm string, secrets goauth.SecretProvider) *LoggedBasicAuth {
return &LoggedBasicAuth{BasicAuth: goauth.BasicAuth{Realm: realm, Secrets: secrets}}
}
// Helper to generate required interface for middleware
func basicAuth(authenticator *LoggedBasicAuth) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// skip auth for CORS preflight
if r.Method == "OPTIONS" {
next.ServeHTTP(w, r)
return
}
username := authenticator.CheckAuth(r)
if username == "" {
authenticator.RequireAuth(w, r)
return
}
ctx := context.WithValue(r.Context(), ctxKeyUser, username)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// MiddlewareAuthCertificateUser instantiates middleware that extracts the authenticated user via client certificate common name
func MiddlewareAuthCertificateUser() Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// r.TLS is nil on a plain HTTP listener (http:// prefix) where
// there can be no client certificate
if r.TLS != nil {
for _, cert := range r.TLS.PeerCertificates {
if cert.Subject.CommonName != "" {
r = r.WithContext(context.WithValue(r.Context(), ctxKeyUser, cert.Subject.CommonName))
next.ServeHTTP(w, r)
return
}
}
}
code := http.StatusUnauthorized
w.Header().Set("Content-Type", "text/plain")
http.Error(w, http.StatusText(code), code)
})
}
}
// MiddlewareAuthHtpasswd instantiates middleware that authenticates against the passed htpasswd file
func MiddlewareAuthHtpasswd(path, realm string) Middleware {
fs.Infof(nil, "Using %q as htpasswd storage", path)
secretProvider := goauth.HtpasswdFileProvider(path)
authenticator := NewLoggedBasicAuthenticator(realm, secretProvider)
return basicAuth(authenticator)
}
// MiddlewareAuthBasic instantiates middleware that authenticates for a single user
func MiddlewareAuthBasic(user, pass, realm, salt string) Middleware {
fs.Infof(nil, "Using --user %s --pass XXXX as authenticated user", user)
pass = string(goauth.MD5Crypt([]byte(pass), []byte(salt), []byte("$1$")))
secretProvider := func(u, r string) string {
if user == u {
return pass
}
return ""
}
authenticator := NewLoggedBasicAuthenticator(realm, secretProvider)
return basicAuth(authenticator)
}
// MiddlewareAuthCustom instantiates middleware that authenticates using a custom function
func MiddlewareAuthCustom(fn CustomAuthFn, realm string, userFromContext bool) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// skip auth for CORS preflight
if r.Method == "OPTIONS" {
next.ServeHTTP(w, r)
return
}
user, pass, ok := parseAuthorization(r)
if !ok && userFromContext {
user, ok = CtxGetUser(r.Context())
}
if !ok {
code := http.StatusUnauthorized
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm=%q, charset="UTF-8"`, realm))
http.Error(w, http.StatusText(code), code)
return
}
value, err := fn(r, user, pass)
if err != nil {
fs.Infof(r.URL.Path, "%s: Auth failed from %s: %v", r.RemoteAddr, user, err)
goauth.NewBasicAuthenticator(realm, func(user, realm string) string { return "" }).RequireAuth(w, r) //Reuse BasicAuth error reporting
return
}
if value != nil {
r = r.WithContext(context.WithValue(r.Context(), ctxKeyAuth, value))
}
next.ServeHTTP(w, r)
})
}
}
var validUsernameRegexp = regexp.MustCompile(`^[\p{L}\d@._-]+$`)
// MiddlewareAuthGetUserFromHeader middleware that bypasses authentication and extracts the user via a specified HTTP header(ideal for proxied setups).
func MiddlewareAuthGetUserFromHeader(header string) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
username := strings.TrimSpace(r.Header.Get(header))
if username != "" && validUsernameRegexp.MatchString(username) {
r = r.WithContext(context.WithValue(r.Context(), ctxKeyUser, username))
next.ServeHTTP(w, r)
return
}
code := http.StatusUnauthorized
w.Header().Set("Content-Type", "text/plain")
http.Error(w, http.StatusText(code), code)
})
}
}
var onlyOnceWarningAllowOrigin sync.Once
// MiddlewareCORS instantiates middleware that handles basic CORS protections for rcd
func MiddlewareCORS(allowOrigin string) Middleware {
onlyOnceWarningAllowOrigin.Do(func() {
if allowOrigin == "*" {
fs.Logf(nil, "Warning: Allow origin set to *. This can cause serious security problems.")
}
})
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if allowOrigin != "" {
w.Header().Add("Access-Control-Allow-Origin", allowOrigin)
w.Header().Add("Access-Control-Allow-Headers", "Authorization, Content-Type, Depth, Destination, If, Lock-Token, Overwrite, Prefer, TimeOut, Translate")
w.Header().Add("Access-Control-Allow-Methods", "COPY, DELETE, GET, HEAD, LOCK, MKCOL, MOVE, OPTIONS, POST, PROPFIND, PROPPATCH, PUT, TRACE, UNLOCK")
w.Header().Add("Access-Control-Max-Age", "86400")
}
next.ServeHTTP(w, r)
})
}
}
// MiddlewareResponseHeaders instantiates middleware that apply custom headers to every response
func MiddlewareResponseHeaders(headers []*fs.HTTPOption) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for _, header := range headers {
w.Header().Set(header.Key, header.Value)
}
next.ServeHTTP(w, r)
})
}
}
// MiddlewareStripPrefix instantiates middleware that removes the BaseURL from the path
func MiddlewareStripPrefix(prefix string) Middleware {
return func(next http.Handler) http.Handler {
stripPrefixHandler := http.StripPrefix(prefix, next)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Allow OPTIONS on the root only
if r.URL.Path == "/" && r.Method == "OPTIONS" {
next.ServeHTTP(w, r)
return
}
stripPrefixHandler.ServeHTTP(w, r)
})
}
}