2019-07-31 23:20:19 +02:00
|
|
|
// Package proxy implements a programmable proxy for rclone serve
|
|
|
|
package proxy
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
|
|
|
"os/exec"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/rclone/rclone/fs"
|
|
|
|
"github.com/rclone/rclone/fs/cache"
|
|
|
|
"github.com/rclone/rclone/fs/config/configmap"
|
|
|
|
"github.com/rclone/rclone/fs/config/obscure"
|
|
|
|
libcache "github.com/rclone/rclone/lib/cache"
|
|
|
|
"github.com/rclone/rclone/vfs"
|
|
|
|
"github.com/rclone/rclone/vfs/vfsflags"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Help contains text describing how to use the proxy
|
|
|
|
var Help = strings.Replace(`
|
|
|
|
### Auth Proxy
|
|
|
|
|
|
|
|
If you supply the parameter |--auth-proxy /path/to/program| then
|
|
|
|
rclone will use that program to generate backends on the fly which
|
|
|
|
then are used to authenticate incoming requests. This uses a simple
|
|
|
|
JSON based protocl with input on STDIN and output on STDOUT.
|
|
|
|
|
|
|
|
There is an example program
|
|
|
|
[bin/test_proxy.py](https://github.com/rclone/rclone/blob/master/test_proxy.py)
|
|
|
|
in the rclone source code.
|
|
|
|
|
|
|
|
The program's job is to take a |user| and |pass| on the input and turn
|
|
|
|
those into the config for a backend on STDOUT in JSON format. This
|
|
|
|
config will have any default parameters for the backend added, but it
|
|
|
|
won't use configuration from environment variables or command line
|
|
|
|
options - it is the job of the proxy program to make a complete
|
|
|
|
config.
|
|
|
|
|
|
|
|
This config generated must have this extra parameter
|
|
|
|
- |_root| - root to use for the backend
|
|
|
|
|
|
|
|
And it may have this parameter
|
|
|
|
- |_obscure| - comma separated strings for parameters to obscure
|
|
|
|
|
|
|
|
For example the program might take this on STDIN
|
|
|
|
|
|
|
|
|||
|
|
|
|
{
|
|
|
|
"user": "me",
|
|
|
|
"pass": "mypassword"
|
|
|
|
}
|
|
|
|
|||
|
|
|
|
|
|
|
|
And return this on STDOUT
|
|
|
|
|
|
|
|
|||
|
|
|
|
{
|
|
|
|
"type": "sftp",
|
|
|
|
"_root": "",
|
|
|
|
"_obscure": "pass",
|
|
|
|
"user": "me",
|
|
|
|
"pass": "mypassword",
|
|
|
|
"host": "sftp.example.com"
|
|
|
|
}
|
|
|
|
|||
|
|
|
|
|
|
|
|
This would mean that an SFTP backend would be created on the fly for
|
|
|
|
the |user| and |pass| returned in the output to the host given. Note
|
|
|
|
that since |_obscure| is set to |pass|, rclone will obscure the |pass|
|
|
|
|
parameter before creating the backend (which is required for sftp
|
|
|
|
backends).
|
|
|
|
|
|
|
|
The progam can manipulate the supplied |user| in any way, for example
|
|
|
|
to make proxy to many different sftp backends, you could make the
|
|
|
|
|user| be |user@example.com| and then set the |host| to |example.com|
|
|
|
|
in the output and the user to |user|. For security you'd probably want
|
|
|
|
to restrict the |host| to a limited list.
|
|
|
|
|
|
|
|
Note that an internal cache is keyed on |user| so only use that for
|
|
|
|
configuration, don't use |pass|. This also means that if a user's
|
|
|
|
password is changed the cache will need to expire (which takes 5 mins)
|
|
|
|
before it takes effect.
|
|
|
|
|
|
|
|
This can be used to build general purpose proxies to any kind of
|
|
|
|
backend that rclone supports.
|
|
|
|
`, "|", "`", -1)
|
|
|
|
|
|
|
|
// Options is options for creating the proxy
|
|
|
|
type Options struct {
|
|
|
|
AuthProxy string
|
|
|
|
}
|
|
|
|
|
|
|
|
// DefaultOpt is the default values uses for Opt
|
|
|
|
var DefaultOpt = Options{
|
|
|
|
AuthProxy: "",
|
|
|
|
}
|
|
|
|
|
|
|
|
// Proxy represents a proxy to turn auth requests into a VFS
|
|
|
|
type Proxy struct {
|
|
|
|
cmdLine []string // broken down command line
|
|
|
|
vfsCache *libcache.Cache
|
|
|
|
Opt Options
|
|
|
|
}
|
|
|
|
|
|
|
|
// cacheEntry is what is stored in the vfsCache
|
|
|
|
type cacheEntry struct {
|
|
|
|
vfs *vfs.VFS // stored VFS
|
|
|
|
pwHash []byte // bcrypt hash of the password
|
|
|
|
}
|
|
|
|
|
|
|
|
// New creates a new proxy with the Options passed in
|
|
|
|
func New(opt *Options) *Proxy {
|
|
|
|
return &Proxy{
|
|
|
|
Opt: *opt,
|
|
|
|
cmdLine: strings.Fields(opt.AuthProxy),
|
|
|
|
vfsCache: libcache.New(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// run the proxy command returning a config map
|
|
|
|
func (p *Proxy) run(in map[string]string) (config configmap.Simple, err error) {
|
|
|
|
cmd := exec.Command(p.cmdLine[0], p.cmdLine[1:]...)
|
|
|
|
inBytes, err := json.MarshalIndent(in, "", "\t")
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "Proxy.Call failed to marshal input: %v")
|
|
|
|
}
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
|
|
cmd.Stdin = bytes.NewBuffer(inBytes)
|
|
|
|
cmd.Stdout = &stdout
|
|
|
|
cmd.Stderr = &stderr
|
|
|
|
start := time.Now()
|
|
|
|
err = cmd.Run()
|
|
|
|
fs.Debugf(nil, "Calling proxy %v", p.cmdLine)
|
|
|
|
duration := time.Since(start)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "proxy: failed on %v: %q", p.cmdLine, strings.TrimSpace(string(stderr.Bytes())))
|
|
|
|
}
|
|
|
|
err = json.Unmarshal(stdout.Bytes(), &config)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "proxy: failed to read output: %q", string(stdout.Bytes()))
|
|
|
|
}
|
|
|
|
fs.Debugf(nil, "Proxy returned in %v", duration)
|
|
|
|
|
|
|
|
// Obscure any values in the config map that need it
|
|
|
|
obscureFields, ok := config.Get("_obscure")
|
|
|
|
if ok {
|
|
|
|
for _, key := range strings.Split(obscureFields, ",") {
|
|
|
|
value, ok := config.Get(key)
|
|
|
|
if ok {
|
|
|
|
obscuredValue, err := obscure.Obscure(value)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "proxy")
|
|
|
|
}
|
|
|
|
config.Set(key, obscuredValue)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return config, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// call runs the auth proxy and returns a cacheEntry and an error
|
|
|
|
func (p *Proxy) call(user, pass string, passwordBytes []byte) (value interface{}, err error) {
|
|
|
|
// Contact the proxy
|
|
|
|
config, err := p.run(map[string]string{
|
|
|
|
"user": user,
|
|
|
|
"pass": pass,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Look for required fields in the answer
|
|
|
|
fsName, ok := config.Get("type")
|
|
|
|
if !ok {
|
|
|
|
return nil, errors.New("proxy: type not set in result")
|
|
|
|
}
|
|
|
|
root, ok := config.Get("_root")
|
|
|
|
if !ok {
|
|
|
|
return nil, errors.New("proxy: _root not set in result")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find the backend
|
|
|
|
fsInfo, err := fs.Find(fsName)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "proxy: couldn't find backend for %q", fsName)
|
|
|
|
}
|
|
|
|
|
|
|
|
// base name of config on user name. This may appear in logs
|
|
|
|
name := "proxy-" + user
|
|
|
|
fsString := name + ":" + root
|
|
|
|
|
|
|
|
// Look for fs in the VFS cache
|
|
|
|
value, err = p.vfsCache.Get(user, func(key string) (value interface{}, ok bool, err error) {
|
|
|
|
// Create the Fs from the cache
|
|
|
|
f, err := cache.GetFn(fsString, func(fsString string) (fs.Fs, error) {
|
|
|
|
// Update the config with the default values
|
|
|
|
for i := range fsInfo.Options {
|
|
|
|
o := &fsInfo.Options[i]
|
|
|
|
if _, found := config.Get(o.Name); !found && o.Default != nil && o.String() != "" {
|
|
|
|
config.Set(o.Name, o.String())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return fsInfo.NewFs(name, root, config)
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, false, err
|
|
|
|
}
|
2019-11-07 15:06:02 +01:00
|
|
|
// The bcrypt cost is a compromise between security and speed. The password is looked up on every
|
|
|
|
// transaction for WebDAV so we store it lightly hashed. An attacker would find it easier to go after
|
|
|
|
// the unencrypted password in memory most likely.
|
|
|
|
pwHash, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.MinCost)
|
2019-07-31 23:20:19 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, false, err
|
|
|
|
}
|
|
|
|
entry := cacheEntry{
|
|
|
|
vfs: vfs.New(f, &vfsflags.Opt),
|
|
|
|
pwHash: pwHash,
|
|
|
|
}
|
|
|
|
return entry, true, nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "proxy: failed to create backend")
|
|
|
|
}
|
|
|
|
return value, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Call runs the auth proxy with the given input, returning a *vfs.VFS
|
|
|
|
// and the key used in the VFS cache.
|
|
|
|
func (p *Proxy) Call(user, pass string) (VFS *vfs.VFS, vfsKey string, err error) {
|
|
|
|
var passwordBytes = []byte(pass)
|
|
|
|
|
|
|
|
// Look in the cache first
|
|
|
|
value, ok := p.vfsCache.GetMaybe(user)
|
|
|
|
|
|
|
|
// If not found then call the proxy for a fresh answer
|
|
|
|
if !ok {
|
|
|
|
value, err = p.call(user, pass, passwordBytes)
|
|
|
|
if err != nil {
|
|
|
|
return nil, "", err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// check we got what we were expecting
|
|
|
|
entry, ok := value.(cacheEntry)
|
|
|
|
if !ok {
|
|
|
|
return nil, "", errors.Errorf("proxy: value is not cache entry: %#v", value)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check the password is correct in the cached entry. This
|
|
|
|
// prevents an attack where subsequent requests for the same
|
|
|
|
// user don't have their auth checked. It does mean that if
|
|
|
|
// the password is changed, the user will have to wait for
|
|
|
|
// cache expiry (5m) before trying again.
|
|
|
|
err = bcrypt.CompareHashAndPassword(entry.pwHash, passwordBytes)
|
|
|
|
if err != nil {
|
|
|
|
return nil, "", errors.Wrap(err, "proxy: incorrect password")
|
|
|
|
}
|
|
|
|
|
|
|
|
return entry.vfs, user, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get VFS from the cache using key - returns nil if not found
|
|
|
|
func (p *Proxy) Get(key string) *vfs.VFS {
|
|
|
|
value, ok := p.vfsCache.GetMaybe(key)
|
|
|
|
if !ok {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
entry := value.(cacheEntry)
|
|
|
|
return entry.vfs
|
|
|
|
}
|