2022-11-08 07:49:19 -04:00
|
|
|
package http
|
|
|
|
|
|
|
|
import (
|
2023-07-30 13:22:28 +01:00
|
|
|
"bytes"
|
2022-11-08 07:49:19 -04:00
|
|
|
"context"
|
|
|
|
"encoding/base64"
|
|
|
|
"fmt"
|
2023-07-30 13:22:28 +01:00
|
|
|
"io"
|
2022-11-08 07:49:19 -04:00
|
|
|
"net/http"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
|
|
|
|
goauth "github.com/abbot/go-http-auth"
|
|
|
|
"github.com/rclone/rclone/fs"
|
2023-07-30 13:22:28 +01:00
|
|
|
"github.com/rclone/rclone/fs/fshttp/fshttpdump"
|
2022-11-08 07:49:19 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
// 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 08:03:26 -04:00
|
|
|
// LoggedBasicAuth simply wraps the goauth.BasicAuth struct
|
2022-11-08 07:49:19 -04: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) {
|
|
|
|
// skip auth for unix socket
|
|
|
|
if IsUnixSocket(r) {
|
|
|
|
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))
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-26 15:26:13 +10: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 07:49:19 -04: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 15:26:13 +10:00
|
|
|
func MiddlewareAuthCustom(fn CustomAuthFn, realm string, userFromContext bool) Middleware {
|
2022-11-08 07:49:19 -04:00
|
|
|
return func(next http.Handler) http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
// skip auth for unix socket
|
|
|
|
if IsUnixSocket(r) {
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
user, pass, ok := parseAuthorization(r)
|
2023-05-26 15:26:13 +10:00
|
|
|
if !ok && userFromContext {
|
|
|
|
user, ok = CtxGetUser(r.Context())
|
|
|
|
}
|
|
|
|
|
2022-11-08 07:49:19 -04: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)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
|
|
|
// skip cors for unix sockets
|
|
|
|
if IsUnixSocket(r) {
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if allowOrigin != "" {
|
|
|
|
w.Header().Add("Access-Control-Allow-Origin", allowOrigin)
|
2023-07-29 02:58:37 +00:00
|
|
|
w.Header().Add("Access-Control-Request-Method", "POST, OPTIONS, GET, HEAD")
|
|
|
|
w.Header().Add("Access-Control-Allow-Headers", "authorization, Content-Type")
|
2022-11-08 07:49:19 -04:00
|
|
|
}
|
|
|
|
|
2023-07-26 05:15:54 -04:00
|
|
|
if r.Method == "OPTIONS" {
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
return
|
|
|
|
// Because CORS preflight OPTIONS requests are not authenticated,
|
|
|
|
// and require a 200 OK response, we will return early here.
|
|
|
|
}
|
|
|
|
|
2022-11-08 07:49:19 -04: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 {
|
|
|
|
return http.StripPrefix(prefix, next)
|
|
|
|
}
|
|
|
|
}
|
2023-07-30 13:22:28 +01:00
|
|
|
|
|
|
|
type dumpWriter struct {
|
|
|
|
w http.ResponseWriter
|
|
|
|
resp http.Response
|
|
|
|
buf bytes.Buffer
|
|
|
|
out io.Writer
|
|
|
|
dump fs.DumpFlags
|
|
|
|
dumpBody bool
|
|
|
|
}
|
|
|
|
|
|
|
|
func newDumpWriter(w http.ResponseWriter, req *http.Request, dump fs.DumpFlags) *dumpWriter {
|
|
|
|
d := &dumpWriter{
|
|
|
|
w: w,
|
|
|
|
resp: http.Response{
|
|
|
|
Status: "200 probably OK",
|
|
|
|
StatusCode: 200,
|
|
|
|
Proto: req.Proto,
|
|
|
|
ProtoMajor: req.ProtoMajor,
|
|
|
|
ProtoMinor: req.ProtoMinor,
|
|
|
|
},
|
|
|
|
dump: dump,
|
|
|
|
dumpBody: dump&(fs.DumpBodies|fs.DumpResponses) != 0,
|
|
|
|
}
|
|
|
|
if d.dumpBody {
|
|
|
|
d.out = io.MultiWriter(w, &d.buf)
|
|
|
|
}
|
|
|
|
return d
|
|
|
|
}
|
|
|
|
|
|
|
|
// Header returns the header map that will be sent by WriteHeader.
|
|
|
|
func (d *dumpWriter) Header() http.Header {
|
|
|
|
return d.w.Header()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write writes the data to the connection as part of an HTTP reply.
|
|
|
|
func (d *dumpWriter) Write(buf []byte) (int, error) {
|
|
|
|
if d.dumpBody {
|
|
|
|
return d.out.Write(buf)
|
|
|
|
}
|
|
|
|
return d.w.Write(buf)
|
|
|
|
}
|
|
|
|
|
|
|
|
// WriteHeader sends an HTTP response header with the provided status
|
|
|
|
// code.
|
|
|
|
func (d *dumpWriter) WriteHeader(statusCode int) {
|
|
|
|
d.resp.StatusCode = statusCode
|
|
|
|
d.w.WriteHeader(statusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
// dump the recorded contents.
|
|
|
|
func (d *dumpWriter) dumpResponse(req *http.Request) {
|
|
|
|
d.resp.Header = d.w.Header()
|
|
|
|
if d.dumpBody {
|
|
|
|
d.resp.Body = io.NopCloser(bytes.NewBuffer(d.buf.Bytes()))
|
|
|
|
}
|
|
|
|
fshttpdump.DumpResponse(&d.resp, req, nil, d.dump)
|
|
|
|
}
|
|
|
|
|
|
|
|
// MiddlewareDump dumps requests and responses to the log
|
|
|
|
func MiddlewareDump(dump fs.DumpFlags) Middleware {
|
|
|
|
return func(next http.Handler) http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
// First dump the incoming request
|
|
|
|
fshttpdump.DumpRequest(r, dump, false)
|
|
|
|
|
|
|
|
// Now intercept the body write
|
|
|
|
d := newDumpWriter(w, r, dump)
|
|
|
|
|
|
|
|
// Do the request
|
|
|
|
next.ServeHTTP(d, r)
|
|
|
|
|
|
|
|
// Now dump the contents
|
|
|
|
d.dumpResponse(r)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|