serve: pass the client IP address to the auth proxy - fixes #4499

The auth proxy was only given the user and their password or public
key, so a proxy program had no way to restrict logins to particular
networks, or to record where an authentication attempt came from.

The JSON sent to the program now has a client_ip key holding the bare
IP the client connected from, with the port stripped so IPv6 arrives
as 2001:db8::1 rather than [2001:db8::1]:52344. An IPv4-mapped IPv6
address is reported as plain IPv4 so that a client arriving over a
dual-stack listener still matches IPv4 networks. The key is omitted
when the client has no IP address.

The IP is also mixed into the backend cache key. That is needed as the
program is only run on a cache miss, so a client from a
non-allowlisted address presenting valid credentials within the 5
minute cache lifetime would get a cache hit and be let in without the
program being consulted at all.
This commit is contained in:
am-at-enrollvb
2026-08-01 12:25:06 +01:00
committed by GitHub
parent 7804c1b315
commit 5dd34275dc
11 changed files with 190 additions and 59 deletions
+16
View File
@@ -4,13 +4,29 @@ A demo proxy for rclone serve sftp/webdav/ftp, etc.
This takes the incoming user/pass and converts it into an sftp backend This takes the incoming user/pass and converts it into an sftp backend
running on localhost. running on localhost.
Logins from outside ALLOWED_NETWORKS are refused.
""" """
import sys import sys
import json import json
import ipaddress
ALLOWED_NETWORKS = ["127.0.0.0/8", "::1/128"]
def allowed(ip):
"""Return True if ip is in one of ALLOWED_NETWORKS."""
if ip is None:
return False
address = ipaddress.ip_address(ip)
return any(address in ipaddress.ip_network(network) for network in ALLOWED_NETWORKS)
def main(): def main():
i = json.load(sys.stdin) i = json.load(sys.stdin)
# Exiting non zero refuses the login - rclone logs whatever we
# write on stderr, so say why.
if not allowed(i.get("client_ip")):
sys.exit("client_ip %s not allowed" % i.get("client_ip"))
o = { o = {
"type": "sftp", # type of backend "type": "sftp", # type of backend
"_root": "", # root of the fs "_root": "", # root of the fs
+2 -2
View File
@@ -317,7 +317,7 @@ func (l *Logger) PrintResponse(sessionID string, code int, message string) {
// CheckPasswd handle auth based on configuration // CheckPasswd handle auth based on configuration
func (d *driver) CheckPasswd(sctx *ftp.Context, user, pass string) (ok bool, err error) { func (d *driver) CheckPasswd(sctx *ftp.Context, user, pass string) (ok bool, err error) {
if d.proxy != nil { if d.proxy != nil {
_, _, err = d.proxy.Call(user, pass, false) _, _, err = d.proxy.Call(user, pass, false, sctx.Sess.RemoteAddr().String())
if err != nil { if err != nil {
fs.Infof(nil, "proxy login failed: %v", err) fs.Infof(nil, "proxy login failed: %v", err)
return false, nil return false, nil
@@ -366,7 +366,7 @@ func (d *driver) getVFS(sctx *ftp.Context) (VFS *vfs.VFS, err error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
VFS, _, err = d.proxy.Call(user, pass, false) VFS, _, err = d.proxy.Call(user, pass, false, sctx.Sess.RemoteAddr().String())
if err != nil { if err != nil {
return nil, fmt.Errorf("proxy login failed: %w", err) return nil, fmt.Errorf("proxy login failed: %w", err)
} }
+2 -2
View File
@@ -175,8 +175,8 @@ func (s *HTTP) getVFS(ctx context.Context) (VFS *vfs.VFS, err error) {
} }
// auth does proxy authorization // auth does proxy authorization
func (s *HTTP) auth(user, pass string) (value any, err error) { func (s *HTTP) auth(r *http.Request, user, pass string) (value any, err error) {
VFS, _, err := s.proxy.Call(user, pass, false) VFS, _, err := s.proxy.Call(user, pass, false, r.RemoteAddr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+60 -30
View File
@@ -12,6 +12,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/netip"
"os/exec" "os/exec"
"strings" "strings"
"time" "time"
@@ -62,7 +63,8 @@ process (on STDIN) would look similar to this:
|||json |||json
{ {
"user": "me", "user": "me",
"pass": "mypassword" "pass": "mypassword",
"client_ip": "192.168.1.1"
} }
||| |||
@@ -72,10 +74,18 @@ proxy process (on STDIN) would look similar to this:
|||json |||json
{ {
"user": "me", "user": "me",
"public_key": "AAAAB3NzaC1yc2EAAAADAQABAAABAQDuwESFdAe14hVS6omeyX7edc...JQdf" "public_key": "AAAAB3NzaC1yc2EAAAADAQABAAABAQDuwESFdAe14hVS6omeyX7edc...JQdf",
"client_ip": "192.168.1.1"
} }
||| |||
The |client_ip| key holds the IP address the client connected from,
without a port number. It can be used to restrict logins to certain
networks, or to log authentication attempts centrally. It is omitted if
the client has no IP address, for example when connecting over a unix
socket. Note that if rclone is behind a reverse proxy this will be the
address of the reverse proxy and not the original client.
And as an example return this on STDOUT And as an example return this on STDOUT
|||json |||json
@@ -101,11 +111,12 @@ to make proxy to many different sftp backends, you could make the
in the output and the user to |user|. For security you'd probably want in the output and the user to |user|. For security you'd probably want
to restrict the |host| to a limited list. to restrict the |host| to a limited list.
An internal cache of backends is keyed on the |user| and a hash of the An internal cache of backends is keyed on the |user|, a hash of the
|pass| or |public_key|. This means that if a user's password or |pass| or |public_key|, and the |client_ip|. This means that if a
public-key changes, or the proxy returns different config parameters user's password or public-key changes, the client connects from a new IP
(eg a rotated |api_key|), a fresh backend will be created on the next address, or the proxy returns different config parameters (eg a rotated
request rather than the cached one being reused. |api_key|), a fresh backend will be created on the next request rather
than the cached one being reused.
This can be used to build general purpose proxies to any kind of This can be used to build general purpose proxies to any kind of
backend that rclone supports. backend that rclone supports.
@@ -212,29 +223,41 @@ var cacheKeyHMACKey = func() []byte {
return key return key
}() }()
// generateCacheKey creates a composite cache key from user and auth credentials // ipFromAddr returns the bare IP from a "host:port" address, or "" if it has none.
func generateCacheKey(user, auth string) string { func ipFromAddr(addr string) string {
ap, err := netip.ParseAddrPort(addr)
if err != nil {
return ""
}
return ap.Addr().Unmap().String()
}
// generateCacheKey creates a composite cache key from the user, the auth
// credentials and the client's IP address.
func generateCacheKey(user, auth, clientIP string) string {
mac := hmac.New(sha256.New, cacheKeyHMACKey) mac := hmac.New(sha256.New, cacheKeyHMACKey)
mac.Write([]byte(auth)) mac.Write([]byte(auth))
// Separate the two so ("ab", "c") can't collide with ("a", "bc")
mac.Write([]byte{0})
mac.Write([]byte(clientIP))
return user + "-" + hex.EncodeToString(mac.Sum(nil)[:8]) return user + "-" + hex.EncodeToString(mac.Sum(nil)[:8])
} }
// call runs the auth proxy and returns a cacheEntry and an error // call runs the auth proxy and returns a cacheEntry and an error
func (p *Proxy) call(user, auth string, isPublicKey bool) (value any, err error) { func (p *Proxy) call(user, auth string, isPublicKey bool, clientIP string) (value any, err error) {
var config configmap.Simple
// Contact the proxy // Contact the proxy
if isPublicKey { in := map[string]string{
config, err = p.run(map[string]string{ "user": user,
"user": user,
"public_key": auth,
})
} else {
config, err = p.run(map[string]string{
"user": user,
"pass": auth,
})
} }
if isPublicKey {
in["public_key"] = auth
} else {
in["pass"] = auth
}
if clientIP != "" {
in["client_ip"] = clientIP
}
config, err := p.run(in)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -255,10 +278,10 @@ func (p *Proxy) call(user, auth string, isPublicKey bool) (value any, err error)
return nil, fmt.Errorf("proxy: couldn't find backend for %q: %w", fsName, err) return nil, fmt.Errorf("proxy: couldn't find backend for %q: %w", fsName, err)
} }
// Make the cache key include the auth so that changes to the // Make the cache key include the auth and the client IP so that
// auth (eg the proxy returning new config) create a fresh // changes to either (eg the proxy returning new config) create a
// backend rather than reusing the cached one. // fresh backend rather than reusing the cached one.
cacheKey := generateCacheKey(user, auth) cacheKey := generateCacheKey(user, auth, clientIP)
// base name of config on user name and auth hash. This may appear in logs // base name of config on user name and auth hash. This may appear in logs
name := "proxy-" + cacheKey name := "proxy-" + cacheKey
@@ -298,16 +321,23 @@ func (p *Proxy) call(user, auth string, isPublicKey bool) (value any, err error)
// Call runs the auth proxy with the username and password/public key provided // Call runs the auth proxy with the username and password/public key provided
// returning a *vfs.VFS and the key used in the VFS cache. // returning a *vfs.VFS and the key used in the VFS cache.
func (p *Proxy) Call(user, auth string, isPublicKey bool) (VFS *vfs.VFS, vfsKey string, err error) { //
// Cache key includes the auth so credential changes don't hit a stale entry // remoteAddr is the address of the client as returned by net.Addr.String().
cacheKey := generateCacheKey(user, auth) // It may be empty if the client has no IP address, for example when
// connecting over a unix socket.
func (p *Proxy) Call(user, auth string, isPublicKey bool, remoteAddr string) (VFS *vfs.VFS, vfsKey string, err error) {
clientIP := ipFromAddr(remoteAddr)
// Cache key includes the auth and the client IP so credential or
// address changes don't hit a stale entry
cacheKey := generateCacheKey(user, auth, clientIP)
// Look in the cache first with the credential-aware key // Look in the cache first with the credential-aware key
value, ok := p.vfsCache.GetMaybe(cacheKey) value, ok := p.vfsCache.GetMaybe(cacheKey)
// If not found then call the proxy for a fresh answer // If not found then call the proxy for a fresh answer
if !ok { if !ok {
value, err = p.call(user, auth, isPublicKey) value, err = p.call(user, auth, isPublicKey, clientIP)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
+93 -13
View File
@@ -42,6 +42,25 @@ func TestRun(t *testing.T) {
}, config) }, config)
}) })
t.Run("ClientIP", func(t *testing.T) {
config, err := p.run(map[string]string{
"type": "ftp",
"user": "me",
"pass": "pass",
"host": "127.0.0.1",
"client_ip": "192.0.2.1",
})
require.NoError(t, err)
assert.Equal(t, configmap.Simple{
"type": "ftp",
"user": "me-test",
"pass": "pass",
"host": "127.0.0.1",
"client_ip": "192.0.2.1",
"_root": "",
}, config)
})
t.Run("Error", func(t *testing.T) { t.Run("Error", func(t *testing.T) {
config, err := p.run(map[string]string{ config, err := p.run(map[string]string{
"error": "potato", "error": "potato",
@@ -74,6 +93,24 @@ func TestRun(t *testing.T) {
const testUser = "testUser" const testUser = "testUser"
const testPass = "testPass" const testPass = "testPass"
const testIP = "192.0.2.1"
const testAddr = testIP + ":1024"
const otherAddr = "198.51.100.1:1024"
t.Run("CacheKey", func(t *testing.T) {
// The source port differs on every connection so it must not
// affect the cache key, otherwise the proxy would be run for
// every connection rather than once per client.
assert.Equal(t,
generateCacheKey(testUser, testPass, ipFromAddr(testIP+":1024")),
generateCacheKey(testUser, testPass, ipFromAddr(testIP+":2048")))
// A different client IP must produce a different key so the
// proxy is consulted again
assert.NotEqual(t,
generateCacheKey(testUser, testPass, ipFromAddr(testAddr)),
generateCacheKey(testUser, testPass, ipFromAddr(otherAddr)))
})
t.Run("call w/Password", func(t *testing.T) { t.Run("call w/Password", func(t *testing.T) {
// check cache empty // check cache empty
@@ -81,7 +118,7 @@ func TestRun(t *testing.T) {
defer p.vfsCache.Clear() defer p.vfsCache.Clear()
passwordBytes := []byte(testPass) passwordBytes := []byte(testPass)
value, err := p.call(testUser, testPass, false) value, err := p.call(testUser, testPass, false, testIP)
require.NoError(t, err) require.NoError(t, err)
entry, ok := value.(cacheEntry) entry, ok := value.(cacheEntry)
require.True(t, ok) require.True(t, ok)
@@ -91,7 +128,7 @@ func TestRun(t *testing.T) {
require.NotNil(t, entry.vfs) require.NotNil(t, entry.vfs)
f := entry.vfs.Fs() f := entry.vfs.Fs()
require.NotNil(t, f) require.NotNil(t, f)
cacheKey := generateCacheKey(testUser, testPass) cacheKey := generateCacheKey(testUser, testPass, testIP)
assert.Equal(t, "proxy-"+cacheKey, f.Name()) assert.Equal(t, "proxy-"+cacheKey, f.Name())
assert.True(t, strings.HasPrefix(f.String(), "Local file system")) assert.True(t, strings.HasPrefix(f.String(), "Local file system"))
@@ -107,8 +144,8 @@ func TestRun(t *testing.T) {
assert.Equal(t, 0, p.vfsCache.Entries()) assert.Equal(t, 0, p.vfsCache.Entries())
defer p.vfsCache.Clear() defer p.vfsCache.Clear()
cacheKey := generateCacheKey(testUser, testPass) cacheKey := generateCacheKey(testUser, testPass, testIP)
vfs, vfsKey, err := p.Call(testUser, testPass, false) vfs, vfsKey, err := p.Call(testUser, testPass, false, testAddr)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, vfs) require.NotNil(t, vfs)
assert.Equal(t, "proxy-"+cacheKey, vfs.Fs().Name()) assert.Equal(t, "proxy-"+cacheKey, vfs.Fs().Name())
@@ -129,7 +166,7 @@ func TestRun(t *testing.T) {
}) })
// now try again from the cache // now try again from the cache
vfs, vfsKey, err = p.Call(testUser, testPass, false) vfs, vfsKey, err = p.Call(testUser, testPass, false, testAddr)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, vfs) require.NotNil(t, vfs)
assert.Equal(t, "proxy-"+cacheKey, vfs.Fs().Name()) assert.Equal(t, "proxy-"+cacheKey, vfs.Fs().Name())
@@ -141,7 +178,7 @@ func TestRun(t *testing.T) {
// A different password produces a different cache key, so it // A different password produces a different cache key, so it
// creates a fresh cache entry rather than hitting the existing // creates a fresh cache entry rather than hitting the existing
// one. Authentication itself is the proxy script's job. // one. Authentication itself is the proxy script's job.
vfs2, vfsKey2, err := p.Call(testUser, testPass+"different", false) vfs2, vfsKey2, err := p.Call(testUser, testPass+"different", false, testAddr)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, vfs2) require.NotNil(t, vfs2)
assert.NotEqual(t, cacheKey, vfsKey2) assert.NotEqual(t, cacheKey, vfsKey2)
@@ -152,18 +189,40 @@ func TestRun(t *testing.T) {
t.Error("fs/cache returned the stale backend after auth change") t.Error("fs/cache returned the stale backend after auth change")
} }
// A different client IP also produces a different cache key, so
// the proxy is consulted again rather than the cached backend
// being reused - the proxy may be filtering on the IP.
vfs3, vfsKey3, err := p.Call(testUser, testPass, false, otherAddr)
require.NoError(t, err)
require.NotNil(t, vfs3)
assert.NotEqual(t, cacheKey, vfsKey3)
assert.Equal(t, 3, p.vfsCache.Entries())
// If a cached entry's pwHash somehow doesn't match the supplied // If a cached entry's pwHash somehow doesn't match the supplied
// auth (eg a hash collision on the cache key), Call must reject // auth (eg a hash collision on the cache key), Call must reject
// it. Simulate by corrupting the cached pwHash. // it. Simulate by corrupting the cached pwHash.
entry := cacheEntry{vfs: vfs, pwHash: sha256.Sum256([]byte("tampered"))} entry := cacheEntry{vfs: vfs, pwHash: sha256.Sum256([]byte("tampered"))}
p.vfsCache.Put(cacheKey, entry) p.vfsCache.Put(cacheKey, entry)
vfs, vfsKey, err = p.Call(testUser, testPass, false) vfs, vfsKey, err = p.Call(testUser, testPass, false, testAddr)
require.Error(t, err) require.Error(t, err)
require.Contains(t, err.Error(), "incorrect password") require.Contains(t, err.Error(), "incorrect password")
require.Nil(t, vfs) require.Nil(t, vfs)
require.Equal(t, "", vfsKey) require.Equal(t, "", vfsKey)
}) })
t.Run("Call w/o Address", func(t *testing.T) {
// A client with no address, eg on a unix socket, must still
// authenticate
assert.Equal(t, 0, p.vfsCache.Entries())
defer p.vfsCache.Clear()
vfs, vfsKey, err := p.Call(testUser, testPass, false, "")
require.NoError(t, err)
require.NotNil(t, vfs)
assert.Equal(t, generateCacheKey(testUser, testPass, ""), vfsKey)
assert.Equal(t, 1, p.vfsCache.Entries())
})
privateKey, privateKeyErr := rsa.GenerateKey(rand.Reader, 2048) privateKey, privateKeyErr := rsa.GenerateKey(rand.Reader, 2048)
if privateKeyErr != nil { if privateKeyErr != nil {
fs.Fatal(nil, "error generating test private key "+privateKeyErr.Error()) fs.Fatal(nil, "error generating test private key "+privateKeyErr.Error())
@@ -180,7 +239,7 @@ func TestRun(t *testing.T) {
assert.Equal(t, 0, p.vfsCache.Entries()) assert.Equal(t, 0, p.vfsCache.Entries())
defer p.vfsCache.Clear() defer p.vfsCache.Clear()
value, err := p.call(testUser, publicKeyString, true) value, err := p.call(testUser, publicKeyString, true, testIP)
require.NoError(t, err) require.NoError(t, err)
entry, ok := value.(cacheEntry) entry, ok := value.(cacheEntry)
require.True(t, ok) require.True(t, ok)
@@ -190,7 +249,7 @@ func TestRun(t *testing.T) {
require.NotNil(t, entry.vfs) require.NotNil(t, entry.vfs)
f := entry.vfs.Fs() f := entry.vfs.Fs()
require.NotNil(t, f) require.NotNil(t, f)
cacheKey := generateCacheKey(testUser, publicKeyString) cacheKey := generateCacheKey(testUser, publicKeyString, testIP)
assert.Equal(t, "proxy-"+cacheKey, f.Name()) assert.Equal(t, "proxy-"+cacheKey, f.Name())
assert.True(t, strings.HasPrefix(f.String(), "Local file system")) assert.True(t, strings.HasPrefix(f.String(), "Local file system"))
@@ -206,11 +265,12 @@ func TestRun(t *testing.T) {
assert.Equal(t, 0, p.vfsCache.Entries()) assert.Equal(t, 0, p.vfsCache.Entries())
defer p.vfsCache.Clear() defer p.vfsCache.Clear()
cacheKey := generateCacheKey(testUser, publicKeyString) cacheKey := generateCacheKey(testUser, publicKeyString, testIP)
vfs, vfsKey, err := p.Call( vfs, vfsKey, err := p.Call(
testUser, testUser,
publicKeyString, publicKeyString,
true, true,
testAddr,
) )
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, vfs) require.NotNil(t, vfs)
@@ -232,7 +292,7 @@ func TestRun(t *testing.T) {
}) })
// now try again from the cache // now try again from the cache
vfs, vfsKey, err = p.Call(testUser, publicKeyString, true) vfs, vfsKey, err = p.Call(testUser, publicKeyString, true, testAddr)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, vfs) require.NotNil(t, vfs)
assert.Equal(t, "proxy-"+cacheKey, vfs.Fs().Name()) assert.Equal(t, "proxy-"+cacheKey, vfs.Fs().Name())
@@ -244,7 +304,7 @@ func TestRun(t *testing.T) {
// A different public key produces a different cache key, so it // A different public key produces a different cache key, so it
// creates a fresh cache entry rather than hitting the existing // creates a fresh cache entry rather than hitting the existing
// one. Authentication itself is the proxy script's job. // one. Authentication itself is the proxy script's job.
vfs2, vfsKey2, err := p.Call(testUser, publicKeyString+"different", true) vfs2, vfsKey2, err := p.Call(testUser, publicKeyString+"different", true, testAddr)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, vfs2) require.NotNil(t, vfs2)
assert.NotEqual(t, cacheKey, vfsKey2) assert.NotEqual(t, cacheKey, vfsKey2)
@@ -260,10 +320,30 @@ func TestRun(t *testing.T) {
// it. Simulate by corrupting the cached pwHash. // it. Simulate by corrupting the cached pwHash.
entry := cacheEntry{vfs: vfs, pwHash: sha256.Sum256([]byte("tampered"))} entry := cacheEntry{vfs: vfs, pwHash: sha256.Sum256([]byte("tampered"))}
p.vfsCache.Put(cacheKey, entry) p.vfsCache.Put(cacheKey, entry)
vfs, vfsKey, err = p.Call(testUser, publicKeyString, true) vfs, vfsKey, err = p.Call(testUser, publicKeyString, true, testAddr)
require.Error(t, err) require.Error(t, err)
require.Contains(t, err.Error(), "incorrect public key") require.Contains(t, err.Error(), "incorrect public key")
require.Nil(t, vfs) require.Nil(t, vfs)
require.Equal(t, "", vfsKey) require.Equal(t, "", vfsKey)
}) })
} }
func TestIPFromAddr(t *testing.T) {
for _, test := range []struct {
in string
want string
}{
{"192.0.2.1:1024", "192.0.2.1"},
{"[2001:db8::1]:1024", "2001:db8::1"},
{"[::ffff:192.0.2.1]:1024", "192.0.2.1"},
{"[fe80::1%eth0]:1024", "fe80::1%eth0"},
{"/tmp/rclone.sock", ""},
{"/tmp/foo:bar.sock", ""},
{`C:\Users\me\rclone.sock`, ""},
{"@", ""},
{"<nil>", ""},
{"", ""},
} {
assert.Equal(t, test.want, ipFromAddr(test.in), test.in)
}
}
+3 -3
View File
@@ -133,8 +133,8 @@ func (w *Server) getVFS(ctx context.Context) (VFS *vfs.VFS, err error) {
} }
// auth does proxy authorization // auth does proxy authorization
func (w *Server) auth(accessKeyID string) (value any, err error) { func (w *Server) auth(r *http.Request, accessKeyID string) (value any, err error) {
VFS, _, err := w.proxy.Call(stringToMd5Hash(accessKeyID), accessKeyID, false) VFS, _, err := w.proxy.Call(stringToMd5Hash(accessKeyID), accessKeyID, false, r.RemoteAddr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -179,7 +179,7 @@ func authPairMiddleware(next http.Handler, ws *Server) http.Handler {
func proxyAuthMiddleware(next http.Handler, ws *Server) http.Handler { func proxyAuthMiddleware(next http.Handler, ws *Server) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
accessKey, _ := parseAccessKeyID(r) accessKey, _ := parseAccessKeyID(r)
value, err := ws.auth(accessKey) value, err := ws.auth(r, accessKey)
if err != nil { if err != nil {
fs.Infof(r.URL.Path, "%s: Auth failed: %v", r.RemoteAddr, err) fs.Infof(r.URL.Path, "%s: Auth failed: %v", r.RemoteAddr, err)
} }
+2 -1
View File
@@ -172,7 +172,7 @@ func (s *server) configure() (err error) {
fs.Debugf(describeConn(c), "Password login attempt for %s", c.User()) fs.Debugf(describeConn(c), "Password login attempt for %s", c.User())
if s.proxy != nil { if s.proxy != nil {
// query the proxy for the config // query the proxy for the config
_, vfsKey, err := s.proxy.Call(c.User(), string(pass), false) _, vfsKey, err := s.proxy.Call(c.User(), string(pass), false, c.RemoteAddr().String())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -199,6 +199,7 @@ func (s *server) configure() (err error) {
c.User(), c.User(),
base64.StdEncoding.EncodeToString(pubKey.Marshal()), base64.StdEncoding.EncodeToString(pubKey.Marshal()),
true, true,
c.RemoteAddr().String(),
) )
if err != nil { if err != nil {
return nil, err return nil, err
+2 -2
View File
@@ -350,8 +350,8 @@ func (w *WebDAV) getVFS(ctx context.Context) (VFS *vfs.VFS, err error) {
} }
// auth does proxy authorization // auth does proxy authorization
func (w *WebDAV) auth(user, pass string) (value any, err error) { func (w *WebDAV) auth(r *http.Request, user, pass string) (value any, err error) {
VFS, _, err := w.proxy.Call(user, pass, false) VFS, _, err := w.proxy.Call(user, pass, false, r.RemoteAddr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+5 -1
View File
@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"html/template" "html/template"
"net/http"
"github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config/flags" "github.com/rclone/rclone/fs/config/flags"
@@ -68,8 +69,11 @@ Use ` + "`--{{ .Prefix }}salt`" + ` to change the password hashing salt from the
// CustomAuthFn if used will be used to authenticate user, pass. If an error // CustomAuthFn if used will be used to authenticate user, pass. If an error
// is returned then the user is not authenticated. // is returned then the user is not authenticated.
// //
// r is the request being authenticated, for example to find the address
// the client connected from in r.RemoteAddr.
//
// If a non nil value is returned then it is added to the context under the key // If a non nil value is returned then it is added to the context under the key
type CustomAuthFn func(user, pass string) (value any, err error) type CustomAuthFn func(r *http.Request, user, pass string) (value any, err error)
// AuthConfigInfo descripts the Options in use // AuthConfigInfo descripts the Options in use
var AuthConfigInfo = fs.Options{{ var AuthConfigInfo = fs.Options{{
+1 -1
View File
@@ -142,7 +142,7 @@ func MiddlewareAuthCustom(fn CustomAuthFn, realm string, userFromContext bool) M
return return
} }
value, err := fn(user, pass) value, err := fn(r, user, pass)
if err != nil { if err != nil {
fs.Infof(r.URL.Path, "%s: Auth failed from %s: %v", r.RemoteAddr, user, err) 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 goauth.NewBasicAuthenticator(realm, func(user, realm string) string { return "" }).RequireAuth(w, r) //Reuse BasicAuth error reporting
+4 -4
View File
@@ -79,8 +79,8 @@ func TestMiddlewareAuth(t *testing.T) {
}, },
auth: AuthConfig{ auth: AuthConfig{
Realm: "test", Realm: "test",
CustomAuthFn: func(user, pass string) (value any, err error) { CustomAuthFn: func(r *http.Request, user, pass string) (value any, err error) {
if user == "custom" && pass == "custom" { if user == "custom" && pass == "custom" && r.RemoteAddr != "" {
return true, nil return true, nil
} }
return nil, errors.New("invalid credentials") return nil, errors.New("invalid credentials")
@@ -304,7 +304,7 @@ func TestMiddlewareAuthCertificateUser(t *testing.T) {
}, },
auth: AuthConfig{ auth: AuthConfig{
Realm: "test", Realm: "test",
CustomAuthFn: func(user, pass string) (value any, err error) { CustomAuthFn: func(_ *http.Request, user, pass string) (value any, err error) {
if user == "custom" && pass == "custom" { if user == "custom" && pass == "custom" {
return true, nil return true, nil
} }
@@ -326,7 +326,7 @@ func TestMiddlewareAuthCertificateUser(t *testing.T) {
}, },
auth: AuthConfig{ auth: AuthConfig{
Realm: "test", Realm: "test",
CustomAuthFn: func(user, pass string) (value any, err error) { CustomAuthFn: func(_ *http.Request, user, pass string) (value any, err error) {
fmt.Println("CUSTOMAUTH", user, pass) fmt.Println("CUSTOMAUTH", user, pass)
if user == "rclone-dev-client" && pass == "" { if user == "rclone-dev-client" && pass == "" {
return true, nil return true, nil