mirror of
https://github.com/rclone/rclone
synced 2024-12-18 07:45:55 +01:00
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
This commit is contained in:
parent
9ed2de3d6e
commit
7463a7a509
@ -10,22 +10,25 @@ package drive
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
"golang.org/x/oauth2/google"
|
||||||
"google.golang.org/api/drive/v2"
|
"google.golang.org/api/drive/v2"
|
||||||
"google.golang.org/api/googleapi"
|
"google.golang.org/api/googleapi"
|
||||||
|
|
||||||
"github.com/ncw/rclone/fs"
|
"github.com/ncw/rclone/fs"
|
||||||
"github.com/ncw/rclone/googleauth"
|
"github.com/ncw/rclone/oauthutil"
|
||||||
"github.com/ogier/pflag"
|
"github.com/ogier/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const (
|
const (
|
||||||
rcloneClientId = "202264815644.apps.googleusercontent.com"
|
rcloneClientID = "202264815644.apps.googleusercontent.com"
|
||||||
rcloneClientSecret = "X4Z3ca8xfWDb1Voo-F9a7ZxJ"
|
rcloneClientSecret = "X4Z3ca8xfWDb1Voo-F9a7ZxJ"
|
||||||
driveFolderType = "application/vnd.google-apps.folder"
|
driveFolderType = "application/vnd.google-apps.folder"
|
||||||
timeFormatIn = time.RFC3339
|
timeFormatIn = time.RFC3339
|
||||||
@ -45,10 +48,12 @@ var (
|
|||||||
chunkSize = fs.SizeSuffix(256 * 1024)
|
chunkSize = fs.SizeSuffix(256 * 1024)
|
||||||
driveUploadCutoff = chunkSize
|
driveUploadCutoff = chunkSize
|
||||||
// Description of how to auth for this app
|
// Description of how to auth for this app
|
||||||
driveAuth = &googleauth.Auth{
|
driveConfig = &oauth2.Config{
|
||||||
Scope: "https://www.googleapis.com/auth/drive",
|
Scopes: []string{"https://www.googleapis.com/auth/drive"},
|
||||||
DefaultClientId: rcloneClientId,
|
Endpoint: google.Endpoint,
|
||||||
DefaultClientSecret: rcloneClientSecret,
|
ClientID: rcloneClientID,
|
||||||
|
ClientSecret: rcloneClientSecret,
|
||||||
|
RedirectURL: oauthutil.TitleBarRedirectURL,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -58,7 +63,10 @@ func init() {
|
|||||||
Name: "drive",
|
Name: "drive",
|
||||||
NewFs: NewFs,
|
NewFs: NewFs,
|
||||||
Config: func(name string) {
|
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{{
|
Options: []fs.Option{{
|
||||||
Name: "client_id",
|
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)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
log.Fatalf("Failed to configure drive: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
root, err := parseDrivePath(path)
|
root, err := parseDrivePath(path)
|
||||||
@ -349,7 +357,7 @@ func NewFs(name, path string) (fs.Fs, error) {
|
|||||||
f.pacer <- struct{}{}
|
f.pacer <- struct{}{}
|
||||||
|
|
||||||
// Create a new authorized Drive client.
|
// Create a new authorized Drive client.
|
||||||
f.client = t.Client()
|
f.client = oAuthClient
|
||||||
f.svc, err = drive.New(f.client)
|
f.svc, err = drive.New(f.client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Couldn't create Drive client: %s", err)
|
return nil, fmt.Errorf("Couldn't create Drive client: %s", err)
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -17,21 +17,24 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
"golang.org/x/oauth2/google"
|
||||||
"google.golang.org/api/googleapi"
|
"google.golang.org/api/googleapi"
|
||||||
"google.golang.org/api/storage/v1"
|
"google.golang.org/api/storage/v1"
|
||||||
|
|
||||||
"github.com/ncw/rclone/fs"
|
"github.com/ncw/rclone/fs"
|
||||||
"github.com/ncw/rclone/googleauth"
|
"github.com/ncw/rclone/oauthutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
rcloneClientId = "202264815644.apps.googleusercontent.com"
|
rcloneClientID = "202264815644.apps.googleusercontent.com"
|
||||||
rcloneClientSecret = "X4Z3ca8xfWDb1Voo-F9a7ZxJ"
|
rcloneClientSecret = "X4Z3ca8xfWDb1Voo-F9a7ZxJ"
|
||||||
timeFormatIn = time.RFC3339
|
timeFormatIn = time.RFC3339
|
||||||
timeFormatOut = "2006-01-02T15:04:05.000000000Z07:00"
|
timeFormatOut = "2006-01-02T15:04:05.000000000Z07:00"
|
||||||
@ -41,10 +44,12 @@ const (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
// Description of how to auth for this app
|
// Description of how to auth for this app
|
||||||
storageAuth = &googleauth.Auth{
|
storageConfig = &oauth2.Config{
|
||||||
Scope: storage.DevstorageFullControlScope,
|
Scopes: []string{storage.DevstorageFullControlScope},
|
||||||
DefaultClientId: rcloneClientId,
|
Endpoint: google.Endpoint,
|
||||||
DefaultClientSecret: rcloneClientSecret,
|
ClientID: rcloneClientID,
|
||||||
|
ClientSecret: rcloneClientSecret,
|
||||||
|
RedirectURL: oauthutil.TitleBarRedirectURL,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -54,7 +59,10 @@ func init() {
|
|||||||
Name: "google cloud storage",
|
Name: "google cloud storage",
|
||||||
NewFs: NewFs,
|
NewFs: NewFs,
|
||||||
Config: func(name string) {
|
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{{
|
Options: []fs.Option{{
|
||||||
Name: "client_id",
|
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
|
// NewFs contstructs an FsStorage from the path, bucket:path
|
||||||
func NewFs(name, root string) (fs.Fs, error) {
|
func NewFs(name, root string) (fs.Fs, error) {
|
||||||
t, err := storageAuth.NewTransport(name)
|
oAuthClient, err := oauthutil.NewClient(name, storageConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
log.Fatalf("Failed to configure Google Cloud Storage: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
bucket, directory, err := parsePath(root)
|
bucket, directory, err := parsePath(root)
|
||||||
@ -192,7 +200,7 @@ func NewFs(name, root string) (fs.Fs, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a new authorized Drive client.
|
// Create a new authorized Drive client.
|
||||||
f.client = t.Client()
|
f.client = oAuthClient
|
||||||
f.svc, err = storage.New(f.client)
|
f.svc, err = storage.New(f.client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Couldn't create Google Cloud Storage client: %s", err)
|
return nil, fmt.Errorf("Couldn't create Google Cloud Storage client: %s", err)
|
||||||
|
166
oauthutil/oauthutil.go
Normal file
166
oauthutil/oauthutil.go
Normal file
@ -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)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user