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.
232 lines
7.5 KiB
Go
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)
|
|
})
|
|
}
|
|
}
|