mirror of
https://github.com/rclone/rclone.git
synced 2024-11-25 18:04:55 +01:00
config: Wrap config library in an interface
This commit is contained in:
parent
2be310cd6e
commit
c95b580478
@ -3,19 +3,12 @@ package config
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
mathrand "math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
@ -23,11 +16,10 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/Unknwon/goconfig"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/rclone/rclone/fs"
|
||||
"github.com/rclone/rclone/fs/accounting"
|
||||
"github.com/rclone/rclone/fs/config/configmap"
|
||||
@ -38,8 +30,6 @@ import (
|
||||
"github.com/rclone/rclone/fs/rc"
|
||||
"github.com/rclone/rclone/lib/random"
|
||||
"github.com/rclone/rclone/lib/terminal"
|
||||
"golang.org/x/crypto/nacl/secretbox"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -74,32 +64,56 @@ const (
|
||||
ConfigAuthNoBrowser = "config_auth_no_browser"
|
||||
)
|
||||
|
||||
// Storage defines an interface for loading and saving the config file.
|
||||
type Storage interface {
|
||||
// GetSectionList returns a slice of strings with names for all the
|
||||
// sections
|
||||
GetSectionList() []string
|
||||
|
||||
// HasSection returns true if section exists in the config file
|
||||
HasSection(section string) bool
|
||||
|
||||
// DeleteSection removes the named section and all config from the
|
||||
// config file
|
||||
DeleteSection(section string)
|
||||
|
||||
// GetKeyList returns the keys in this section
|
||||
GetKeyList(section string) []string
|
||||
|
||||
// GetValue returns the key in section or an error if not found
|
||||
GetValue(section string, key string) (string, error)
|
||||
|
||||
// MustValue returns the key in section returning defaultValue if not set
|
||||
MustValue(section string, key string, defaultValue ...string) string
|
||||
|
||||
// SetValue sets the value under key in section
|
||||
SetValue(section string, key string, value string)
|
||||
|
||||
// DeleteKey removes the key under section
|
||||
DeleteKey(section string, key string) bool
|
||||
|
||||
// Load the config from permanent storage
|
||||
Load() error
|
||||
|
||||
// Save the config to permanent storage
|
||||
Save() error
|
||||
|
||||
// Serialize the config into a string
|
||||
Serialize() (string, error)
|
||||
}
|
||||
|
||||
// Global
|
||||
var (
|
||||
// configFile is the global config data structure. Don't read it directly, use getConfigData()
|
||||
configFile *goconfig.ConfigFile
|
||||
|
||||
// ConfigPath points to the config file
|
||||
ConfigPath = makeConfigPath()
|
||||
// configFile is the global config data structure. Don't read it directly, use Data
|
||||
Data Storage
|
||||
|
||||
// CacheDir points to the cache directory. Users of this
|
||||
// should make a subdirectory and use MkdirAll() to create it
|
||||
// and any parents.
|
||||
CacheDir = makeCacheDir()
|
||||
|
||||
// Key to use for password en/decryption.
|
||||
// When nil, no encryption will be used for saving.
|
||||
configKey []byte
|
||||
|
||||
// output of prompt for password
|
||||
PasswordPromptOutput = os.Stderr
|
||||
|
||||
// If set to true, the configKey is obscured with obscure.Obscure and saved to a temp file when it is
|
||||
// calculated from the password. The path of that temp file is then written to the environment variable
|
||||
// `_RCLONE_CONFIG_KEY_FILE`. If `_RCLONE_CONFIG_KEY_FILE` is present, password prompt is skipped and `RCLONE_CONFIG_PASS` ignored.
|
||||
// For security reasons, the temp file is deleted once the configKey is successfully loaded.
|
||||
// This can be used to pass the configKey to a child process.
|
||||
PassConfigKeyForDaemonization = false
|
||||
// ConfigPath points to the config file
|
||||
ConfigPath = makeConfigPath()
|
||||
|
||||
// Password can be used to configure the random password generator
|
||||
Password = random.Password
|
||||
@ -111,16 +125,8 @@ func init() {
|
||||
fs.ConfigFileSet = SetValueAndSave
|
||||
}
|
||||
|
||||
func getConfigData() *goconfig.ConfigFile {
|
||||
if configFile == nil {
|
||||
LoadConfig(context.Background())
|
||||
}
|
||||
return configFile
|
||||
}
|
||||
|
||||
// Return the path to the configuration file
|
||||
func makeConfigPath() string {
|
||||
|
||||
// Use rclone.conf from rclone executable directory if already existing
|
||||
exe, err := os.Executable()
|
||||
if err == nil {
|
||||
@ -217,11 +223,8 @@ func LoadConfig(ctx context.Context) {
|
||||
_ = os.Setenv("RCLONE_CONFIG_DIR", filepath.Dir(ConfigPath))
|
||||
|
||||
// Load configuration file.
|
||||
var err error
|
||||
configFile, err = loadConfigFile()
|
||||
if err == errorConfigFileNotFound {
|
||||
if err := Data.Load(); err == ErrorConfigFileNotFound {
|
||||
fs.Logf(nil, "Config file %q not found - using defaults", ConfigPath)
|
||||
configFile, _ = goconfig.LoadFromReader(&bytes.Buffer{})
|
||||
} else if err != nil {
|
||||
log.Fatalf("Failed to load config file %q: %v", ConfigPath, err)
|
||||
} else {
|
||||
@ -238,365 +241,8 @@ func LoadConfig(ctx context.Context) {
|
||||
accounting.StartLimitTPS(ctx)
|
||||
}
|
||||
|
||||
var errorConfigFileNotFound = errors.New("config file not found")
|
||||
|
||||
// loadConfigFile will load a config file, and
|
||||
// automatically decrypt it.
|
||||
func loadConfigFile() (*goconfig.ConfigFile, error) {
|
||||
ctx := context.Background()
|
||||
ci := fs.GetConfig(ctx)
|
||||
var usingPasswordCommand bool
|
||||
|
||||
b, err := ioutil.ReadFile(ConfigPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, errorConfigFileNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
// Find first non-empty line
|
||||
r := bufio.NewReader(bytes.NewBuffer(b))
|
||||
for {
|
||||
line, _, err := r.ReadLine()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return goconfig.LoadFromReader(bytes.NewBuffer(b))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
l := strings.TrimSpace(string(line))
|
||||
if len(l) == 0 || strings.HasPrefix(l, ";") || strings.HasPrefix(l, "#") {
|
||||
continue
|
||||
}
|
||||
// First non-empty or non-comment must be ENCRYPT_V0
|
||||
if l == "RCLONE_ENCRYPT_V0:" {
|
||||
break
|
||||
}
|
||||
if strings.HasPrefix(l, "RCLONE_ENCRYPT_V") {
|
||||
return nil, errors.New("unsupported configuration encryption - update rclone for support")
|
||||
}
|
||||
return goconfig.LoadFromReader(bytes.NewBuffer(b))
|
||||
}
|
||||
|
||||
if len(configKey) == 0 {
|
||||
if len(ci.PasswordCommand) != 0 {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
cmd := exec.Command(ci.PasswordCommand[0], ci.PasswordCommand[1:]...)
|
||||
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
// One does not always get the stderr returned in the wrapped error.
|
||||
fs.Errorf(nil, "Using --password-command returned: %v", err)
|
||||
if ers := strings.TrimSpace(stderr.String()); ers != "" {
|
||||
fs.Errorf(nil, "--password-command stderr: %s", ers)
|
||||
}
|
||||
return nil, errors.Wrap(err, "password command failed")
|
||||
}
|
||||
if pass := strings.Trim(stdout.String(), "\r\n"); pass != "" {
|
||||
err := setConfigPassword(pass)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "incorrect password")
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New("password-command returned empty string")
|
||||
}
|
||||
|
||||
if len(configKey) == 0 {
|
||||
return nil, errors.New("unable to decrypt configuration: incorrect password")
|
||||
}
|
||||
usingPasswordCommand = true
|
||||
} else {
|
||||
usingPasswordCommand = false
|
||||
|
||||
envpw := os.Getenv("RCLONE_CONFIG_PASS")
|
||||
|
||||
if envpw != "" {
|
||||
err := setConfigPassword(envpw)
|
||||
if err != nil {
|
||||
fs.Errorf(nil, "Using RCLONE_CONFIG_PASS returned: %v", err)
|
||||
} else {
|
||||
fs.Debugf(nil, "Using RCLONE_CONFIG_PASS password.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypted content is base64 encoded.
|
||||
dec := base64.NewDecoder(base64.StdEncoding, r)
|
||||
box, err := ioutil.ReadAll(dec)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to load base64 encoded data")
|
||||
}
|
||||
if len(box) < 24+secretbox.Overhead {
|
||||
return nil, errors.New("Configuration data too short")
|
||||
}
|
||||
|
||||
var out []byte
|
||||
for {
|
||||
if envKeyFile := os.Getenv("_RCLONE_CONFIG_KEY_FILE"); len(envKeyFile) > 0 {
|
||||
fs.Debugf(nil, "attempting to obtain configKey from temp file %s", envKeyFile)
|
||||
obscuredKey, err := ioutil.ReadFile(envKeyFile)
|
||||
if err != nil {
|
||||
errRemove := os.Remove(envKeyFile)
|
||||
if errRemove != nil {
|
||||
log.Fatalf("unable to read obscured config key and unable to delete the temp file: %v", err)
|
||||
}
|
||||
log.Fatalf("unable to read obscured config key: %v", err)
|
||||
}
|
||||
errRemove := os.Remove(envKeyFile)
|
||||
if errRemove != nil {
|
||||
log.Fatalf("unable to delete temp file with configKey: %v", err)
|
||||
}
|
||||
configKey = []byte(obscure.MustReveal(string(obscuredKey)))
|
||||
fs.Debugf(nil, "using _RCLONE_CONFIG_KEY_FILE for configKey")
|
||||
} else {
|
||||
if len(configKey) == 0 {
|
||||
if usingPasswordCommand {
|
||||
return nil, errors.New("using --password-command derived password, unable to decrypt configuration")
|
||||
}
|
||||
if !ci.AskPassword {
|
||||
return nil, errors.New("unable to decrypt configuration and not allowed to ask for password - set RCLONE_CONFIG_PASS to your configuration password")
|
||||
}
|
||||
getConfigPassword("Enter configuration password:")
|
||||
}
|
||||
}
|
||||
|
||||
// Nonce is first 24 bytes of the ciphertext
|
||||
var nonce [24]byte
|
||||
copy(nonce[:], box[:24])
|
||||
var key [32]byte
|
||||
copy(key[:], configKey[:32])
|
||||
|
||||
// Attempt to decrypt
|
||||
var ok bool
|
||||
out, ok = secretbox.Open(nil, box[24:], &nonce, &key)
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
|
||||
// Retry
|
||||
fs.Errorf(nil, "Couldn't decrypt configuration, most likely wrong password.")
|
||||
configKey = nil
|
||||
}
|
||||
return goconfig.LoadFromReader(bytes.NewBuffer(out))
|
||||
}
|
||||
|
||||
// checkPassword normalises and validates the password
|
||||
func checkPassword(password string) (string, error) {
|
||||
if !utf8.ValidString(password) {
|
||||
return "", errors.New("password contains invalid utf8 characters")
|
||||
}
|
||||
// Check for leading/trailing whitespace
|
||||
trimmedPassword := strings.TrimSpace(password)
|
||||
// Warn user if password has leading+trailing whitespace
|
||||
if len(password) != len(trimmedPassword) {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "Your password contains leading/trailing whitespace - in previous versions of rclone this was stripped")
|
||||
}
|
||||
// Normalize to reduce weird variations.
|
||||
password = norm.NFKC.String(password)
|
||||
if len(password) == 0 || len(trimmedPassword) == 0 {
|
||||
return "", errors.New("no characters in password")
|
||||
}
|
||||
return password, nil
|
||||
}
|
||||
|
||||
// GetPassword asks the user for a password with the prompt given.
|
||||
func GetPassword(prompt string) string {
|
||||
_, _ = fmt.Fprintln(PasswordPromptOutput, prompt)
|
||||
for {
|
||||
_, _ = fmt.Fprint(PasswordPromptOutput, "password:")
|
||||
password := ReadPassword()
|
||||
password, err := checkPassword(password)
|
||||
if err == nil {
|
||||
return password
|
||||
}
|
||||
_, _ = fmt.Fprintf(os.Stderr, "Bad password: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ChangePassword will query the user twice for the named password. If
|
||||
// the same password is entered it is returned.
|
||||
func ChangePassword(name string) string {
|
||||
for {
|
||||
a := GetPassword(fmt.Sprintf("Enter %s password:", name))
|
||||
b := GetPassword(fmt.Sprintf("Confirm %s password:", name))
|
||||
if a == b {
|
||||
return a
|
||||
}
|
||||
fmt.Println("Passwords do not match!")
|
||||
}
|
||||
}
|
||||
|
||||
// getConfigPassword will query the user for a password the
|
||||
// first time it is required.
|
||||
func getConfigPassword(q string) {
|
||||
if len(configKey) != 0 {
|
||||
return
|
||||
}
|
||||
for {
|
||||
password := GetPassword(q)
|
||||
err := setConfigPassword(password)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
_, _ = fmt.Fprintln(os.Stderr, "Error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// setConfigPassword will set the configKey to the hash of
|
||||
// the password. If the length of the password is
|
||||
// zero after trimming+normalization, an error is returned.
|
||||
func setConfigPassword(password string) error {
|
||||
password, err := checkPassword(password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Create SHA256 has of the password
|
||||
sha := sha256.New()
|
||||
_, err = sha.Write([]byte("[" + password + "][rclone-config]"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configKey = sha.Sum(nil)
|
||||
if PassConfigKeyForDaemonization {
|
||||
tempFile, err := ioutil.TempFile("", "rclone")
|
||||
if err != nil {
|
||||
log.Fatalf("cannot create temp file to store configKey: %v", err)
|
||||
}
|
||||
_, err = tempFile.WriteString(obscure.MustObscure(string(configKey)))
|
||||
if err != nil {
|
||||
errRemove := os.Remove(tempFile.Name())
|
||||
if errRemove != nil {
|
||||
log.Fatalf("error writing configKey to temp file and also error deleting it: %v", err)
|
||||
}
|
||||
log.Fatalf("error writing configKey to temp file: %v", err)
|
||||
}
|
||||
err = tempFile.Close()
|
||||
if err != nil {
|
||||
errRemove := os.Remove(tempFile.Name())
|
||||
if errRemove != nil {
|
||||
log.Fatalf("error closing temp file with configKey and also error deleting it: %v", err)
|
||||
}
|
||||
log.Fatalf("error closing temp file with configKey: %v", err)
|
||||
}
|
||||
fs.Debugf(nil, "saving configKey to temp file")
|
||||
err = os.Setenv("_RCLONE_CONFIG_KEY_FILE", tempFile.Name())
|
||||
if err != nil {
|
||||
errRemove := os.Remove(tempFile.Name())
|
||||
if errRemove != nil {
|
||||
log.Fatalf("unable to set environment variable _RCLONE_CONFIG_KEY_FILE and unable to delete the temp file: %v", err)
|
||||
}
|
||||
log.Fatalf("unable to set environment variable _RCLONE_CONFIG_KEY_FILE: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// changeConfigPassword will query the user twice
|
||||
// for a password. If the same password is entered
|
||||
// twice the key is updated.
|
||||
func changeConfigPassword() {
|
||||
err := setConfigPassword(ChangePassword("NEW configuration"))
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to set config password: %v\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// saveConfig saves configuration file.
|
||||
// if configKey has been set, the file will be encrypted.
|
||||
func saveConfig() error {
|
||||
dir, name := filepath.Split(ConfigPath)
|
||||
err := os.MkdirAll(dir, os.ModePerm)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create config directory")
|
||||
}
|
||||
f, err := ioutil.TempFile(dir, name)
|
||||
if err != nil {
|
||||
return errors.Errorf("Failed to create temp file for new config: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.Remove(f.Name()); err != nil && !os.IsNotExist(err) {
|
||||
fs.Errorf(nil, "Failed to remove temp config file: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = goconfig.SaveConfigData(getConfigData(), &buf)
|
||||
if err != nil {
|
||||
return errors.Errorf("Failed to save config file: %v", err)
|
||||
}
|
||||
|
||||
if len(configKey) == 0 {
|
||||
if _, err := buf.WriteTo(f); err != nil {
|
||||
return errors.Errorf("Failed to write temp config file: %v", err)
|
||||
}
|
||||
} else {
|
||||
_, _ = fmt.Fprintln(f, "# Encrypted rclone configuration File")
|
||||
_, _ = fmt.Fprintln(f, "")
|
||||
_, _ = fmt.Fprintln(f, "RCLONE_ENCRYPT_V0:")
|
||||
|
||||
// Generate new nonce and write it to the start of the ciphertext
|
||||
var nonce [24]byte
|
||||
n, _ := rand.Read(nonce[:])
|
||||
if n != 24 {
|
||||
return errors.Errorf("nonce short read: %d", n)
|
||||
}
|
||||
enc := base64.NewEncoder(base64.StdEncoding, f)
|
||||
_, err = enc.Write(nonce[:])
|
||||
if err != nil {
|
||||
return errors.Errorf("Failed to write temp config file: %v", err)
|
||||
}
|
||||
|
||||
var key [32]byte
|
||||
copy(key[:], configKey[:32])
|
||||
|
||||
b := secretbox.Seal(nil, buf.Bytes(), &nonce, &key)
|
||||
_, err = enc.Write(b)
|
||||
if err != nil {
|
||||
return errors.Errorf("Failed to write temp config file: %v", err)
|
||||
}
|
||||
_ = enc.Close()
|
||||
}
|
||||
|
||||
_ = f.Sync()
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
return errors.Errorf("Failed to close config file: %v", err)
|
||||
}
|
||||
|
||||
var fileMode os.FileMode = 0600
|
||||
info, err := os.Stat(ConfigPath)
|
||||
if err != nil {
|
||||
fs.Debugf(nil, "Using default permissions for config file: %v", fileMode)
|
||||
} else if info.Mode() != fileMode {
|
||||
fs.Debugf(nil, "Keeping previous permissions for config file: %v", info.Mode())
|
||||
fileMode = info.Mode()
|
||||
}
|
||||
|
||||
attemptCopyGroup(ConfigPath, f.Name())
|
||||
|
||||
err = os.Chmod(f.Name(), fileMode)
|
||||
if err != nil {
|
||||
fs.Errorf(nil, "Failed to set permissions on config file: %v", err)
|
||||
}
|
||||
|
||||
if err = os.Rename(ConfigPath, ConfigPath+".old"); err != nil && !os.IsNotExist(err) {
|
||||
return errors.Errorf("Failed to move previous config to backup location: %v", err)
|
||||
}
|
||||
if err = os.Rename(f.Name(), ConfigPath); err != nil {
|
||||
return errors.Errorf("Failed to move newly written config from %s to final location: %v", f.Name(), err)
|
||||
}
|
||||
if err := os.Remove(ConfigPath + ".old"); err != nil && !os.IsNotExist(err) {
|
||||
fs.Errorf(nil, "Failed to remove backup config file: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// ErrorConfigFileNotFound is returned when the config file is not found
|
||||
var ErrorConfigFileNotFound = errors.New("config file not found")
|
||||
|
||||
// SaveConfig calling function which saves configuration file.
|
||||
// if saveConfig returns error trying again after sleep.
|
||||
@ -605,7 +251,7 @@ func SaveConfig() {
|
||||
ci := fs.GetConfig(ctx)
|
||||
var err error
|
||||
for i := 0; i < ci.LowLevelRetries+1; i++ {
|
||||
if err = saveConfig(); err == nil {
|
||||
if err = Data.Save(); err == nil {
|
||||
return
|
||||
}
|
||||
waitingTimeMs := mathrand.Intn(1000)
|
||||
@ -619,26 +265,22 @@ func SaveConfig() {
|
||||
// SetValueAndSave sets the key to the value and saves just that
|
||||
// value in the config file. It loads the old config file in from
|
||||
// disk first and overwrites the given value only.
|
||||
func SetValueAndSave(name, key, value string) (err error) {
|
||||
func SetValueAndSave(name, key, value string) error {
|
||||
// Set the value in config in case we fail to reload it
|
||||
getConfigData().SetValue(name, key, value)
|
||||
Data.SetValue(name, key, value)
|
||||
|
||||
// Reload the config file
|
||||
reloadedConfigFile, err := loadConfigFile()
|
||||
if err == errorConfigFileNotFound {
|
||||
err := Data.Load()
|
||||
if err == ErrorConfigFileNotFound {
|
||||
// Config file not written yet so ignore reload
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = reloadedConfigFile.GetSection(name)
|
||||
if err != nil {
|
||||
if !Data.HasSection(name) {
|
||||
// Section doesn't exist yet so ignore reload
|
||||
return nil
|
||||
}
|
||||
// Update the config file with the reloaded version
|
||||
configFile = reloadedConfigFile
|
||||
// Set the value in the reloaded version
|
||||
reloadedConfigFile.SetValue(name, key, value)
|
||||
// Save it again
|
||||
SaveConfig()
|
||||
return nil
|
||||
@ -648,16 +290,15 @@ func SetValueAndSave(name, key, value string) (err error) {
|
||||
// an error if the config file was not found or that value couldn't be
|
||||
// read.
|
||||
func FileGetFresh(section, key string) (value string, err error) {
|
||||
reloadedConfigFile, err := loadConfigFile()
|
||||
if err != nil {
|
||||
if err := Data.Load(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return reloadedConfigFile.GetValue(section, key)
|
||||
return Data.GetValue(section, key)
|
||||
}
|
||||
|
||||
// ShowRemotes shows an overview of the config file
|
||||
func ShowRemotes() {
|
||||
remotes := getConfigData().GetSectionList()
|
||||
remotes := Data.GetSectionList()
|
||||
if len(remotes) == 0 {
|
||||
return
|
||||
}
|
||||
@ -671,7 +312,7 @@ func ShowRemotes() {
|
||||
|
||||
// ChooseRemote chooses a remote name
|
||||
func ChooseRemote() string {
|
||||
remotes := getConfigData().GetSectionList()
|
||||
remotes := Data.GetSectionList()
|
||||
sort.Strings(remotes)
|
||||
return Choose("remote", remotes, nil, false)
|
||||
}
|
||||
@ -855,7 +496,7 @@ func ShowRemote(name string) {
|
||||
fmt.Printf("--------------------\n")
|
||||
fmt.Printf("[%s]\n", name)
|
||||
fs := MustFindByName(name)
|
||||
for _, key := range getConfigData().GetKeyList(name) {
|
||||
for _, key := range Data.GetKeyList(name) {
|
||||
isPassword := false
|
||||
for _, option := range fs.Options {
|
||||
if option.Name == key && option.IsPassword {
|
||||
@ -882,7 +523,7 @@ func OkRemote(name string) bool {
|
||||
case 'e':
|
||||
return false
|
||||
case 'd':
|
||||
getConfigData().DeleteSection(name)
|
||||
Data.DeleteSection(name)
|
||||
return true
|
||||
default:
|
||||
fs.Errorf(nil, "Bad choice %c", i)
|
||||
@ -942,7 +583,7 @@ func matchProvider(providerConfig, provider string) bool {
|
||||
|
||||
// ChooseOption asks the user to choose an option
|
||||
func ChooseOption(o *fs.Option, name string) string {
|
||||
var subProvider = getConfigData().MustValue(name, fs.ConfigProvider, "")
|
||||
var subProvider = Data.MustValue(name, fs.ConfigProvider, "")
|
||||
fmt.Println(o.Help)
|
||||
if o.IsPassword {
|
||||
actions := []string{"yYes type in my own password", "gGenerate random password"}
|
||||
@ -1079,7 +720,7 @@ func UpdateRemote(ctx context.Context, name string, keyValues rc.Params, doObscu
|
||||
}
|
||||
}
|
||||
}
|
||||
getConfigData().SetValue(name, k, vStr)
|
||||
Data.SetValue(name, k, vStr)
|
||||
}
|
||||
RemoteConfig(ctx, name)
|
||||
SaveConfig()
|
||||
@ -1095,9 +736,9 @@ func CreateRemote(ctx context.Context, name string, provider string, keyValues r
|
||||
return err
|
||||
}
|
||||
// Delete the old config if it exists
|
||||
getConfigData().DeleteSection(name)
|
||||
Data.DeleteSection(name)
|
||||
// Set the type
|
||||
getConfigData().SetValue(name, "type", provider)
|
||||
Data.SetValue(name, "type", provider)
|
||||
// Set the remaining values
|
||||
return UpdateRemote(ctx, name, keyValues, doObscure, noObscure)
|
||||
}
|
||||
@ -1152,12 +793,11 @@ func NewRemoteName() (name string) {
|
||||
for {
|
||||
fmt.Printf("name> ")
|
||||
name = ReadLine()
|
||||
_, err := getConfigData().GetSection(name)
|
||||
if err == nil {
|
||||
if Data.HasSection(name) {
|
||||
fmt.Printf("Remote %q already exists.\n", name)
|
||||
continue
|
||||
}
|
||||
err = fspath.CheckConfigName(name)
|
||||
err := fspath.CheckConfigName(name)
|
||||
switch {
|
||||
case name == "":
|
||||
fmt.Printf("Can't use empty name.\n")
|
||||
@ -1192,7 +832,7 @@ func editOptions(ri *fs.RegInfo, name string, isNew bool) {
|
||||
if option.Advanced != advanced {
|
||||
continue
|
||||
}
|
||||
subProvider := getConfigData().MustValue(name, fs.ConfigProvider, "")
|
||||
subProvider := Data.MustValue(name, fs.ConfigProvider, "")
|
||||
if matchProvider(option.Provider, subProvider) && isVisible {
|
||||
if !isNew {
|
||||
fmt.Printf("Value %q = %q\n", option.Name, FileGet(name, option.Name))
|
||||
@ -1225,7 +865,7 @@ func NewRemote(ctx context.Context, name string) {
|
||||
}
|
||||
break
|
||||
}
|
||||
getConfigData().SetValue(name, "type", newType)
|
||||
Data.SetValue(name, "type", newType)
|
||||
|
||||
editOptions(ri, name, true)
|
||||
RemoteConfig(ctx, name)
|
||||
@ -1252,7 +892,7 @@ func EditRemote(ctx context.Context, ri *fs.RegInfo, name string) {
|
||||
|
||||
// DeleteRemote gets the user to delete a remote
|
||||
func DeleteRemote(name string) {
|
||||
getConfigData().DeleteSection(name)
|
||||
Data.DeleteSection(name)
|
||||
SaveConfig()
|
||||
}
|
||||
|
||||
@ -1261,9 +901,9 @@ func DeleteRemote(name string) {
|
||||
func copyRemote(name string) string {
|
||||
newName := NewRemoteName()
|
||||
// Copy the keys
|
||||
for _, key := range getConfigData().GetKeyList(name) {
|
||||
value := getConfigData().MustValue(name, key, "")
|
||||
getConfigData().SetValue(newName, key, value)
|
||||
for _, key := range Data.GetKeyList(name) {
|
||||
value := Data.MustValue(name, key, "")
|
||||
Data.SetValue(newName, key, value)
|
||||
}
|
||||
return newName
|
||||
}
|
||||
@ -1273,7 +913,7 @@ func RenameRemote(name string) {
|
||||
fmt.Printf("Enter new name for %q remote.\n", name)
|
||||
newName := copyRemote(name)
|
||||
if name != newName {
|
||||
getConfigData().DeleteSection(name)
|
||||
Data.DeleteSection(name)
|
||||
SaveConfig()
|
||||
}
|
||||
}
|
||||
@ -1297,11 +937,10 @@ func ShowConfigLocation() {
|
||||
|
||||
// ShowConfig prints the (unencrypted) config options
|
||||
func ShowConfig() {
|
||||
var buf bytes.Buffer
|
||||
if err := goconfig.SaveConfigData(getConfigData(), &buf); err != nil {
|
||||
str, err := Data.Serialize()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to serialize config: %v", err)
|
||||
}
|
||||
str := buf.String()
|
||||
if str == "" {
|
||||
str = "; empty config\n"
|
||||
}
|
||||
@ -1311,7 +950,7 @@ func ShowConfig() {
|
||||
// EditConfig edits the config file interactively
|
||||
func EditConfig(ctx context.Context) {
|
||||
for {
|
||||
haveRemotes := len(getConfigData().GetSectionList()) != 0
|
||||
haveRemotes := len(Data.GetSectionList()) != 0
|
||||
what := []string{"eEdit existing remote", "nNew remote", "dDelete remote", "rRename remote", "cCopy remote", "sSet configuration password", "qQuit config"}
|
||||
if haveRemotes {
|
||||
fmt.Printf("Current remotes:\n\n")
|
||||
@ -1345,44 +984,6 @@ func EditConfig(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// SetPassword will allow the user to modify the current
|
||||
// configuration encryption settings.
|
||||
func SetPassword() {
|
||||
for {
|
||||
if len(configKey) > 0 {
|
||||
fmt.Println("Your configuration is encrypted.")
|
||||
what := []string{"cChange Password", "uUnencrypt configuration", "qQuit to main menu"}
|
||||
switch i := Command(what); i {
|
||||
case 'c':
|
||||
changeConfigPassword()
|
||||
SaveConfig()
|
||||
fmt.Println("Password changed")
|
||||
continue
|
||||
case 'u':
|
||||
configKey = nil
|
||||
SaveConfig()
|
||||
continue
|
||||
case 'q':
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
fmt.Println("Your configuration is not encrypted.")
|
||||
fmt.Println("If you add a password, you will protect your login information to cloud services.")
|
||||
what := []string{"aAdd Password", "qQuit to main menu"}
|
||||
switch i := Command(what); i {
|
||||
case 'a':
|
||||
changeConfigPassword()
|
||||
SaveConfig()
|
||||
fmt.Println("Password set")
|
||||
continue
|
||||
case 'q':
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Authorize is for remote authorization of headless machines.
|
||||
//
|
||||
// It expects 1 or 3 arguments
|
||||
@ -1408,14 +1009,14 @@ func Authorize(ctx context.Context, args []string, noAutoBrowser bool) {
|
||||
defer DeleteRemote(name)
|
||||
|
||||
// Indicate that we are running rclone authorize
|
||||
getConfigData().SetValue(name, ConfigAuthorize, "true")
|
||||
Data.SetValue(name, ConfigAuthorize, "true")
|
||||
if noAutoBrowser {
|
||||
getConfigData().SetValue(name, ConfigAuthNoBrowser, "true")
|
||||
Data.SetValue(name, ConfigAuthNoBrowser, "true")
|
||||
}
|
||||
|
||||
if len(args) == 3 {
|
||||
getConfigData().SetValue(name, ConfigClientID, args[1])
|
||||
getConfigData().SetValue(name, ConfigClientSecret, args[2])
|
||||
Data.SetValue(name, ConfigClientID, args[1])
|
||||
Data.SetValue(name, ConfigClientSecret, args[2])
|
||||
}
|
||||
|
||||
m := fs.ConfigMap(f, name)
|
||||
@ -1425,7 +1026,7 @@ func Authorize(ctx context.Context, args []string, noAutoBrowser bool) {
|
||||
// FileGetFlag gets the config key under section returning the
|
||||
// the value and true if found and or ("", false) otherwise
|
||||
func FileGetFlag(section, key string) (string, bool) {
|
||||
newValue, err := getConfigData().GetValue(section, key)
|
||||
newValue, err := Data.GetValue(section, key)
|
||||
return newValue, err == nil
|
||||
}
|
||||
|
||||
@ -1439,14 +1040,14 @@ func FileGet(section, key string, defaultVal ...string) string {
|
||||
if found {
|
||||
defaultVal = []string{newValue}
|
||||
}
|
||||
return getConfigData().MustValue(section, key, defaultVal...)
|
||||
return Data.MustValue(section, key, defaultVal...)
|
||||
}
|
||||
|
||||
// FileSet sets the key in section to value. It doesn't save
|
||||
// the config file.
|
||||
func FileSet(section, key, value string) {
|
||||
if value != "" {
|
||||
getConfigData().SetValue(section, key, value)
|
||||
Data.SetValue(section, key, value)
|
||||
} else {
|
||||
FileDeleteKey(section, key)
|
||||
}
|
||||
@ -1456,25 +1057,20 @@ func FileSet(section, key, value string) {
|
||||
// It returns true if the key was deleted,
|
||||
// or returns false if the section or key didn't exist.
|
||||
func FileDeleteKey(section, key string) bool {
|
||||
return getConfigData().DeleteKey(section, key)
|
||||
return Data.DeleteKey(section, key)
|
||||
}
|
||||
|
||||
var matchEnv = regexp.MustCompile(`^RCLONE_CONFIG_(.*?)_TYPE=.*$`)
|
||||
|
||||
// FileRefresh ensures the latest configFile is loaded from disk
|
||||
func FileRefresh() error {
|
||||
reloadedConfigFile, err := loadConfigFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configFile = reloadedConfigFile
|
||||
return nil
|
||||
return Data.Load()
|
||||
}
|
||||
|
||||
// FileSections returns the sections in the config file
|
||||
// including any defined by environment variables.
|
||||
func FileSections() []string {
|
||||
sections := getConfigData().GetSectionList()
|
||||
sections := Data.GetSectionList()
|
||||
for _, item := range os.Environ() {
|
||||
matches := matchEnv.FindStringSubmatch(item)
|
||||
if len(matches) == 2 {
|
||||
@ -1487,7 +1083,7 @@ func FileSections() []string {
|
||||
// DumpRcRemote dumps the config for a single remote
|
||||
func DumpRcRemote(name string) (dump rc.Params) {
|
||||
params := rc.Params{}
|
||||
for _, key := range getConfigData().GetKeyList(name) {
|
||||
for _, key := range Data.GetKeyList(name) {
|
||||
params[key] = FileGet(name, key)
|
||||
}
|
||||
return params
|
||||
@ -1497,7 +1093,7 @@ func DumpRcRemote(name string) (dump rc.Params) {
|
||||
// for the rc
|
||||
func DumpRcBlob() (dump rc.Params) {
|
||||
dump = rc.Params{}
|
||||
for _, name := range getConfigData().GetSectionList() {
|
||||
for _, name := range Data.GetSectionList() {
|
||||
dump[name] = DumpRcRemote(name)
|
||||
}
|
||||
return dump
|
||||
|
@ -31,16 +31,16 @@ func testConfigFile(t *testing.T, configFileName string) func() {
|
||||
oldOsStdout := os.Stdout
|
||||
oldConfigPath := ConfigPath
|
||||
oldConfig := *ci
|
||||
oldConfigFile := configFile
|
||||
oldConfigFile := Data
|
||||
oldReadLine := ReadLine
|
||||
oldPassword := Password
|
||||
os.Stdout = nil
|
||||
ConfigPath = path
|
||||
ci = &fs.ConfigInfo{}
|
||||
configFile = nil
|
||||
Data = nil
|
||||
|
||||
LoadConfig(ctx)
|
||||
assert.Equal(t, []string{}, getConfigData().GetSectionList())
|
||||
assert.Equal(t, []string{}, Data.GetSectionList())
|
||||
|
||||
// Fake a remote
|
||||
fs.Register(&fs.RegInfo{
|
||||
@ -69,7 +69,7 @@ func testConfigFile(t *testing.T, configFileName string) func() {
|
||||
ReadLine = oldReadLine
|
||||
Password = oldPassword
|
||||
*ci = oldConfig
|
||||
configFile = oldConfigFile
|
||||
Data = oldConfigFile
|
||||
|
||||
_ = os.Unsetenv("_RCLONE_CONFIG_KEY_FILE")
|
||||
_ = os.Unsetenv("RCLONE_CONFIG_PASS")
|
||||
@ -101,7 +101,7 @@ func TestCRUD(t *testing.T) {
|
||||
})
|
||||
NewRemote(ctx, "test")
|
||||
|
||||
assert.Equal(t, []string{"test"}, configFile.GetSectionList())
|
||||
assert.Equal(t, []string{"test"}, Data.GetSectionList())
|
||||
assert.Equal(t, "config_test_remote", FileGet("test", "type"))
|
||||
assert.Equal(t, "true", FileGet("test", "bool"))
|
||||
assert.Equal(t, "secret", obscure.MustReveal(FileGet("test", "pass")))
|
||||
@ -114,14 +114,14 @@ func TestCRUD(t *testing.T) {
|
||||
})
|
||||
RenameRemote("test")
|
||||
|
||||
assert.Equal(t, []string{"asdf"}, configFile.GetSectionList())
|
||||
assert.Equal(t, []string{"asdf"}, Data.GetSectionList())
|
||||
assert.Equal(t, "config_test_remote", FileGet("asdf", "type"))
|
||||
assert.Equal(t, "true", FileGet("asdf", "bool"))
|
||||
assert.Equal(t, "secret", obscure.MustReveal(FileGet("asdf", "pass")))
|
||||
|
||||
// delete remote
|
||||
DeleteRemote("asdf")
|
||||
assert.Equal(t, []string{}, configFile.GetSectionList())
|
||||
assert.Equal(t, []string{}, Data.GetSectionList())
|
||||
}
|
||||
|
||||
func TestChooseOption(t *testing.T) {
|
||||
@ -198,7 +198,7 @@ func TestCreateUpdatePasswordRemote(t *testing.T) {
|
||||
"pass": "potato",
|
||||
}, doObscure, noObscure))
|
||||
|
||||
assert.Equal(t, []string{"test2"}, configFile.GetSectionList())
|
||||
assert.Equal(t, []string{"test2"}, Data.GetSectionList())
|
||||
assert.Equal(t, "config_test_remote", FileGet("test2", "type"))
|
||||
assert.Equal(t, "true", FileGet("test2", "bool"))
|
||||
gotPw := FileGet("test2", "pass")
|
||||
@ -214,7 +214,7 @@ func TestCreateUpdatePasswordRemote(t *testing.T) {
|
||||
"spare": "spare",
|
||||
}, doObscure, noObscure))
|
||||
|
||||
assert.Equal(t, []string{"test2"}, configFile.GetSectionList())
|
||||
assert.Equal(t, []string{"test2"}, Data.GetSectionList())
|
||||
assert.Equal(t, "config_test_remote", FileGet("test2", "type"))
|
||||
assert.Equal(t, "false", FileGet("test2", "bool"))
|
||||
gotPw = FileGet("test2", "pass")
|
||||
@ -227,7 +227,7 @@ func TestCreateUpdatePasswordRemote(t *testing.T) {
|
||||
"pass": "potato3",
|
||||
}))
|
||||
|
||||
assert.Equal(t, []string{"test2"}, configFile.GetSectionList())
|
||||
assert.Equal(t, []string{"test2"}, Data.GetSectionList())
|
||||
assert.Equal(t, "config_test_remote", FileGet("test2", "type"))
|
||||
assert.Equal(t, "false", FileGet("test2", "bool"))
|
||||
assert.Equal(t, "potato3", obscure.MustReveal(FileGet("test2", "pass")))
|
||||
@ -260,15 +260,15 @@ func TestConfigLoad(t *testing.T) {
|
||||
ConfigPath = oldConfigPath
|
||||
}()
|
||||
configKey = nil // reset password
|
||||
c, err := loadConfigFile()
|
||||
err := Data.Load()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sections := c.GetSectionList()
|
||||
sections := Data.GetSectionList()
|
||||
var expect = []string{"RCLONE_ENCRYPT_V0", "nounc", "unc"}
|
||||
assert.Equal(t, expect, sections)
|
||||
|
||||
keys := c.GetKeyList("nounc")
|
||||
keys := Data.GetKeyList("nounc")
|
||||
expect = []string{"type", "nounc"}
|
||||
assert.Equal(t, expect, keys)
|
||||
}
|
||||
@ -285,13 +285,13 @@ func TestConfigLoadEncrypted(t *testing.T) {
|
||||
// Set correct password
|
||||
err = setConfigPassword("asdf")
|
||||
require.NoError(t, err)
|
||||
c, err := loadConfigFile()
|
||||
err = Data.Load()
|
||||
require.NoError(t, err)
|
||||
sections := c.GetSectionList()
|
||||
sections := Data.GetSectionList()
|
||||
var expect = []string{"nounc", "unc"}
|
||||
assert.Equal(t, expect, sections)
|
||||
|
||||
keys := c.GetKeyList("nounc")
|
||||
keys := Data.GetKeyList("nounc")
|
||||
expect = []string{"type", "nounc"}
|
||||
assert.Equal(t, expect, keys)
|
||||
}
|
||||
@ -313,14 +313,14 @@ func TestConfigLoadEncryptedWithValidPassCommand(t *testing.T) {
|
||||
|
||||
configKey = nil // reset password
|
||||
|
||||
c, err := loadConfigFile()
|
||||
err := Data.Load()
|
||||
require.NoError(t, err)
|
||||
|
||||
sections := c.GetSectionList()
|
||||
sections := Data.GetSectionList()
|
||||
var expect = []string{"nounc", "unc"}
|
||||
assert.Equal(t, expect, sections)
|
||||
|
||||
keys := c.GetKeyList("nounc")
|
||||
keys := Data.GetKeyList("nounc")
|
||||
expect = []string{"type", "nounc"}
|
||||
assert.Equal(t, expect, keys)
|
||||
}
|
||||
@ -342,7 +342,7 @@ func TestConfigLoadEncryptedWithInvalidPassCommand(t *testing.T) {
|
||||
|
||||
configKey = nil // reset password
|
||||
|
||||
_, err := loadConfigFile()
|
||||
err := Data.Load()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "using --password-command derived password")
|
||||
}
|
||||
@ -354,24 +354,23 @@ func TestConfigLoadEncryptedFailures(t *testing.T) {
|
||||
oldConfigPath := ConfigPath
|
||||
ConfigPath = "./testdata/enc-short.conf"
|
||||
defer func() { ConfigPath = oldConfigPath }()
|
||||
_, err = loadConfigFile()
|
||||
err = Data.Load()
|
||||
require.Error(t, err)
|
||||
|
||||
// This file contains invalid base64 characters.
|
||||
ConfigPath = "./testdata/enc-invalid.conf"
|
||||
_, err = loadConfigFile()
|
||||
err = Data.Load()
|
||||
require.Error(t, err)
|
||||
|
||||
// This file contains invalid base64 characters.
|
||||
ConfigPath = "./testdata/enc-too-new.conf"
|
||||
_, err = loadConfigFile()
|
||||
err = Data.Load()
|
||||
require.Error(t, err)
|
||||
|
||||
// This file does not exist.
|
||||
ConfigPath = "./testdata/filenotfound.conf"
|
||||
c, err := loadConfigFile()
|
||||
assert.Equal(t, errorConfigFileNotFound, err)
|
||||
assert.Nil(t, c)
|
||||
err = Data.Load()
|
||||
assert.Equal(t, ErrorConfigFileNotFound, err)
|
||||
}
|
||||
|
||||
func TestPassword(t *testing.T) {
|
||||
@ -448,8 +447,8 @@ func TestFileRefresh(t *testing.T) {
|
||||
err = ioutil.WriteFile(ConfigPath, b, 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NotEqual(t, []string{"refreshed_test"}, configFile.GetSectionList())
|
||||
assert.NotEqual(t, []string{"refreshed_test"}, Data.GetSectionList())
|
||||
err = FileRefresh()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"refreshed_test"}, configFile.GetSectionList())
|
||||
assert.Equal(t, []string{"refreshed_test"}, Data.GetSectionList())
|
||||
}
|
||||
|
146
fs/config/configfile/configfile.go
Normal file
146
fs/config/configfile/configfile.go
Normal file
@ -0,0 +1,146 @@
|
||||
package configfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Unknwon/goconfig"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rclone/rclone/fs"
|
||||
"github.com/rclone/rclone/fs/config"
|
||||
)
|
||||
|
||||
// GoConfig implements config file saving using a simple ini based
|
||||
// format.
|
||||
type GoConfig struct {
|
||||
*goconfig.ConfigFile
|
||||
}
|
||||
|
||||
// Load the config from permanent storage
|
||||
func (gc *GoConfig) Load() error {
|
||||
b, err := os.Open(config.ConfigPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return config.ErrorConfigFileNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer fs.CheckClose(b, &err)
|
||||
|
||||
cryptReader, err := config.Decrypt(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gc.ConfigFile == nil {
|
||||
c, err := goconfig.LoadFromReader(cryptReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gc.ConfigFile = c
|
||||
} else {
|
||||
return gc.ReloadData(cryptReader)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save the config to permanent storage
|
||||
func (gc *GoConfig) Save() error {
|
||||
dir, name := filepath.Split(config.ConfigPath)
|
||||
err := os.MkdirAll(dir, os.ModePerm)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create config directory")
|
||||
}
|
||||
f, err := ioutil.TempFile(dir, name)
|
||||
if err != nil {
|
||||
return errors.Errorf("Failed to create temp file for new config: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
if err := os.Remove(f.Name()); err != nil && !os.IsNotExist(err) {
|
||||
fs.Errorf(nil, "Failed to remove temp config file: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := goconfig.SaveConfigData(gc.ConfigFile, &buf); err != nil {
|
||||
return errors.Errorf("Failed to save config file: %v", err)
|
||||
}
|
||||
|
||||
if err := config.Encrypt(&buf, f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = f.Sync()
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
return errors.Errorf("Failed to close config file: %v", err)
|
||||
}
|
||||
|
||||
var fileMode os.FileMode = 0600
|
||||
info, err := os.Stat(config.ConfigPath)
|
||||
if err != nil {
|
||||
fs.Debugf(nil, "Using default permissions for config file: %v", fileMode)
|
||||
} else if info.Mode() != fileMode {
|
||||
fs.Debugf(nil, "Keeping previous permissions for config file: %v", info.Mode())
|
||||
fileMode = info.Mode()
|
||||
}
|
||||
|
||||
attemptCopyGroup(config.ConfigPath, f.Name())
|
||||
|
||||
err = os.Chmod(f.Name(), fileMode)
|
||||
if err != nil {
|
||||
fs.Errorf(nil, "Failed to set permissions on config file: %v", err)
|
||||
}
|
||||
|
||||
if err = os.Rename(config.ConfigPath, config.ConfigPath+".old"); err != nil && !os.IsNotExist(err) {
|
||||
return errors.Errorf("Failed to move previous config to backup location: %v", err)
|
||||
}
|
||||
if err = os.Rename(f.Name(), config.ConfigPath); err != nil {
|
||||
return errors.Errorf("Failed to move newly written config from %s to final location: %v", f.Name(), err)
|
||||
}
|
||||
if err := os.Remove(config.ConfigPath + ".old"); err != nil && !os.IsNotExist(err) {
|
||||
fs.Errorf(nil, "Failed to remove backup config file: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Serialize the config into a string
|
||||
func (gc *GoConfig) Serialize() (string, error) {
|
||||
var buf bytes.Buffer
|
||||
if err := goconfig.SaveConfigData(gc.ConfigFile, &buf); err != nil {
|
||||
return "", errors.Errorf("Failed to save config file: %v", err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// HasSection returns true if section exists in the config file
|
||||
func (gc *GoConfig) HasSection(section string) bool {
|
||||
_, err := gc.ConfigFile.GetSection(section)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// DeleteSection removes the named section and all config from the
|
||||
// config file
|
||||
func (gc *GoConfig) DeleteSection(section string) {
|
||||
gc.ConfigFile.DeleteSection(section)
|
||||
}
|
||||
|
||||
// SetValue sets the value under key in section
|
||||
func (gc *GoConfig) SetValue(section string, key string, value string) {
|
||||
gc.ConfigFile.SetValue(section, key, value)
|
||||
}
|
||||
|
||||
// Check the interfaces are satisfied
|
||||
var (
|
||||
_ config.File = (*GoConfig)(nil)
|
||||
)
|
@ -3,7 +3,7 @@
|
||||
|
||||
// +build !darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris
|
||||
|
||||
package config
|
||||
package configfile
|
||||
|
||||
// attemptCopyGroups tries to keep the group the same, which only makes sense
|
||||
// for system with user-group-world permission model.
|
@ -3,7 +3,7 @@
|
||||
|
||||
// +build darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||
|
||||
package config
|
||||
package configfile
|
||||
|
||||
import (
|
||||
"os"
|
388
fs/config/crypt.go
Normal file
388
fs/config/crypt.go
Normal file
@ -0,0 +1,388 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/nacl/secretbox"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
|
||||
"github.com/rclone/rclone/fs"
|
||||
"github.com/rclone/rclone/fs/config/obscure"
|
||||
)
|
||||
|
||||
var (
|
||||
// Key to use for password en/decryption.
|
||||
// When nil, no encryption will be used for saving.
|
||||
configKey []byte
|
||||
|
||||
// PasswordPromptOutput is output of prompt for password
|
||||
PasswordPromptOutput = os.Stderr
|
||||
|
||||
// PassConfigKeyForDaemonization if set to true, the configKey
|
||||
// is obscured with obscure.Obscure and saved to a temp file
|
||||
// when it is calculated from the password. The path of that
|
||||
// temp file is then written to the environment variable
|
||||
// `_RCLONE_CONFIG_KEY_FILE`. If `_RCLONE_CONFIG_KEY_FILE` is
|
||||
// present, password prompt is skipped and
|
||||
// `RCLONE_CONFIG_PASS` ignored. For security reasons, the
|
||||
// temp file is deleted once the configKey is successfully
|
||||
// loaded. This can be used to pass the configKey to a child
|
||||
// process.
|
||||
PassConfigKeyForDaemonization = false
|
||||
)
|
||||
|
||||
// Decrypt will automatically decrypt a reader
|
||||
func Decrypt(b io.ReadSeeker) (io.Reader, error) {
|
||||
ctx := context.Background()
|
||||
ci := fs.GetConfig(ctx)
|
||||
var usingPasswordCommand bool
|
||||
|
||||
// Find first non-empty line
|
||||
r := bufio.NewReader(b)
|
||||
for {
|
||||
line, _, err := r.ReadLine()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
if _, err := b.Seek(0, io.SeekStart); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
l := strings.TrimSpace(string(line))
|
||||
if len(l) == 0 || strings.HasPrefix(l, ";") || strings.HasPrefix(l, "#") {
|
||||
continue
|
||||
}
|
||||
// First non-empty or non-comment must be ENCRYPT_V0
|
||||
if l == "RCLONE_ENCRYPT_V0:" {
|
||||
break
|
||||
}
|
||||
if strings.HasPrefix(l, "RCLONE_ENCRYPT_V") {
|
||||
return nil, errors.New("unsupported configuration encryption - update rclone for support")
|
||||
}
|
||||
if _, err := b.Seek(0, io.SeekStart); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
if len(configKey) == 0 {
|
||||
if len(ci.PasswordCommand) != 0 {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
cmd := exec.Command(ci.PasswordCommand[0], ci.PasswordCommand[1:]...)
|
||||
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
// One does not always get the stderr returned in the wrapped error.
|
||||
fs.Errorf(nil, "Using --password-command returned: %v", err)
|
||||
if ers := strings.TrimSpace(stderr.String()); ers != "" {
|
||||
fs.Errorf(nil, "--password-command stderr: %s", ers)
|
||||
}
|
||||
return nil, errors.Wrap(err, "password command failed")
|
||||
}
|
||||
if pass := strings.Trim(stdout.String(), "\r\n"); pass != "" {
|
||||
err := setConfigPassword(pass)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "incorrect password")
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New("password-command returned empty string")
|
||||
}
|
||||
|
||||
if len(configKey) == 0 {
|
||||
return nil, errors.New("unable to decrypt configuration: incorrect password")
|
||||
}
|
||||
usingPasswordCommand = true
|
||||
} else {
|
||||
usingPasswordCommand = false
|
||||
|
||||
envpw := os.Getenv("RCLONE_CONFIG_PASS")
|
||||
|
||||
if envpw != "" {
|
||||
err := setConfigPassword(envpw)
|
||||
if err != nil {
|
||||
fs.Errorf(nil, "Using RCLONE_CONFIG_PASS returned: %v", err)
|
||||
} else {
|
||||
fs.Debugf(nil, "Using RCLONE_CONFIG_PASS password.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypted content is base64 encoded.
|
||||
dec := base64.NewDecoder(base64.StdEncoding, r)
|
||||
box, err := ioutil.ReadAll(dec)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to load base64 encoded data")
|
||||
}
|
||||
if len(box) < 24+secretbox.Overhead {
|
||||
return nil, errors.New("Configuration data too short")
|
||||
}
|
||||
|
||||
var out []byte
|
||||
for {
|
||||
if envKeyFile := os.Getenv("_RCLONE_CONFIG_KEY_FILE"); len(envKeyFile) > 0 {
|
||||
fs.Debugf(nil, "attempting to obtain configKey from temp file %s", envKeyFile)
|
||||
obscuredKey, err := ioutil.ReadFile(envKeyFile)
|
||||
if err != nil {
|
||||
errRemove := os.Remove(envKeyFile)
|
||||
if errRemove != nil {
|
||||
log.Fatalf("unable to read obscured config key and unable to delete the temp file: %v", err)
|
||||
}
|
||||
log.Fatalf("unable to read obscured config key: %v", err)
|
||||
}
|
||||
errRemove := os.Remove(envKeyFile)
|
||||
if errRemove != nil {
|
||||
log.Fatalf("unable to delete temp file with configKey: %v", err)
|
||||
}
|
||||
configKey = []byte(obscure.MustReveal(string(obscuredKey)))
|
||||
fs.Debugf(nil, "using _RCLONE_CONFIG_KEY_FILE for configKey")
|
||||
} else {
|
||||
if len(configKey) == 0 {
|
||||
if usingPasswordCommand {
|
||||
return nil, errors.New("using --password-command derived password, unable to decrypt configuration")
|
||||
}
|
||||
if !ci.AskPassword {
|
||||
return nil, errors.New("unable to decrypt configuration and not allowed to ask for password - set RCLONE_CONFIG_PASS to your configuration password")
|
||||
}
|
||||
getConfigPassword("Enter configuration password:")
|
||||
}
|
||||
}
|
||||
|
||||
// Nonce is first 24 bytes of the ciphertext
|
||||
var nonce [24]byte
|
||||
copy(nonce[:], box[:24])
|
||||
var key [32]byte
|
||||
copy(key[:], configKey[:32])
|
||||
|
||||
// Attempt to decrypt
|
||||
var ok bool
|
||||
out, ok = secretbox.Open(nil, box[24:], &nonce, &key)
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
|
||||
// Retry
|
||||
fs.Errorf(nil, "Couldn't decrypt configuration, most likely wrong password.")
|
||||
configKey = nil
|
||||
}
|
||||
return bytes.NewReader(out), nil
|
||||
}
|
||||
|
||||
// Encrypt the config file
|
||||
func Encrypt(src io.Reader, dst io.Writer) error {
|
||||
if len(configKey) == 0 {
|
||||
_, err := io.Copy(dst, src)
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(dst, "# Encrypted rclone configuration File")
|
||||
_, _ = fmt.Fprintln(dst, "")
|
||||
_, _ = fmt.Fprintln(dst, "RCLONE_ENCRYPT_V0:")
|
||||
|
||||
// Generate new nonce and write it to the start of the ciphertext
|
||||
var nonce [24]byte
|
||||
n, _ := rand.Read(nonce[:])
|
||||
if n != 24 {
|
||||
return errors.Errorf("nonce short read: %d", n)
|
||||
}
|
||||
enc := base64.NewEncoder(base64.StdEncoding, dst)
|
||||
_, err := enc.Write(nonce[:])
|
||||
if err != nil {
|
||||
return errors.Errorf("Failed to write config file: %v", err)
|
||||
}
|
||||
|
||||
var key [32]byte
|
||||
copy(key[:], configKey[:32])
|
||||
|
||||
data, err := ioutil.ReadAll(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b := secretbox.Seal(nil, data, &nonce, &key)
|
||||
_, err = enc.Write(b)
|
||||
if err != nil {
|
||||
return errors.Errorf("Failed to write config file: %v", err)
|
||||
}
|
||||
return enc.Close()
|
||||
}
|
||||
|
||||
// checkPassword normalises and validates the password
|
||||
func checkPassword(password string) (string, error) {
|
||||
if !utf8.ValidString(password) {
|
||||
return "", errors.New("password contains invalid utf8 characters")
|
||||
}
|
||||
// Check for leading/trailing whitespace
|
||||
trimmedPassword := strings.TrimSpace(password)
|
||||
// Warn user if password has leading+trailing whitespace
|
||||
if len(password) != len(trimmedPassword) {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "Your password contains leading/trailing whitespace - in previous versions of rclone this was stripped")
|
||||
}
|
||||
// Normalize to reduce weird variations.
|
||||
password = norm.NFKC.String(password)
|
||||
if len(password) == 0 || len(trimmedPassword) == 0 {
|
||||
return "", errors.New("no characters in password")
|
||||
}
|
||||
return password, nil
|
||||
}
|
||||
|
||||
// GetPassword asks the user for a password with the prompt given.
|
||||
func GetPassword(prompt string) string {
|
||||
_, _ = fmt.Fprintln(PasswordPromptOutput, prompt)
|
||||
for {
|
||||
_, _ = fmt.Fprint(PasswordPromptOutput, "password:")
|
||||
password := ReadPassword()
|
||||
password, err := checkPassword(password)
|
||||
if err == nil {
|
||||
return password
|
||||
}
|
||||
_, _ = fmt.Fprintf(os.Stderr, "Bad password: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ChangePassword will query the user twice for the named password. If
|
||||
// the same password is entered it is returned.
|
||||
func ChangePassword(name string) string {
|
||||
for {
|
||||
a := GetPassword(fmt.Sprintf("Enter %s password:", name))
|
||||
b := GetPassword(fmt.Sprintf("Confirm %s password:", name))
|
||||
if a == b {
|
||||
return a
|
||||
}
|
||||
fmt.Println("Passwords do not match!")
|
||||
}
|
||||
}
|
||||
|
||||
// getConfigPassword will query the user for a password the
|
||||
// first time it is required.
|
||||
func getConfigPassword(q string) {
|
||||
if len(configKey) != 0 {
|
||||
return
|
||||
}
|
||||
for {
|
||||
password := GetPassword(q)
|
||||
err := setConfigPassword(password)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
_, _ = fmt.Fprintln(os.Stderr, "Error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// setConfigPassword will set the configKey to the hash of
|
||||
// the password. If the length of the password is
|
||||
// zero after trimming+normalization, an error is returned.
|
||||
func setConfigPassword(password string) error {
|
||||
password, err := checkPassword(password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Create SHA256 has of the password
|
||||
sha := sha256.New()
|
||||
_, err = sha.Write([]byte("[" + password + "][rclone-config]"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configKey = sha.Sum(nil)
|
||||
if PassConfigKeyForDaemonization {
|
||||
tempFile, err := ioutil.TempFile("", "rclone")
|
||||
if err != nil {
|
||||
log.Fatalf("cannot create temp file to store configKey: %v", err)
|
||||
}
|
||||
_, err = tempFile.WriteString(obscure.MustObscure(string(configKey)))
|
||||
if err != nil {
|
||||
errRemove := os.Remove(tempFile.Name())
|
||||
if errRemove != nil {
|
||||
log.Fatalf("error writing configKey to temp file and also error deleting it: %v", err)
|
||||
}
|
||||
log.Fatalf("error writing configKey to temp file: %v", err)
|
||||
}
|
||||
err = tempFile.Close()
|
||||
if err != nil {
|
||||
errRemove := os.Remove(tempFile.Name())
|
||||
if errRemove != nil {
|
||||
log.Fatalf("error closing temp file with configKey and also error deleting it: %v", err)
|
||||
}
|
||||
log.Fatalf("error closing temp file with configKey: %v", err)
|
||||
}
|
||||
fs.Debugf(nil, "saving configKey to temp file")
|
||||
err = os.Setenv("_RCLONE_CONFIG_KEY_FILE", tempFile.Name())
|
||||
if err != nil {
|
||||
errRemove := os.Remove(tempFile.Name())
|
||||
if errRemove != nil {
|
||||
log.Fatalf("unable to set environment variable _RCLONE_CONFIG_KEY_FILE and unable to delete the temp file: %v", err)
|
||||
}
|
||||
log.Fatalf("unable to set environment variable _RCLONE_CONFIG_KEY_FILE: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// changeConfigPassword will query the user twice
|
||||
// for a password. If the same password is entered
|
||||
// twice the key is updated.
|
||||
func changeConfigPassword() {
|
||||
err := setConfigPassword(ChangePassword("NEW configuration"))
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to set config password: %v\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// SetPassword will allow the user to modify the current
|
||||
// configuration encryption settings.
|
||||
func SetPassword() {
|
||||
for {
|
||||
if len(configKey) > 0 {
|
||||
fmt.Println("Your configuration is encrypted.")
|
||||
what := []string{"cChange Password", "uUnencrypt configuration", "qQuit to main menu"}
|
||||
switch i := Command(what); i {
|
||||
case 'c':
|
||||
changeConfigPassword()
|
||||
SaveConfig()
|
||||
fmt.Println("Password changed")
|
||||
continue
|
||||
case 'u':
|
||||
configKey = nil
|
||||
SaveConfig()
|
||||
continue
|
||||
case 'q':
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
fmt.Println("Your configuration is not encrypted.")
|
||||
fmt.Println("If you add a password, you will protect your login information to cloud services.")
|
||||
what := []string{"aAdd Password", "qQuit to main menu"}
|
||||
switch i := Command(what); i {
|
||||
case 'a':
|
||||
changeConfigPassword()
|
||||
SaveConfig()
|
||||
fmt.Println("Password set")
|
||||
continue
|
||||
case 'q':
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -72,7 +72,7 @@ See the [listremotes command](/commands/rclone_listremotes/) command for more in
|
||||
// Return the a list of remotes in the config file
|
||||
func rcListRemotes(ctx context.Context, in rc.Params) (out rc.Params, err error) {
|
||||
var remotes = []string{}
|
||||
for _, remote := range getConfigData().GetSectionList() {
|
||||
for _, remote := range Data.GetSectionList() {
|
||||
remotes = append(remotes, remote)
|
||||
}
|
||||
out = rc.Params{
|
||||
|
Loading…
Reference in New Issue
Block a user