From 4ad62ec0166300a5d96a459bcd1c3b742a554ddf Mon Sep 17 00:00:00 2001 From: Nolan Woods Date: Mon, 19 Apr 2021 21:35:45 -0700 Subject: [PATCH] lib/http: Add authentication middleware with basic auth implementation --- lib/http/auth/auth.go | 77 ++++++++++++++++++++++++++ lib/http/auth/basic.go | 119 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 lib/http/auth/auth.go create mode 100644 lib/http/auth/basic.go diff --git a/lib/http/auth/auth.go b/lib/http/auth/auth.go new file mode 100644 index 000000000..f7e7143ba --- /dev/null +++ b/lib/http/auth/auth.go @@ -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) +} diff --git a/lib/http/auth/basic.go b/lib/http/auth/basic.go new file mode 100644 index 000000000..057524b4a --- /dev/null +++ b/lib/http/auth/basic.go @@ -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)) + } + } + }) + } +}