github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/lib/http/middleware.go (about) 1 package http 2 3 import ( 4 "context" 5 "encoding/base64" 6 "fmt" 7 "net/http" 8 "strings" 9 "sync" 10 11 goauth "github.com/abbot/go-http-auth" 12 "github.com/rclone/rclone/fs" 13 ) 14 15 // parseAuthorization parses the Authorization header into user, pass 16 // it returns a boolean as to whether the parse was successful 17 func parseAuthorization(r *http.Request) (user, pass string, ok bool) { 18 authHeader := r.Header.Get("Authorization") 19 if authHeader != "" { 20 s := strings.SplitN(authHeader, " ", 2) 21 if len(s) == 2 && s[0] == "Basic" { 22 b, err := base64.StdEncoding.DecodeString(s[1]) 23 if err == nil { 24 parts := strings.SplitN(string(b), ":", 2) 25 user = parts[0] 26 if len(parts) > 1 { 27 pass = parts[1] 28 ok = true 29 } 30 } 31 } 32 } 33 return 34 } 35 36 // LoggedBasicAuth simply wraps the goauth.BasicAuth struct 37 type LoggedBasicAuth struct { 38 goauth.BasicAuth 39 } 40 41 // CheckAuth extends BasicAuth.CheckAuth to emit a log entry for unauthorised requests 42 func (a *LoggedBasicAuth) CheckAuth(r *http.Request) string { 43 username := a.BasicAuth.CheckAuth(r) 44 if username == "" { 45 user, _, _ := parseAuthorization(r) 46 fs.Infof(r.URL.Path, "%s: Unauthorized request from %s", r.RemoteAddr, user) 47 } 48 return username 49 } 50 51 // NewLoggedBasicAuthenticator instantiates a new instance of LoggedBasicAuthenticator 52 func NewLoggedBasicAuthenticator(realm string, secrets goauth.SecretProvider) *LoggedBasicAuth { 53 return &LoggedBasicAuth{BasicAuth: goauth.BasicAuth{Realm: realm, Secrets: secrets}} 54 } 55 56 // Helper to generate required interface for middleware 57 func basicAuth(authenticator *LoggedBasicAuth) func(next http.Handler) http.Handler { 58 return func(next http.Handler) http.Handler { 59 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 60 // skip auth for unix socket 61 if IsUnixSocket(r) { 62 next.ServeHTTP(w, r) 63 return 64 } 65 // skip auth for CORS preflight 66 if r.Method == "OPTIONS" { 67 next.ServeHTTP(w, r) 68 return 69 } 70 71 username := authenticator.CheckAuth(r) 72 if username == "" { 73 authenticator.RequireAuth(w, r) 74 return 75 } 76 ctx := context.WithValue(r.Context(), ctxKeyUser, username) 77 next.ServeHTTP(w, r.WithContext(ctx)) 78 }) 79 } 80 } 81 82 // MiddlewareAuthCertificateUser instantiates middleware that extracts the authenticated user via client certificate common name 83 func MiddlewareAuthCertificateUser() Middleware { 84 return func(next http.Handler) http.Handler { 85 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 86 for _, cert := range r.TLS.PeerCertificates { 87 if cert.Subject.CommonName != "" { 88 r = r.WithContext(context.WithValue(r.Context(), ctxKeyUser, cert.Subject.CommonName)) 89 next.ServeHTTP(w, r) 90 return 91 } 92 } 93 code := http.StatusUnauthorized 94 w.Header().Set("Content-Type", "text/plain") 95 http.Error(w, http.StatusText(code), code) 96 }) 97 } 98 } 99 100 // MiddlewareAuthHtpasswd instantiates middleware that authenticates against the passed htpasswd file 101 func MiddlewareAuthHtpasswd(path, realm string) Middleware { 102 fs.Infof(nil, "Using %q as htpasswd storage", path) 103 secretProvider := goauth.HtpasswdFileProvider(path) 104 authenticator := NewLoggedBasicAuthenticator(realm, secretProvider) 105 return basicAuth(authenticator) 106 } 107 108 // MiddlewareAuthBasic instantiates middleware that authenticates for a single user 109 func MiddlewareAuthBasic(user, pass, realm, salt string) Middleware { 110 fs.Infof(nil, "Using --user %s --pass XXXX as authenticated user", user) 111 pass = string(goauth.MD5Crypt([]byte(pass), []byte(salt), []byte("$1$"))) 112 secretProvider := func(u, r string) string { 113 if user == u { 114 return pass 115 } 116 return "" 117 } 118 authenticator := NewLoggedBasicAuthenticator(realm, secretProvider) 119 return basicAuth(authenticator) 120 } 121 122 // MiddlewareAuthCustom instantiates middleware that authenticates using a custom function 123 func MiddlewareAuthCustom(fn CustomAuthFn, realm string, userFromContext bool) Middleware { 124 return func(next http.Handler) http.Handler { 125 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 126 // skip auth for unix socket 127 if IsUnixSocket(r) { 128 next.ServeHTTP(w, r) 129 return 130 } 131 // skip auth for CORS preflight 132 if r.Method == "OPTIONS" { 133 next.ServeHTTP(w, r) 134 return 135 } 136 137 user, pass, ok := parseAuthorization(r) 138 if !ok && userFromContext { 139 user, ok = CtxGetUser(r.Context()) 140 } 141 142 if !ok { 143 code := http.StatusUnauthorized 144 w.Header().Set("Content-Type", "text/plain") 145 w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm=%q, charset="UTF-8"`, realm)) 146 http.Error(w, http.StatusText(code), code) 147 return 148 } 149 150 value, err := fn(user, pass) 151 if err != nil { 152 fs.Infof(r.URL.Path, "%s: Auth failed from %s: %v", r.RemoteAddr, user, err) 153 goauth.NewBasicAuthenticator(realm, func(user, realm string) string { return "" }).RequireAuth(w, r) //Reuse BasicAuth error reporting 154 return 155 } 156 157 if value != nil { 158 r = r.WithContext(context.WithValue(r.Context(), ctxKeyAuth, value)) 159 } 160 161 next.ServeHTTP(w, r) 162 }) 163 } 164 } 165 166 var onlyOnceWarningAllowOrigin sync.Once 167 168 // MiddlewareCORS instantiates middleware that handles basic CORS protections for rcd 169 func MiddlewareCORS(allowOrigin string) Middleware { 170 onlyOnceWarningAllowOrigin.Do(func() { 171 if allowOrigin == "*" { 172 fs.Logf(nil, "Warning: Allow origin set to *. This can cause serious security problems.") 173 } 174 }) 175 176 return func(next http.Handler) http.Handler { 177 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 178 // skip cors for unix sockets 179 if IsUnixSocket(r) { 180 next.ServeHTTP(w, r) 181 return 182 } 183 184 if allowOrigin != "" { 185 w.Header().Add("Access-Control-Allow-Origin", allowOrigin) 186 w.Header().Add("Access-Control-Allow-Headers", "authorization, Content-Type") 187 w.Header().Add("Access-Control-Allow-Methods", "COPY, DELETE, GET, HEAD, LOCK, MKCOL, MOVE, OPTIONS, POST, PROPFIND, PROPPATCH, PUT, TRACE, UNLOCK") 188 } 189 190 next.ServeHTTP(w, r) 191 }) 192 } 193 } 194 195 // MiddlewareStripPrefix instantiates middleware that removes the BaseURL from the path 196 func MiddlewareStripPrefix(prefix string) Middleware { 197 return func(next http.Handler) http.Handler { 198 stripPrefixHandler := http.StripPrefix(prefix, next) 199 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 200 // Allow OPTIONS on the root only 201 if r.URL.Path == "/" && r.Method == "OPTIONS" { 202 next.ServeHTTP(w, r) 203 return 204 } 205 stripPrefixHandler.ServeHTTP(w, r) 206 }) 207 } 208 }