| // Copyright 2017 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package main | |
| import ( | |
| "context" | |
| "encoding/json" | |
| "fmt" | |
| "log" | |
| "os" | |
| "os/user" | |
| "path/filepath" | |
| "golang.org/x/oauth2" | |
| "golang.org/x/oauth2/google" | |
| ) | |
| var oauthConfig = oauth2.Config{ | |
| ClientID: "337869179296-kfjqcm11uodlrj39ifek1adtjjfb0b1p.apps.googleusercontent.com", | |
| ClientSecret: "zOMue3fEEUnz4Em39Ia_-4TN", | |
| Endpoint: google.Endpoint, | |
| Scopes: []string{"https://www.googleapis.com/auth/userinfo.email"}, | |
| RedirectURL: "oob", | |
| } | |
| // newTokenSource constructs a token source that will try to read a | |
| // cached token from a file and if missing or invalid will prompt the | |
| // user to refresh it. | |
| func newTokenSource() *tokenReplacer { | |
| // nil token will cause a refresh | |
| tok, _ := readToken() | |
| return &tokenReplacer{tok, oauthConfig.TokenSource(context.Background(), tok), &tokenPrompt{}} | |
| } | |
| // tokenPrompt is a TokenSource that interactively prompts the user | |
| // for a token using copy-paste. | |
| type tokenPrompt struct{} | |
| // Token obtains a new token from the user. | |
| func (*tokenPrompt) Token() (*oauth2.Token, error) { | |
| url := oauthConfig.AuthCodeURL("") | |
| fmt.Fprintf(os.Stderr, "benchsave must authenticate to %s.\n"+ | |
| "Note that uploaded files will be publicly recorded with your email address.\n"+ | |
| "\n"+ | |
| "%s\n"+ | |
| "\n"+ | |
| "Visit the URL above and enter the auth code: ", *server, url) | |
| var code string | |
| fmt.Scanln(&code) | |
| ctx := context.Background() | |
| return oauthConfig.Exchange(ctx, code) | |
| } | |
| // tokenReplacer is a TokenSource that tries to obtain a token from ts, and if it fails, obtains a token from tsr and uses it to replace ts. New tokens are cached on disk. | |
| type tokenReplacer struct { | |
| lastTok *oauth2.Token | |
| ts, tsr oauth2.TokenSource | |
| } | |
| // Token returns an existing or refreshed token, or uses t.tsr to obtain a new token. | |
| func (t *tokenReplacer) Token() (tok *oauth2.Token, err error) { | |
| defer func() { | |
| // If the AccessToken field changes, cache the new token. | |
| if tok != nil && (t.lastTok == nil || tok.AccessToken != t.lastTok.AccessToken) { | |
| if err := writeToken(tok); err != nil { | |
| log.Printf("cannot cache authentication: %v", err) | |
| } | |
| } | |
| }() | |
| tok, err = t.ts.Token() | |
| if err == nil { | |
| return | |
| } | |
| tok, err = t.tsr.Token() | |
| if err == nil { | |
| t.ts = oauthConfig.TokenSource(context.Background(), tok) | |
| } | |
| return | |
| } | |
| // tokenFilePath returns "$XDG_CONFIG_HOME/.config/benchsave/token.json" | |
| // with recursive expansion of defaults. | |
| func tokenFilePath() string { | |
| d := os.Getenv("XDG_CONFIG_HOME") | |
| if d == "" { | |
| home := os.Getenv("HOME") | |
| if home == "" { | |
| u, err := user.Current() | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| home = u.HomeDir | |
| } | |
| d = filepath.Join(home, ".config") | |
| } | |
| return filepath.Join(d, "benchsave", "token.json") | |
| } | |
| // readToken obtains a token from the filesystem. | |
| // If there is no valid token found, it returns a nil token and a reason. | |
| func readToken() (*oauth2.Token, error) { | |
| data, err := os.ReadFile(tokenFilePath()) | |
| if err != nil { | |
| return nil, err | |
| } | |
| tok := &oauth2.Token{} | |
| if err := json.Unmarshal(data, tok); err != nil { | |
| return nil, err | |
| } | |
| return tok, nil | |
| } | |
| // writeToken writes the token to the filesystem. | |
| func writeToken(tok *oauth2.Token) (err error) { | |
| p := tokenFilePath() | |
| if err := os.MkdirAll(filepath.Dir(p), 0700); err != nil { | |
| return err | |
| } | |
| data, err := json.Marshal(tok) | |
| if err != nil { | |
| return err | |
| } | |
| return os.WriteFile(p, data, 0600) | |
| } |