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 }