Add easier headless configuration.

This will allow setting up a remote with copy&paste of values to a headless machine. It will allow copy+pasting a token into the configuration.

This requires rclone to be on a machine with a proper browser. Custom client id and secrets are supported.

To test token generation, use `rclone auth "fs type"`.
This commit is contained in:
klauspost 2016-01-04 16:13:36 +01:00 committed by Nick Craig-Wood
parent c245183101
commit bcbd30bb8a
9 changed files with 120 additions and 13 deletions

View File

@ -63,7 +63,7 @@ func init() {
Name: "amazon cloud drive", Name: "amazon cloud drive",
NewFs: NewFs, NewFs: NewFs,
Config: func(name string) { Config: func(name string) {
err := oauthutil.Config(name, acdConfig) err := oauthutil.Config("amazon cloud drive", name, acdConfig)
if err != nil { if err != nil {
log.Fatalf("Failed to configure token: %v", err) log.Fatalf("Failed to configure token: %v", err)
} }

View File

@ -66,7 +66,7 @@ func init() {
Name: "drive", Name: "drive",
NewFs: NewFs, NewFs: NewFs,
Config: func(name string) { Config: func(name string) {
err := oauthutil.Config(name, driveConfig) err := oauthutil.Config("drive", name, driveConfig)
if err != nil { if err != nil {
log.Fatalf("Failed to configure token: %v", err) log.Fatalf("Failed to configure token: %v", err)
} }

View File

@ -532,3 +532,47 @@ func EditConfig() {
} }
} }
} }
// Duplicated from oauthutil to avoid circular reference.
const (
// ConfigClientID is the config key used to store the client id
ConfigClientID = "client_id"
// ConfigClientSecret is the config key used to store the client secret
ConfigClientSecret = "client_secret"
// ConfigAutomatic indicates that we want non-interactive configuration
ConfigAutomatic = "config_automatic"
)
// Authorize is for remote authorization of headless machines.
func Authorize() {
args := pflag.Args()[1:]
switch len(args) {
case 1, 3:
default:
log.Fatalf("Invalid number of arguments: %d", len(args))
}
newType := args[0]
fs, err := Find(newType)
if err != nil {
log.Fatalf("Failed to find fs: %v", err)
}
if fs.Config == nil {
log.Fatalf("No configuration on fs %v", newType)
}
// Name used for temporary fs
name := "**temp-fs**"
// Make sure we delete it
defer DeleteRemote(name)
// Indicate that we want fully automatic configuration.
ConfigFile.SetValue(name, ConfigAutomatic, "yes")
if len(args) == 3 {
ConfigFile.SetValue(name, ConfigClientID, args[1])
ConfigFile.SetValue(name, ConfigClientSecret, args[2])
}
fs.Config(name)
}

View File

@ -59,7 +59,7 @@ func init() {
Name: "google cloud storage", Name: "google cloud storage",
NewFs: NewFs, NewFs: NewFs,
Config: func(name string) { Config: func(name string) {
err := oauthutil.Config(name, storageConfig) err := oauthutil.Config("google cloud storage", name, storageConfig)
if err != nil { if err != nil {
log.Fatalf("Failed to configure token: %v", err) log.Fatalf("Failed to configure token: %v", err)
} }

View File

@ -48,7 +48,7 @@ func init() {
Name: "hubic", Name: "hubic",
NewFs: NewFs, NewFs: NewFs,
Config: func(name string) { Config: func(name string) {
err := oauthutil.Config(name, oauthConfig) err := oauthutil.Config("hubic", name, oauthConfig)
if err != nil { if err != nil {
log.Fatalf("Failed to configure token: %v", err) log.Fatalf("Failed to configure token: %v", err)
} }

View File

@ -7,6 +7,7 @@ import (
"log" "log"
"net" "net"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/ncw/rclone/fs" "github.com/ncw/rclone/fs"
@ -25,6 +26,9 @@ const (
// ConfigClientSecret is the config key used to store the client secret // ConfigClientSecret is the config key used to store the client secret
ConfigClientSecret = "client_secret" ConfigClientSecret = "client_secret"
// ConfigAutomatic indicates that we want non-interactive configuration
ConfigAutomatic = "config_automatic"
// TitleBarRedirectURL is the OAuth2 redirect URL to use when the authorization // TitleBarRedirectURL is the OAuth2 redirect URL to use when the authorization
// code should be returned in the title bar of the browser, with the page text // code should be returned in the title bar of the browser, with the page text
// prompting the user to copy the code and paste it in the application. // prompting the user to copy the code and paste it in the application.
@ -147,16 +151,21 @@ func Context() context.Context {
} }
// overrideCredentials sets the ClientID and ClientSecret from the // overrideCredentials sets the ClientID and ClientSecret from the
// config file if they are not blank // config file if they are not blank.
func overrideCredentials(name string, config *oauth2.Config) { // If any value is overridden, true is returned.
func overrideCredentials(name string, config *oauth2.Config) bool {
changed := false
ClientID := fs.ConfigFile.MustValue(name, ConfigClientID) ClientID := fs.ConfigFile.MustValue(name, ConfigClientID)
if ClientID != "" { if ClientID != "" {
config.ClientID = ClientID config.ClientID = ClientID
changed = true
} }
ClientSecret := fs.ConfigFile.MustValue(name, ConfigClientSecret) ClientSecret := fs.ConfigFile.MustValue(name, ConfigClientSecret)
if ClientSecret != "" { if ClientSecret != "" {
config.ClientSecret = ClientSecret config.ClientSecret = ClientSecret
changed = true
} }
return changed
} }
// NewClient gets a token from the config file and configures a Client // NewClient gets a token from the config file and configures a Client
@ -185,8 +194,10 @@ func NewClient(name string, config *oauth2.Config) (*http.Client, error) {
// Config does the initial creation of the token // Config does the initial creation of the token
// //
// It may run an internal webserver to receive the results // It may run an internal webserver to receive the results
func Config(name string, config *oauth2.Config) error { func Config(id, name string, config *oauth2.Config) error {
overrideCredentials(name, config) changed := overrideCredentials(name, config)
automatic := fs.ConfigFile.MustValue(name, ConfigAutomatic) != ""
// See if already have a token // See if already have a token
tokenString := fs.ConfigFile.MustValue(name, "token") tokenString := fs.ConfigFile.MustValue(name, "token")
if tokenString != "" { if tokenString != "" {
@ -201,11 +212,42 @@ func Config(name string, config *oauth2.Config) error {
switch config.RedirectURL { switch config.RedirectURL {
case RedirectURL, RedirectPublicURL, RedirectLocalhostURL: case RedirectURL, RedirectPublicURL, RedirectLocalhostURL:
useWebServer = true useWebServer = true
case TitleBarRedirectURL: if automatic {
break
}
fmt.Printf("Use auto config?\n") fmt.Printf("Use auto config?\n")
fmt.Printf(" * Say Y if not sure\n") fmt.Printf(" * Say Y if not sure\n")
fmt.Printf(" * Say N if you are working on a remote or headless machine or Y didn't work\n") fmt.Printf(" * Say N if you are working on a remote or headless machine\n")
useWebServer = fs.Confirm() auto := fs.Confirm()
if !auto {
fmt.Printf("For this to work, you will need rclone available on a machine that has a web browser available.\n")
fmt.Printf("Execute the following on your machine:\n")
if changed {
fmt.Printf("\trclone authorize %q %q %q\n", id, config.ClientID, config.ClientSecret)
} else {
fmt.Printf("\trclone authorize %q\n", id)
}
fmt.Println("Then paste the result below:")
code := ""
for code == "" {
fmt.Printf("result> ")
code = strings.TrimSpace(fs.ReadLine())
}
token := &oauth2.Token{}
err := json.Unmarshal([]byte(code), token)
if err != nil {
return err
}
return putToken(name, token)
}
case TitleBarRedirectURL:
useWebServer = automatic
if !automatic {
fmt.Printf("Use auto config?\n")
fmt.Printf(" * Say Y if not sure\n")
fmt.Printf(" * Say N if you are working on a remote or headless machine or Y didn't work\n")
useWebServer = fs.Confirm()
}
if useWebServer { if useWebServer {
// copy the config and set to use the internal webserver // copy the config and set to use the internal webserver
configCopy := *config configCopy := *config
@ -260,6 +302,15 @@ func Config(name string, config *oauth2.Config) error {
if err != nil { if err != nil {
return fmt.Errorf("Failed to get token: %v", err) return fmt.Errorf("Failed to get token: %v", err)
} }
// Print code if we do automatic retrieval
if automatic {
result, err := json.Marshal(token)
if err != nil {
return fmt.Errorf("Failed to marshal token: %v", err)
}
fmt.Printf("Paste the following into your remote machine --->\n%s\n<---End paste", result)
}
return putToken(name, token) return putToken(name, token)
} }

View File

@ -59,7 +59,7 @@ func init() {
Name: "onedrive", Name: "onedrive",
NewFs: NewFs, NewFs: NewFs,
Config: func(name string) { Config: func(name string) {
err := oauthutil.Config(name, oauthConfig) err := oauthutil.Config("onedrive", name, oauthConfig)
if err != nil { if err != nil {
log.Fatalf("Failed to configure token: %v", err) log.Fatalf("Failed to configure token: %v", err)
} }

View File

@ -224,6 +224,18 @@ var Commands = []Command{
}, },
NoStats: true, NoStats: true,
}, },
{
Name: "authorize",
Help: `
Remote authorization.`,
Run: func(fdst, fsrc fs.Fs) error {
fs.Authorize()
return nil
},
NoStats: true,
MinArgs: 1,
MaxArgs: 3,
},
{ {
Name: "help", Name: "help",
Help: ` Help: `

View File

@ -45,7 +45,7 @@ func init() {
Name: "yandex", Name: "yandex",
NewFs: NewFs, NewFs: NewFs,
Config: func(name string) { Config: func(name string) {
err := oauthutil.Config(name, oauthConfig) err := oauthutil.Config("yandex", name, oauthConfig)
if err != nil { if err != nil {
log.Fatalf("Failed to configure token: %v", err) log.Fatalf("Failed to configure token: %v", err)
} }