mirror of
https://github.com/rclone/rclone
synced 2024-12-23 14:23:44 +01:00
lib/http: Add authentication middleware with basic auth implementation
This commit is contained in:
parent
95ee14bb2c
commit
4ad62ec016
77
lib/http/auth/auth.go
Normal file
77
lib/http/auth/auth.go
Normal file
@ -0,0 +1,77 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/rclone/rclone/fs/config/flags"
|
||||
"github.com/rclone/rclone/lib/http"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// Help contains text describing the http authentication to add to the command
|
||||
// help.
|
||||
var Help = `
|
||||
#### Authentication
|
||||
|
||||
By default this will serve files without needing a login.
|
||||
|
||||
You can either use an htpasswd file which can take lots of users, or
|
||||
set a single username and password with the --user and --pass flags.
|
||||
|
||||
Use --htpasswd /path/to/htpasswd to provide an htpasswd file. This is
|
||||
in standard apache format and supports MD5, SHA1 and BCrypt for basic
|
||||
authentication. Bcrypt is recommended.
|
||||
|
||||
To create an htpasswd file:
|
||||
|
||||
touch htpasswd
|
||||
htpasswd -B htpasswd user
|
||||
htpasswd -B htpasswd anotherUser
|
||||
|
||||
The password file can be updated while rclone is running.
|
||||
|
||||
Use --realm to set the authentication realm.
|
||||
`
|
||||
|
||||
// CustomAuthFn if used will be used to authenticate user, pass. If an error
|
||||
// is returned then the user is not authenticated.
|
||||
//
|
||||
// If a non nil value is returned then it is added to the context under the key
|
||||
type CustomAuthFn func(user, pass string) (value interface{}, err error)
|
||||
|
||||
// Options contains options for the http authentication
|
||||
type Options struct {
|
||||
HtPasswd string // htpasswd file - if not provided no authentication is done
|
||||
Realm string // realm for authentication
|
||||
BasicUser string // single username for basic auth if not using Htpasswd
|
||||
BasicPass string // password for BasicUser
|
||||
Auth CustomAuthFn `json:"-"` // custom Auth (not set by command line flags)
|
||||
}
|
||||
|
||||
// Auth instantiates middleware that authenticates users based on the configuration
|
||||
func Auth(opt Options) http.Middleware {
|
||||
if opt.Auth != nil {
|
||||
return CustomAuth(opt.Auth, opt.Realm)
|
||||
} else if opt.HtPasswd != "" {
|
||||
return HtPasswdAuth(opt.HtPasswd, opt.Realm)
|
||||
} else if opt.BasicUser != "" {
|
||||
return SingleAuth(opt.BasicUser, opt.BasicPass, opt.Realm)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Options set by command line flags
|
||||
var (
|
||||
Opt = Options{}
|
||||
)
|
||||
|
||||
// AddFlagsPrefix adds flags for http/auth
|
||||
func AddFlagsPrefix(flagSet *pflag.FlagSet, prefix string, Opt *Options) {
|
||||
flags.StringVarP(flagSet, &Opt.HtPasswd, prefix+"htpasswd", "", Opt.HtPasswd, "htpasswd file - if not provided no authentication is done")
|
||||
flags.StringVarP(flagSet, &Opt.Realm, prefix+"realm", "", Opt.Realm, "realm for authentication")
|
||||
flags.StringVarP(flagSet, &Opt.BasicUser, prefix+"user", "", Opt.BasicUser, "User name for authentication.")
|
||||
flags.StringVarP(flagSet, &Opt.BasicPass, prefix+"pass", "", Opt.BasicPass, "Password for authentication.")
|
||||
}
|
||||
|
||||
// AddFlags adds flags for the http/auth
|
||||
func AddFlags(flagSet *pflag.FlagSet) {
|
||||
AddFlagsPrefix(flagSet, "", &Opt)
|
||||
}
|
119
lib/http/auth/basic.go
Normal file
119
lib/http/auth/basic.go
Normal file
@ -0,0 +1,119 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
auth "github.com/abbot/go-http-auth"
|
||||
"github.com/rclone/rclone/fs"
|
||||
httplib "github.com/rclone/rclone/lib/http"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
type contextUserType struct{}
|
||||
|
||||
// ContextUserKey is a simple context key for storing the username of the request
|
||||
var ContextUserKey = &contextUserType{}
|
||||
|
||||
type contextAuthType struct{}
|
||||
|
||||
// ContextAuthKey is a simple context key for storing info returned by CustomAuthFn
|
||||
var ContextAuthKey = &contextAuthType{}
|
||||
|
||||
// LoggedBasicAuth extends BasicAuth to include access logging
|
||||
type LoggedBasicAuth struct {
|
||||
auth.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 auth.SecretProvider) *LoggedBasicAuth {
|
||||
return &LoggedBasicAuth{BasicAuth: auth.BasicAuth{Realm: realm, Secrets: secrets}}
|
||||
}
|
||||
|
||||
// Helper to generate required interface for middleware
|
||||
func basicAuth(authenticator *LoggedBasicAuth) httplib.Middleware {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if username := authenticator.CheckAuth(r); username == "" {
|
||||
authenticator.RequireAuth(w, r)
|
||||
} else {
|
||||
r = r.WithContext(context.WithValue(r.Context(), ContextUserKey, username))
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// HtPasswdAuth instantiates middleware that authenticates against the passed htpasswd file
|
||||
func HtPasswdAuth(path, realm string) httplib.Middleware {
|
||||
fs.Infof(nil, "Using %q as htpasswd storage", path)
|
||||
secretProvider := auth.HtpasswdFileProvider(path)
|
||||
authenticator := NewLoggedBasicAuthenticator(realm, secretProvider)
|
||||
return basicAuth(authenticator)
|
||||
}
|
||||
|
||||
// SingleAuth instantiates middleware that authenticates for a single user
|
||||
func SingleAuth(user, pass, realm string) httplib.Middleware {
|
||||
fs.Infof(nil, "Using --user %s --pass XXXX as authenticated user", user)
|
||||
pass = string(auth.MD5Crypt([]byte(pass), []byte("dlPL2MqE"), []byte("$1$")))
|
||||
secretProvider := func(user, realm string) string {
|
||||
if user == user {
|
||||
return pass
|
||||
}
|
||||
return ""
|
||||
}
|
||||
authenticator := NewLoggedBasicAuthenticator(realm, secretProvider)
|
||||
return basicAuth(authenticator)
|
||||
}
|
||||
|
||||
// CustomAuth instantiates middleware that authenticates using a custom function
|
||||
func CustomAuth(fn CustomAuthFn, realm string) httplib.Middleware {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user, pass, ok := parseAuthorization(r)
|
||||
if ok {
|
||||
value, err := fn(user, pass)
|
||||
if err != nil {
|
||||
fs.Infof(r.URL.Path, "%s: Auth failed from %s: %v", r.RemoteAddr, user, err)
|
||||
auth.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(), ContextAuthKey, value))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user