2022-11-08 19:49:19 +08:00
package http
import (
"context"
"encoding/base64"
"fmt"
"net/http"
2025-01-17 23:53:23 +08:00
"regexp"
2022-11-08 19:49:19 +08:00
"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
}
2022-11-08 20:03:26 +08:00
// LoggedBasicAuth simply wraps the goauth.BasicAuth struct
2022-11-08 19:49:19 +08:00
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 ) {
2023-07-31 02:59:54 +08:00
// skip auth for CORS preflight
if r . Method == "OPTIONS" {
next . ServeHTTP ( w , r )
return
}
2022-11-08 19:49:19 +08:00
username := authenticator . CheckAuth ( r )
if username == "" {
authenticator . RequireAuth ( w , r )
return
}
ctx := context . WithValue ( r . Context ( ) , ctxKeyUser , username )
next . ServeHTTP ( w , r . WithContext ( ctx ) )
} )
}
}
2023-05-26 13:26:13 +08:00
// 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 ) {
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 )
} )
}
}
2022-11-08 19:49:19 +08:00
// 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
2023-05-26 13:26:13 +08:00
func MiddlewareAuthCustom ( fn CustomAuthFn , realm string , userFromContext bool ) Middleware {
2022-11-08 19:49:19 +08:00
return func ( next http . Handler ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
2023-07-31 02:59:54 +08:00
// skip auth for CORS preflight
if r . Method == "OPTIONS" {
next . ServeHTTP ( w , r )
return
}
2022-11-08 19:49:19 +08:00
user , pass , ok := parseAuthorization ( r )
2023-05-26 13:26:13 +08:00
if ! ok && userFromContext {
user , ok = CtxGetUser ( r . Context ( ) )
}
2022-11-08 19:49:19 +08:00
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 ( 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 )
} )
}
}
2025-01-17 23:53:23 +08:00
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 )
} )
}
}
2022-11-08 19:49:19 +08:00
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 )
2023-07-29 10:58:37 +08:00
w . Header ( ) . Add ( "Access-Control-Allow-Headers" , "authorization, Content-Type" )
2023-11-22 20:23:34 +08:00
w . Header ( ) . Add ( "Access-Control-Allow-Methods" , "COPY, DELETE, GET, HEAD, LOCK, MKCOL, MOVE, OPTIONS, POST, PROPFIND, PROPPATCH, PUT, TRACE, UNLOCK" )
2022-11-08 19:49:19 +08:00
}
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 {
2024-03-27 20:23:35 +08:00
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 )
} )
2022-11-08 19:49:19 +08:00
}
}