github.com/ncw/rclone@v1.48.1-0.20190724201158-a35aa1360e3e/lib/oauthutil/oauthutil.go (about)

     1  package oauthutil
     2  
     3  import (
     4  	"context"
     5  	"crypto/rand"
     6  	"encoding/json"
     7  	"fmt"
     8  	"html/template"
     9  	"log"
    10  	"net"
    11  	"net/http"
    12  	"strings"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/ncw/rclone/fs"
    17  	"github.com/ncw/rclone/fs/config"
    18  	"github.com/ncw/rclone/fs/config/configmap"
    19  	"github.com/ncw/rclone/fs/fshttp"
    20  	"github.com/pkg/errors"
    21  	"github.com/skratchdot/open-golang/open"
    22  	"golang.org/x/oauth2"
    23  )
    24  
    25  const (
    26  	// TitleBarRedirectURL is the OAuth2 redirect URL to use when the authorization
    27  	// code should be returned in the title bar of the browser, with the page text
    28  	// prompting the user to copy the code and paste it in the application.
    29  	TitleBarRedirectURL = "urn:ietf:wg:oauth:2.0:oob"
    30  
    31  	// bindPort is the port that we bind the local webserver to
    32  	bindPort = "53682"
    33  
    34  	// bindAddress is binding for local webserver when active
    35  	bindAddress = "127.0.0.1:" + bindPort
    36  
    37  	// RedirectURL is redirect to local webserver when active
    38  	RedirectURL = "http://" + bindAddress + "/"
    39  
    40  	// RedirectPublicURL is redirect to local webserver when active with public name
    41  	RedirectPublicURL = "http://localhost.rclone.org:" + bindPort + "/"
    42  
    43  	// RedirectLocalhostURL is redirect to local webserver when active with localhost
    44  	RedirectLocalhostURL = "http://localhost:" + bindPort + "/"
    45  
    46  	// AuthResponse is a template to handle the redirect URL for oauth requests
    47  	AuthResponse = `<!DOCTYPE html>
    48  <html lang="en">
    49  <head>
    50  <meta charset="utf-8">
    51  <title>{{ if .OK }}Success!{{ else }}Failure!{{ end }}</title>
    52  </head>
    53  <body>
    54  <h1>{{ if .OK }}Success!{{ else }}Failure!{{ end }}</h1>
    55  <hr>
    56  <pre style="width: 750px; white-space: pre-wrap;">
    57  {{ if eq .OK false }}
    58  Error: {{ .AuthError.Name }}<br>
    59  {{ if .AuthError.Description }}Description: {{ .AuthError.Description }}<br>{{ end }}
    60  {{ if .AuthError.Code }}Code: {{ .AuthError.Code }}<br>{{ end }}
    61  {{ if .AuthError.HelpURL }}Look here for help: <a href="{{ .AuthError.HelpURL }}">{{ .AuthError.HelpURL }}</a><br>{{ end }}
    62  {{ else }}
    63  	{{ if .Code }}
    64  Please copy this code into rclone:
    65  {{ .Code }}
    66  	{{ else }}
    67  All done. Please go back to rclone.
    68  	{{ end }}
    69  {{ end }}
    70  </pre>
    71  </body>
    72  </html>
    73  `
    74  )
    75  
    76  // oldToken contains an end-user's tokens.
    77  // This is the data you must store to persist authentication.
    78  //
    79  // From the original code.google.com/p/goauth2/oauth package - used
    80  // for backwards compatibility in the rclone config file
    81  type oldToken struct {
    82  	AccessToken  string
    83  	RefreshToken string
    84  	Expiry       time.Time
    85  }
    86  
    87  // GetToken returns the token saved in the config file under
    88  // section name.
    89  func GetToken(name string, m configmap.Mapper) (*oauth2.Token, error) {
    90  	tokenString, ok := m.Get(config.ConfigToken)
    91  	if !ok || tokenString == "" {
    92  		return nil, errors.New("empty token found - please run rclone config again")
    93  	}
    94  	token := new(oauth2.Token)
    95  	err := json.Unmarshal([]byte(tokenString), token)
    96  	if err != nil {
    97  		return nil, err
    98  	}
    99  	// if has data then return it
   100  	if token.AccessToken != "" {
   101  		return token, nil
   102  	}
   103  	// otherwise try parsing as oldToken
   104  	oldtoken := new(oldToken)
   105  	err = json.Unmarshal([]byte(tokenString), oldtoken)
   106  	if err != nil {
   107  		return nil, err
   108  	}
   109  	// Fill in result into new token
   110  	token.AccessToken = oldtoken.AccessToken
   111  	token.RefreshToken = oldtoken.RefreshToken
   112  	token.Expiry = oldtoken.Expiry
   113  	// Save new format in config file
   114  	err = PutToken(name, m, token, false)
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  	return token, nil
   119  }
   120  
   121  // PutToken stores the token in the config file
   122  //
   123  // This saves the config file if it changes
   124  func PutToken(name string, m configmap.Mapper, token *oauth2.Token, newSection bool) error {
   125  	tokenBytes, err := json.Marshal(token)
   126  	if err != nil {
   127  		return err
   128  	}
   129  	tokenString := string(tokenBytes)
   130  	old, ok := m.Get(config.ConfigToken)
   131  	if !ok || tokenString != old {
   132  		err = config.SetValueAndSave(name, config.ConfigToken, tokenString)
   133  		if newSection && err != nil {
   134  			fs.Debugf(name, "Added new token to config, still needs to be saved")
   135  		} else if err != nil {
   136  			fs.Errorf(nil, "Failed to save new token in config file: %v", err)
   137  		} else {
   138  			fs.Debugf(name, "Saved new token in config file")
   139  		}
   140  	}
   141  	return nil
   142  }
   143  
   144  // TokenSource stores updated tokens in the config file
   145  type TokenSource struct {
   146  	mu          sync.Mutex
   147  	name        string
   148  	m           configmap.Mapper
   149  	tokenSource oauth2.TokenSource
   150  	token       *oauth2.Token
   151  	config      *oauth2.Config
   152  	ctx         context.Context
   153  	expiryTimer *time.Timer // signals whenever the token expires
   154  }
   155  
   156  // If token has expired then first try re-reading it from the config
   157  // file in case a concurrently running rclone has updated it already
   158  func (ts *TokenSource) reReadToken() bool {
   159  	tokenString, err := config.FileGetFresh(ts.name, config.ConfigToken)
   160  	if err != nil {
   161  		fs.Debugf(ts.name, "Failed to read token out of config file: %v", err)
   162  		return false
   163  	}
   164  	newToken := new(oauth2.Token)
   165  	err = json.Unmarshal([]byte(tokenString), newToken)
   166  	if err != nil {
   167  		fs.Debugf(ts.name, "Failed to parse token out of config file: %v", err)
   168  		return false
   169  	}
   170  	if !newToken.Valid() {
   171  		fs.Debugf(ts.name, "Loaded invalid token from config file - ignoring")
   172  		return false
   173  	}
   174  	fs.Debugf(ts.name, "Loaded fresh token from config file")
   175  	ts.token = newToken
   176  	ts.tokenSource = nil // invalidate since we changed the token
   177  	return true
   178  }
   179  
   180  // Token returns a token or an error.
   181  // Token must be safe for concurrent use by multiple goroutines.
   182  // The returned Token must not be modified.
   183  //
   184  // This saves the token in the config file if it has changed
   185  func (ts *TokenSource) Token() (*oauth2.Token, error) {
   186  	ts.mu.Lock()
   187  	defer ts.mu.Unlock()
   188  	var (
   189  		token   *oauth2.Token
   190  		err     error
   191  		changed = false
   192  	)
   193  	const maxTries = 5
   194  
   195  	// Try getting the token a few times
   196  	for i := 1; i <= maxTries; i++ {
   197  		// Try reading the token from the config file in case it has
   198  		// been updated by a concurrent rclone process
   199  		if !ts.token.Valid() {
   200  			if ts.reReadToken() {
   201  				changed = true
   202  			}
   203  		}
   204  
   205  		// Make a new token source if required
   206  		if ts.tokenSource == nil {
   207  			ts.tokenSource = ts.config.TokenSource(ts.ctx, ts.token)
   208  		}
   209  
   210  		token, err = ts.tokenSource.Token()
   211  		if err == nil {
   212  			break
   213  		}
   214  		fs.Debugf(ts.name, "Token refresh failed try %d/%d: %v", i, maxTries, err)
   215  		time.Sleep(1 * time.Second)
   216  	}
   217  	if err != nil {
   218  		return nil, err
   219  	}
   220  	changed = changed || (*token != *ts.token)
   221  	ts.token = token
   222  	if changed {
   223  		// Bump on the expiry timer if it is set
   224  		if ts.expiryTimer != nil {
   225  			ts.expiryTimer.Reset(ts.timeToExpiry())
   226  		}
   227  		err = PutToken(ts.name, ts.m, token, false)
   228  		if err != nil {
   229  			return nil, err
   230  		}
   231  	}
   232  	return token, nil
   233  }
   234  
   235  // Invalidate invalidates the token
   236  func (ts *TokenSource) Invalidate() {
   237  	ts.mu.Lock()
   238  	ts.token.AccessToken = ""
   239  	ts.mu.Unlock()
   240  }
   241  
   242  // timeToExpiry returns how long until the token expires
   243  //
   244  // Call with the lock held
   245  func (ts *TokenSource) timeToExpiry() time.Duration {
   246  	t := ts.token
   247  	if t == nil {
   248  		return 0
   249  	}
   250  	if t.Expiry.IsZero() {
   251  		return 3E9 * time.Second // ~95 years
   252  	}
   253  	return t.Expiry.Sub(time.Now())
   254  }
   255  
   256  // OnExpiry returns a channel which has the time written to it when
   257  // the token expires.  Note that there is only one channel so if
   258  // attaching multiple go routines it will only signal to one of them.
   259  func (ts *TokenSource) OnExpiry() <-chan time.Time {
   260  	ts.mu.Lock()
   261  	defer ts.mu.Unlock()
   262  	if ts.expiryTimer == nil {
   263  		ts.expiryTimer = time.NewTimer(ts.timeToExpiry())
   264  	}
   265  	return ts.expiryTimer.C
   266  }
   267  
   268  // Check interface satisfied
   269  var _ oauth2.TokenSource = (*TokenSource)(nil)
   270  
   271  // Context returns a context with our HTTP Client baked in for oauth2
   272  func Context(client *http.Client) context.Context {
   273  	return context.WithValue(context.Background(), oauth2.HTTPClient, client)
   274  }
   275  
   276  // overrideCredentials sets the ClientID and ClientSecret from the
   277  // config file if they are not blank.
   278  // If any value is overridden, true is returned.
   279  // the origConfig is copied
   280  func overrideCredentials(name string, m configmap.Mapper, origConfig *oauth2.Config) (newConfig *oauth2.Config, changed bool) {
   281  	newConfig = new(oauth2.Config)
   282  	*newConfig = *origConfig
   283  	changed = false
   284  	ClientID, ok := m.Get(config.ConfigClientID)
   285  	if ok && ClientID != "" {
   286  		newConfig.ClientID = ClientID
   287  		changed = true
   288  	}
   289  	ClientSecret, ok := m.Get(config.ConfigClientSecret)
   290  	if ok && ClientSecret != "" {
   291  		newConfig.ClientSecret = ClientSecret
   292  		changed = true
   293  	}
   294  	AuthURL, ok := m.Get(config.ConfigAuthURL)
   295  	if ok && AuthURL != "" {
   296  		newConfig.Endpoint.AuthURL = AuthURL
   297  		changed = true
   298  	}
   299  	TokenURL, ok := m.Get(config.ConfigTokenURL)
   300  	if ok && TokenURL != "" {
   301  		newConfig.Endpoint.TokenURL = TokenURL
   302  		changed = true
   303  	}
   304  	return newConfig, changed
   305  }
   306  
   307  // NewClientWithBaseClient gets a token from the config file and
   308  // configures a Client with it.  It returns the client and a
   309  // TokenSource which Invalidate may need to be called on.  It uses the
   310  // httpClient passed in as the base client.
   311  func NewClientWithBaseClient(name string, m configmap.Mapper, config *oauth2.Config, baseClient *http.Client) (*http.Client, *TokenSource, error) {
   312  	config, _ = overrideCredentials(name, m, config)
   313  	token, err := GetToken(name, m)
   314  	if err != nil {
   315  		return nil, nil, err
   316  	}
   317  
   318  	// Set our own http client in the context
   319  	ctx := Context(baseClient)
   320  
   321  	// Wrap the TokenSource in our TokenSource which saves changed
   322  	// tokens in the config file
   323  	ts := &TokenSource{
   324  		name:   name,
   325  		m:      m,
   326  		token:  token,
   327  		config: config,
   328  		ctx:    ctx,
   329  	}
   330  	return oauth2.NewClient(ctx, ts), ts, nil
   331  
   332  }
   333  
   334  // NewClient gets a token from the config file and configures a Client
   335  // with it.  It returns the client and a TokenSource which Invalidate may need to be called on
   336  func NewClient(name string, m configmap.Mapper, oauthConfig *oauth2.Config) (*http.Client, *TokenSource, error) {
   337  	return NewClientWithBaseClient(name, m, oauthConfig, fshttp.NewClient(fs.Config))
   338  }
   339  
   340  // Config does the initial creation of the token
   341  //
   342  // It may run an internal webserver to receive the results
   343  func Config(id, name string, m configmap.Mapper, config *oauth2.Config, opts ...oauth2.AuthCodeOption) error {
   344  	return doConfig(id, name, m, nil, config, true, opts)
   345  }
   346  
   347  // ConfigNoOffline does the same as Config but does not pass the
   348  // "access_type=offline" parameter.
   349  func ConfigNoOffline(id, name string, m configmap.Mapper, config *oauth2.Config, opts ...oauth2.AuthCodeOption) error {
   350  	return doConfig(id, name, m, nil, config, false, opts)
   351  }
   352  
   353  // ConfigErrorCheck does the same as Config, but allows the backend to pass a error handling function
   354  // This function gets called with the request made to rclone as a parameter if no code was found
   355  func ConfigErrorCheck(id, name string, m configmap.Mapper, errorHandler func(*http.Request) AuthError, config *oauth2.Config, opts ...oauth2.AuthCodeOption) error {
   356  	return doConfig(id, name, m, errorHandler, config, true, opts)
   357  }
   358  
   359  func doConfig(id, name string, m configmap.Mapper, errorHandler func(*http.Request) AuthError, oauthConfig *oauth2.Config, offline bool, opts []oauth2.AuthCodeOption) error {
   360  	oauthConfig, changed := overrideCredentials(name, m, oauthConfig)
   361  	authorizeOnlyValue, ok := m.Get(config.ConfigAuthorize)
   362  	authorizeOnly := ok && authorizeOnlyValue != "" // set if being run by "rclone authorize"
   363  
   364  	// See if already have a token
   365  	tokenString, ok := m.Get("token")
   366  	if ok && tokenString != "" {
   367  		fmt.Printf("Already have a token - refresh?\n")
   368  		if !config.ConfirmWithConfig(m, "config_refresh_token", true) {
   369  			return nil
   370  		}
   371  	}
   372  
   373  	// Ask the user whether they are using a local machine
   374  	isLocal := func() bool {
   375  		fmt.Printf("Use auto config?\n")
   376  		fmt.Printf(" * Say Y if not sure\n")
   377  		fmt.Printf(" * Say N if you are working on a remote or headless machine\n")
   378  		return config.ConfirmWithConfig(m, "config_is_local", true)
   379  	}
   380  
   381  	// Detect whether we should use internal web server
   382  	useWebServer := false
   383  	switch oauthConfig.RedirectURL {
   384  	case RedirectURL, RedirectPublicURL, RedirectLocalhostURL:
   385  		if changed {
   386  			fmt.Printf("Make sure your Redirect URL is set to %q in your custom config.\n", oauthConfig.RedirectURL)
   387  		}
   388  		useWebServer = true
   389  		if authorizeOnly {
   390  			break
   391  		}
   392  		if !isLocal() {
   393  			fmt.Printf("For this to work, you will need rclone available on a machine that has a web browser available.\n")
   394  			fmt.Printf("Execute the following on your machine:\n")
   395  			if changed {
   396  				fmt.Printf("\trclone authorize %q %q %q\n", id, oauthConfig.ClientID, oauthConfig.ClientSecret)
   397  			} else {
   398  				fmt.Printf("\trclone authorize %q\n", id)
   399  			}
   400  			fmt.Println("Then paste the result below:")
   401  			code := ""
   402  			for code == "" {
   403  				fmt.Printf("result> ")
   404  				code = strings.TrimSpace(config.ReadLine())
   405  			}
   406  			token := &oauth2.Token{}
   407  			err := json.Unmarshal([]byte(code), token)
   408  			if err != nil {
   409  				return err
   410  			}
   411  			return PutToken(name, m, token, true)
   412  		}
   413  	case TitleBarRedirectURL:
   414  		useWebServer = authorizeOnly
   415  		if !authorizeOnly {
   416  			useWebServer = isLocal()
   417  		}
   418  		if useWebServer {
   419  			// copy the config and set to use the internal webserver
   420  			configCopy := *oauthConfig
   421  			oauthConfig = &configCopy
   422  			oauthConfig.RedirectURL = RedirectURL
   423  		}
   424  	}
   425  
   426  	// Make random state
   427  	stateBytes := make([]byte, 16)
   428  	_, err := rand.Read(stateBytes)
   429  	if err != nil {
   430  		return err
   431  	}
   432  	state := fmt.Sprintf("%x", stateBytes)
   433  	if offline {
   434  		opts = append(opts, oauth2.AccessTypeOffline)
   435  	}
   436  	authURL := oauthConfig.AuthCodeURL(state, opts...)
   437  
   438  	// Prepare webserver
   439  	server := authServer{
   440  		state:        state,
   441  		bindAddress:  bindAddress,
   442  		authURL:      authURL,
   443  		errorHandler: errorHandler,
   444  	}
   445  	if useWebServer {
   446  		server.code = make(chan string, 1)
   447  		server.err = make(chan error, 1)
   448  		go server.Start()
   449  		defer server.Stop()
   450  		authURL = "http://" + bindAddress + "/auth"
   451  	}
   452  
   453  	// Generate a URL for the user to visit for authorization.
   454  	_ = open.Start(authURL)
   455  	fmt.Printf("If your browser doesn't open automatically go to the following link: %s\n", authURL)
   456  	fmt.Printf("Log in and authorize rclone for access\n")
   457  
   458  	var authCode string
   459  	if useWebServer {
   460  		// Read the code, and exchange it for a token.
   461  		fmt.Printf("Waiting for code...\n")
   462  		authCode = <-server.code
   463  		authError := <-server.err
   464  		if authCode != "" {
   465  			fmt.Printf("Got code\n")
   466  		} else {
   467  			if authError != nil {
   468  				return authError
   469  			}
   470  			return errors.New("failed to get code")
   471  		}
   472  	} else {
   473  		// Read the code, and exchange it for a token.
   474  		fmt.Printf("Enter verification code> ")
   475  		authCode = config.ReadLine()
   476  	}
   477  	token, err := oauthConfig.Exchange(oauth2.NoContext, authCode)
   478  	if err != nil {
   479  		return errors.Wrap(err, "failed to get token")
   480  	}
   481  
   482  	// Print code if we do automatic retrieval
   483  	if authorizeOnly {
   484  		result, err := json.Marshal(token)
   485  		if err != nil {
   486  			return errors.Wrap(err, "failed to marshal token")
   487  		}
   488  		fmt.Printf("Paste the following into your remote machine --->\n%s\n<---End paste\n", result)
   489  	}
   490  	return PutToken(name, m, token, true)
   491  }
   492  
   493  // Local web server for collecting auth
   494  type authServer struct {
   495  	state        string
   496  	listener     net.Listener
   497  	bindAddress  string
   498  	code         chan string
   499  	err          chan error
   500  	authURL      string
   501  	server       *http.Server
   502  	errorHandler func(*http.Request) AuthError
   503  }
   504  
   505  // AuthError gets returned by the backend's errorHandler function
   506  type AuthError struct {
   507  	Name        string
   508  	Description string
   509  	Code        string
   510  	HelpURL     string
   511  }
   512  
   513  // AuthResponseData can fill the AuthResponse template
   514  type AuthResponseData struct {
   515  	OK   bool   // Failure or Success?
   516  	Code string // code to paste into rclone config
   517  	AuthError
   518  }
   519  
   520  // startWebServer runs an internal web server to receive config details
   521  func (s *authServer) Start() {
   522  	fs.Debugf(nil, "Starting auth server on %s", s.bindAddress)
   523  	mux := http.NewServeMux()
   524  	s.server = &http.Server{
   525  		Addr:    s.bindAddress,
   526  		Handler: mux,
   527  	}
   528  	s.server.SetKeepAlivesEnabled(false)
   529  	mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, req *http.Request) {
   530  		http.Error(w, "", 404)
   531  		return
   532  	})
   533  	mux.HandleFunc("/auth", func(w http.ResponseWriter, req *http.Request) {
   534  		http.Redirect(w, req, s.authURL, http.StatusTemporaryRedirect)
   535  		return
   536  	})
   537  	mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
   538  		w.Header().Set("Content-Type", "text/html")
   539  		fs.Debugf(nil, "Received request on auth server")
   540  		code := req.FormValue("code")
   541  		var err error
   542  		var t = template.Must(template.New("authResponse").Parse(AuthResponse))
   543  		resp := AuthResponseData{AuthError: AuthError{}}
   544  		if code != "" {
   545  			state := req.FormValue("state")
   546  			if state != s.state {
   547  				fs.Debugf(nil, "State did not match: want %q got %q", s.state, state)
   548  				resp.OK = false
   549  				resp.AuthError = AuthError{
   550  					Name: "Auth State doesn't match",
   551  				}
   552  			} else {
   553  				fs.Debugf(nil, "Successfully got code")
   554  				resp.OK = true
   555  				if s.code == nil {
   556  					resp.Code = code
   557  				}
   558  			}
   559  		} else {
   560  			fs.Debugf(nil, "No code found on request")
   561  			var authError AuthError
   562  			if s.errorHandler == nil {
   563  				authError = AuthError{
   564  					Name:        "Auth Error",
   565  					Description: "No code found returned by remote server.",
   566  				}
   567  			} else {
   568  				authError = s.errorHandler(req)
   569  			}
   570  			err = fmt.Errorf("Error: %s\nCode: %s\nDescription: %s\nHelp: %s",
   571  				authError.Name, authError.Code, authError.Description, authError.HelpURL)
   572  			resp.OK = false
   573  			resp.AuthError = authError
   574  			w.WriteHeader(500)
   575  		}
   576  		if err := t.Execute(w, resp); err != nil {
   577  			fs.Debugf(nil, "Could not execute template for web response.")
   578  		}
   579  		if s.code != nil {
   580  			s.code <- code
   581  			s.err <- err
   582  		}
   583  	})
   584  
   585  	var err error
   586  	s.listener, err = net.Listen("tcp", s.bindAddress)
   587  	if err != nil {
   588  		log.Fatalf("Failed to start auth webserver: %v", err)
   589  	}
   590  	err = s.server.Serve(s.listener)
   591  	fs.Debugf(nil, "Closed auth server with error: %v", err)
   592  }
   593  
   594  func (s *authServer) Stop() {
   595  	fs.Debugf(nil, "Closing auth server")
   596  	if s.code != nil {
   597  		close(s.code)
   598  		s.code = nil
   599  	}
   600  	_ = s.listener.Close()
   601  
   602  	// close the server
   603  	_ = s.server.Close()
   604  }