From 7463a7a509caed54001b6dff2e81d299747d1eea Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Tue, 18 Aug 2015 08:55:09 +0100 Subject: [PATCH] Use "golang.org/x/oauth2" as oauth libary of choice - fixes #102 * get rid of depreprecated "code.google.com/p/goauth2/oauth" * store tokens in config file as before * read old format tokens and write in new format seamlessly * set our own transport to enforce timeouts etc --- drive/drive.go | 28 ++-- googleauth/googleauth.go | 137 ------------------- googlecloudstorage/googlecloudstorage.go | 28 ++-- oauthutil/oauthutil.go | 166 +++++++++++++++++++++++ 4 files changed, 202 insertions(+), 157 deletions(-) delete mode 100644 googleauth/googleauth.go create mode 100644 oauthutil/oauthutil.go diff --git a/drive/drive.go b/drive/drive.go index 9bdd7ea3b..a64aca415 100644 --- a/drive/drive.go +++ b/drive/drive.go @@ -10,22 +10,25 @@ package drive import ( "fmt" "io" + "log" "net/http" "strings" "sync" "time" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" "google.golang.org/api/drive/v2" "google.golang.org/api/googleapi" "github.com/ncw/rclone/fs" - "github.com/ncw/rclone/googleauth" + "github.com/ncw/rclone/oauthutil" "github.com/ogier/pflag" ) // Constants const ( - rcloneClientId = "202264815644.apps.googleusercontent.com" + rcloneClientID = "202264815644.apps.googleusercontent.com" rcloneClientSecret = "X4Z3ca8xfWDb1Voo-F9a7ZxJ" driveFolderType = "application/vnd.google-apps.folder" timeFormatIn = time.RFC3339 @@ -45,10 +48,12 @@ var ( chunkSize = fs.SizeSuffix(256 * 1024) driveUploadCutoff = chunkSize // Description of how to auth for this app - driveAuth = &googleauth.Auth{ - Scope: "https://www.googleapis.com/auth/drive", - DefaultClientId: rcloneClientId, - DefaultClientSecret: rcloneClientSecret, + driveConfig = &oauth2.Config{ + Scopes: []string{"https://www.googleapis.com/auth/drive"}, + Endpoint: google.Endpoint, + ClientID: rcloneClientID, + ClientSecret: rcloneClientSecret, + RedirectURL: oauthutil.TitleBarRedirectURL, } ) @@ -58,7 +63,10 @@ func init() { Name: "drive", NewFs: NewFs, Config: func(name string) { - driveAuth.Config(name) + err := oauthutil.Config(name, driveConfig) + if err != nil { + log.Fatalf("Failed to configure token: %v", err) + } }, Options: []fs.Option{{ Name: "client_id", @@ -327,9 +335,9 @@ func NewFs(name, path string) (fs.Fs, error) { return nil, fmt.Errorf("drive: chunk size can't be less than 256k - was %v", chunkSize) } - t, err := driveAuth.NewTransport(name) + oAuthClient, err := oauthutil.NewClient(name, driveConfig) if err != nil { - return nil, err + log.Fatalf("Failed to configure drive: %v", err) } root, err := parseDrivePath(path) @@ -349,7 +357,7 @@ func NewFs(name, path string) (fs.Fs, error) { f.pacer <- struct{}{} // Create a new authorized Drive client. - f.client = t.Client() + f.client = oAuthClient f.svc, err = drive.New(f.client) if err != nil { return nil, fmt.Errorf("Couldn't create Drive client: %s", err) diff --git a/googleauth/googleauth.go b/googleauth/googleauth.go deleted file mode 100644 index 59a6853f2..000000000 --- a/googleauth/googleauth.go +++ /dev/null @@ -1,137 +0,0 @@ -// Common authentication between Google Drive and Google Cloud Storage -package googleauth - -import ( - "encoding/json" - "fmt" - "log" - - "code.google.com/p/goauth2/oauth" - "github.com/ncw/rclone/fs" -) - -// A token cache to save the token in the config file section named -type TokenCache string - -// Get the token from the config file - returns an error if it isn't present -func (name TokenCache) Token() (*oauth.Token, error) { - tokenString, err := fs.ConfigFile.GetValue(string(name), "token") - if err != nil { - return nil, err - } - if tokenString == "" { - return nil, fmt.Errorf("Empty token found - please reconfigure") - } - token := new(oauth.Token) - err = json.Unmarshal([]byte(tokenString), token) - if err != nil { - return nil, err - } - return token, nil - -} - -// Save the token to the config file -// -// This saves the config file if it changes -func (name TokenCache) PutToken(token *oauth.Token) error { - tokenBytes, err := json.Marshal(token) - if err != nil { - return err - } - tokenString := string(tokenBytes) - old := fs.ConfigFile.MustValue(string(name), "token") - if tokenString != old { - fs.ConfigFile.SetValue(string(name), "token", tokenString) - fs.SaveConfig() - } - return nil -} - -// Auth contains information to authenticate an app against google services -type Auth struct { - Scope string - DefaultClientId string - DefaultClientSecret string -} - -// Makes a new transport using authorisation from the config -// -// Doesn't have a token yet -func (auth *Auth) newTransport(name string) (*oauth.Transport, error) { - clientId := fs.ConfigFile.MustValue(name, "client_id") - if clientId == "" { - clientId = auth.DefaultClientId - } - clientSecret := fs.ConfigFile.MustValue(name, "client_secret") - if clientSecret == "" { - clientSecret = auth.DefaultClientSecret - } - - // Settings for authorization. - var config = &oauth.Config{ - ClientId: clientId, - ClientSecret: clientSecret, - Scope: auth.Scope, - RedirectURL: "urn:ietf:wg:oauth:2.0:oob", - AuthURL: "https://accounts.google.com/o/oauth2/auth", - TokenURL: "https://accounts.google.com/o/oauth2/token", - TokenCache: TokenCache(name), - } - - t := &oauth.Transport{ - Config: config, - Transport: fs.Config.Transport(), - } - - return t, nil -} - -// Makes a new transport using authorisation from the config with token -func (auth *Auth) NewTransport(name string) (*oauth.Transport, error) { - t, err := auth.newTransport(name) - if err != nil { - return nil, err - } - - // Try to pull the token from the cache; if this fails, we need to get one. - token, err := t.Config.TokenCache.Token() - if err != nil { - return nil, fmt.Errorf("Failed to get token: %s", err) - } - t.Token = token - - return t, nil -} - -// Configuration helper - called after the user has put in the defaults -func (auth *Auth) Config(name string) { - // See if already have a token - tokenString := fs.ConfigFile.MustValue(name, "token") - if tokenString != "" { - fmt.Printf("Already have a token - refresh?\n") - if !fs.Confirm() { - return - } - } - - // Get a transport - t, err := auth.newTransport(name) - if err != nil { - log.Fatalf("Couldn't make transport: %v", err) - } - - // Generate a URL for the user to visit for authorization. - authUrl := t.Config.AuthCodeURL("state") - fmt.Printf("Go to the following link in your browser\n") - fmt.Printf("%s\n", authUrl) - fmt.Printf("Log in, then type paste the token that is returned in the browser here\n") - - // Read the code, and exchange it for a token. - fmt.Printf("Enter verification code> ") - authCode := fs.ReadLine() - _, err = t.Exchange(authCode) - if err != nil { - log.Fatalf("Failed to get token: %v", err) - } -} diff --git a/googlecloudstorage/googlecloudstorage.go b/googlecloudstorage/googlecloudstorage.go index 61a3b2a45..8ced5a430 100644 --- a/googlecloudstorage/googlecloudstorage.go +++ b/googlecloudstorage/googlecloudstorage.go @@ -17,21 +17,24 @@ import ( "encoding/hex" "fmt" "io" + "log" "net/http" "path" "regexp" "strings" "time" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" "google.golang.org/api/googleapi" "google.golang.org/api/storage/v1" "github.com/ncw/rclone/fs" - "github.com/ncw/rclone/googleauth" + "github.com/ncw/rclone/oauthutil" ) const ( - rcloneClientId = "202264815644.apps.googleusercontent.com" + rcloneClientID = "202264815644.apps.googleusercontent.com" rcloneClientSecret = "X4Z3ca8xfWDb1Voo-F9a7ZxJ" timeFormatIn = time.RFC3339 timeFormatOut = "2006-01-02T15:04:05.000000000Z07:00" @@ -41,10 +44,12 @@ const ( var ( // Description of how to auth for this app - storageAuth = &googleauth.Auth{ - Scope: storage.DevstorageFullControlScope, - DefaultClientId: rcloneClientId, - DefaultClientSecret: rcloneClientSecret, + storageConfig = &oauth2.Config{ + Scopes: []string{storage.DevstorageFullControlScope}, + Endpoint: google.Endpoint, + ClientID: rcloneClientID, + ClientSecret: rcloneClientSecret, + RedirectURL: oauthutil.TitleBarRedirectURL, } ) @@ -54,7 +59,10 @@ func init() { Name: "google cloud storage", NewFs: NewFs, Config: func(name string) { - storageAuth.Config(name) + err := oauthutil.Config(name, storageConfig) + if err != nil { + log.Fatalf("Failed to configure token: %v", err) + } }, Options: []fs.Option{{ Name: "client_id", @@ -166,9 +174,9 @@ func parsePath(path string) (bucket, directory string, err error) { // NewFs contstructs an FsStorage from the path, bucket:path func NewFs(name, root string) (fs.Fs, error) { - t, err := storageAuth.NewTransport(name) + oAuthClient, err := oauthutil.NewClient(name, storageConfig) if err != nil { - return nil, err + log.Fatalf("Failed to configure Google Cloud Storage: %v", err) } bucket, directory, err := parsePath(root) @@ -192,7 +200,7 @@ func NewFs(name, root string) (fs.Fs, error) { } // Create a new authorized Drive client. - f.client = t.Client() + f.client = oAuthClient f.svc, err = storage.New(f.client) if err != nil { return nil, fmt.Errorf("Couldn't create Google Cloud Storage client: %s", err) diff --git a/oauthutil/oauthutil.go b/oauthutil/oauthutil.go new file mode 100644 index 000000000..24b8b04f9 --- /dev/null +++ b/oauthutil/oauthutil.go @@ -0,0 +1,166 @@ +package oauthutil + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/ncw/rclone/fs" + "golang.org/x/net/context" + "golang.org/x/oauth2" +) + +// configKey is the key used to store the token under +const configKey = "token" + +// 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 +// prompting the user to copy the code and paste it in the application. +const TitleBarRedirectURL = "urn:ietf:wg:oauth:2.0:oob" + +// oldToken contains an end-user's tokens. +// This is the data you must store to persist authentication. +// +// From the original code.google.com/p/goauth2/oauth package - used +// for backwards compatibility in the rclone config file +type oldToken struct { + AccessToken string + RefreshToken string + Expiry time.Time +} + +// getToken returns the token saved in the config file under +// section name. +func getToken(name string) (*oauth2.Token, error) { + tokenString, err := fs.ConfigFile.GetValue(string(name), configKey) + if err != nil { + return nil, err + } + if tokenString == "" { + return nil, fmt.Errorf("Empty token found - please run rclone config again") + } + token := new(oauth2.Token) + err = json.Unmarshal([]byte(tokenString), token) + if err != nil { + return nil, err + } + // if has data then return it + if token.AccessToken != "" && token.RefreshToken != "" { + return token, nil + } + // otherwise try parsing as oldToken + oldtoken := new(oldToken) + err = json.Unmarshal([]byte(tokenString), oldtoken) + if err != nil { + return nil, err + } + // Fill in result into new token + token.AccessToken = oldtoken.AccessToken + token.RefreshToken = oldtoken.RefreshToken + token.Expiry = oldtoken.Expiry + // Save new format in config file + err = putToken(name, token) + if err != nil { + return nil, err + } + return token, nil +} + +// putToken stores the token in the config file +// +// This saves the config file if it changes +func putToken(name string, token *oauth2.Token) error { + tokenBytes, err := json.Marshal(token) + if err != nil { + return err + } + tokenString := string(tokenBytes) + old := fs.ConfigFile.MustValue(name, configKey) + if tokenString != old { + fs.ConfigFile.SetValue(name, configKey, tokenString) + fs.SaveConfig() + fs.Debug(name, "Saving new token in config file") + } + return nil +} + +// tokenSource stores updated tokens in the config file +type tokenSource struct { + Name string + TokenSource oauth2.TokenSource + OldToken oauth2.Token +} + +// Token returns a token or an error. +// Token must be safe for concurrent use by multiple goroutines. +// The returned Token must not be modified. +// +// This saves the token in the config file if it has changed +func (ts *tokenSource) Token() (*oauth2.Token, error) { + token, err := ts.TokenSource.Token() + if err != nil { + return nil, err + } + if *token != ts.OldToken { + putToken(ts.Name, token) + } + return token, nil +} + +// Check interface satisfied +var _ oauth2.TokenSource = (*tokenSource)(nil) + +// Context returns a context with our HTTP Client baked in for oauth2 +func Context() context.Context { + return context.WithValue(nil, oauth2.HTTPClient, fs.Config.Client()) +} + +// NewClient gets a token from the config file and configures a Client +// with it +func NewClient(name string, config *oauth2.Config) (*http.Client, error) { + token, err := getToken(name) + if err != nil { + return nil, err + } + + // Set our own http client in the context + ctx := Context() + + // Wrap the TokenSource in our TokenSource which saves changed + // tokens in the config file + ts := &tokenSource{ + Name: name, + OldToken: *token, + TokenSource: config.TokenSource(ctx, token), + } + return oauth2.NewClient(ctx, ts), nil + +} + +// Config does the initial creation of the token +func Config(name string, config *oauth2.Config) error { + // See if already have a token + tokenString := fs.ConfigFile.MustValue(name, "token") + if tokenString != "" { + fmt.Printf("Already have a token - refresh?\n") + if !fs.Confirm() { + return nil + } + } + + // Generate a URL for the user to visit for authorization. + authUrl := config.AuthCodeURL("state") + fmt.Printf("Go to the following link in your browser\n") + fmt.Printf("%s\n", authUrl) + fmt.Printf("Log in, then type paste the token that is returned in the browser here\n") + + // Read the code, and exchange it for a token. + fmt.Printf("Enter verification code> ") + authCode := fs.ReadLine() + token, err := config.Exchange(oauth2.NoContext, authCode) + if err != nil { + return fmt.Errorf("Failed to get token: %v", err) + } + return putToken(name, token) +}