| // Copyright 2011 Google Inc. 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/gob" | |
| "errors" | |
| "flag" | |
| "fmt" | |
| "hash/fnv" | |
| "io/ioutil" | |
| "log" | |
| "net/http" | |
| "net/http/httptest" | |
| "net/url" | |
| "os" | |
| "os/exec" | |
| "path/filepath" | |
| "runtime" | |
| "strings" | |
| "time" | |
| "golang.org/x/oauth2" | |
| "golang.org/x/oauth2/google" | |
| ) | |
| // Flags | |
| var ( | |
| clientID = flag.String("clientid", "", "OAuth 2.0 Client ID. If non-empty, overrides --clientid_file") | |
| clientIDFile = flag.String("clientid-file", "clientid.dat", | |
| "Name of a file containing just the project's OAuth 2.0 Client ID from https://developers.google.com/console.") | |
| secret = flag.String("secret", "", "OAuth 2.0 Client Secret. If non-empty, overrides --secret_file") | |
| secretFile = flag.String("secret-file", "clientsecret.dat", | |
| "Name of a file containing just the project's OAuth 2.0 Client Secret from https://developers.google.com/console.") | |
| cacheToken = flag.Bool("cachetoken", true, "cache the OAuth 2.0 token") | |
| debug = flag.Bool("debug", false, "show HTTP traffic") | |
| ) | |
| func usage() { | |
| fmt.Fprintf(os.Stderr, "Usage: go-api-demo <api-demo-name> [api name args]\n\nPossible APIs:\n\n") | |
| for n := range demoFunc { | |
| fmt.Fprintf(os.Stderr, " * %s\n", n) | |
| } | |
| os.Exit(2) | |
| } | |
| func main() { | |
| flag.Parse() | |
| if flag.NArg() == 0 { | |
| usage() | |
| } | |
| name := flag.Arg(0) | |
| demo, ok := demoFunc[name] | |
| if !ok { | |
| usage() | |
| } | |
| config := &oauth2.Config{ | |
| ClientID: valueOrFileContents(*clientID, *clientIDFile), | |
| ClientSecret: valueOrFileContents(*secret, *secretFile), | |
| Endpoint: google.Endpoint, | |
| Scopes: []string{demoScope[name]}, | |
| } | |
| ctx := context.Background() | |
| if *debug { | |
| ctx = context.WithValue(ctx, oauth2.HTTPClient, &http.Client{ | |
| Transport: &logTransport{http.DefaultTransport}, | |
| }) | |
| } | |
| c := newOAuthClient(ctx, config) | |
| demo(c, flag.Args()[1:]) | |
| } | |
| var ( | |
| demoFunc = make(map[string]func(*http.Client, []string)) | |
| demoScope = make(map[string]string) | |
| ) | |
| func registerDemo(name, scope string, main func(c *http.Client, argv []string)) { | |
| if demoFunc[name] != nil { | |
| panic(name + " already registered") | |
| } | |
| demoFunc[name] = main | |
| demoScope[name] = scope | |
| } | |
| func osUserCacheDir() string { | |
| switch runtime.GOOS { | |
| case "darwin": | |
| return filepath.Join(os.Getenv("HOME"), "Library", "Caches") | |
| case "linux", "freebsd": | |
| return filepath.Join(os.Getenv("HOME"), ".cache") | |
| } | |
| log.Printf("TODO: osUserCacheDir on GOOS %q", runtime.GOOS) | |
| return "." | |
| } | |
| func tokenCacheFile(config *oauth2.Config) string { | |
| hash := fnv.New32a() | |
| hash.Write([]byte(config.ClientID)) | |
| hash.Write([]byte(config.ClientSecret)) | |
| hash.Write([]byte(strings.Join(config.Scopes, " "))) | |
| fn := fmt.Sprintf("go-api-demo-tok%v", hash.Sum32()) | |
| return filepath.Join(osUserCacheDir(), url.QueryEscape(fn)) | |
| } | |
| func tokenFromFile(file string) (*oauth2.Token, error) { | |
| if !*cacheToken { | |
| return nil, errors.New("--cachetoken is false") | |
| } | |
| f, err := os.Open(file) | |
| if err != nil { | |
| return nil, err | |
| } | |
| t := new(oauth2.Token) | |
| err = gob.NewDecoder(f).Decode(t) | |
| return t, err | |
| } | |
| func saveToken(file string, token *oauth2.Token) { | |
| f, err := os.Create(file) | |
| if err != nil { | |
| log.Printf("Warning: failed to cache oauth token: %v", err) | |
| return | |
| } | |
| defer f.Close() | |
| gob.NewEncoder(f).Encode(token) | |
| } | |
| func newOAuthClient(ctx context.Context, config *oauth2.Config) *http.Client { | |
| cacheFile := tokenCacheFile(config) | |
| token, err := tokenFromFile(cacheFile) | |
| if err != nil { | |
| token = tokenFromWeb(ctx, config) | |
| saveToken(cacheFile, token) | |
| } else { | |
| log.Printf("Using cached token %#v from %q", token, cacheFile) | |
| } | |
| return config.Client(ctx, token) | |
| } | |
| func tokenFromWeb(ctx context.Context, config *oauth2.Config) *oauth2.Token { | |
| ch := make(chan string) | |
| randState := fmt.Sprintf("st%d", time.Now().UnixNano()) | |
| ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { | |
| if req.URL.Path == "/favicon.ico" { | |
| http.Error(rw, "", 404) | |
| return | |
| } | |
| if req.FormValue("state") != randState { | |
| log.Printf("State doesn't match: req = %#v", req) | |
| http.Error(rw, "", 500) | |
| return | |
| } | |
| if code := req.FormValue("code"); code != "" { | |
| fmt.Fprintf(rw, "<h1>Success</h1>Authorized.") | |
| rw.(http.Flusher).Flush() | |
| ch <- code | |
| return | |
| } | |
| log.Printf("no code") | |
| http.Error(rw, "", 500) | |
| })) | |
| defer ts.Close() | |
| config.RedirectURL = ts.URL | |
| authURL := config.AuthCodeURL(randState) | |
| go openURL(authURL) | |
| log.Printf("Authorize this app at: %s", authURL) | |
| code := <-ch | |
| log.Printf("Got code: %s", code) | |
| token, err := config.Exchange(ctx, code) | |
| if err != nil { | |
| log.Fatalf("Token exchange error: %v", err) | |
| } | |
| return token | |
| } | |
| func openURL(url string) { | |
| try := []string{"xdg-open", "google-chrome", "open"} | |
| for _, bin := range try { | |
| err := exec.Command(bin, url).Run() | |
| if err == nil { | |
| return | |
| } | |
| } | |
| log.Printf("Error opening URL in browser.") | |
| } | |
| func valueOrFileContents(value string, filename string) string { | |
| if value != "" { | |
| return value | |
| } | |
| slurp, err := ioutil.ReadFile(filename) | |
| if err != nil { | |
| log.Fatalf("Error reading %q: %v", filename, err) | |
| } | |
| return strings.TrimSpace(string(slurp)) | |
| } |