1// Copyright 2011 Google LLC. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5package main 6 7import ( 8 "context" 9 "encoding/gob" 10 "errors" 11 "flag" 12 "fmt" 13 "hash/fnv" 14 "io/ioutil" 15 "log" 16 "net/http" 17 "net/http/httptest" 18 "net/url" 19 "os" 20 "os/exec" 21 "path/filepath" 22 "runtime" 23 "strings" 24 "time" 25 26 "golang.org/x/oauth2" 27 "golang.org/x/oauth2/google" 28) 29 30// Flags 31var ( 32 clientID = flag.String("clientid", "", "OAuth 2.0 Client ID. If non-empty, overrides --clientid_file") 33 clientIDFile = flag.String("clientid-file", "clientid.dat", 34 "Name of a file containing just the project's OAuth 2.0 Client ID from https://developers.google.com/console.") 35 secret = flag.String("secret", "", "OAuth 2.0 Client Secret. If non-empty, overrides --secret_file") 36 secretFile = flag.String("secret-file", "clientsecret.dat", 37 "Name of a file containing just the project's OAuth 2.0 Client Secret from https://developers.google.com/console.") 38 cacheToken = flag.Bool("cachetoken", true, "cache the OAuth 2.0 token") 39 debug = flag.Bool("debug", false, "show HTTP traffic") 40) 41 42func usage() { 43 fmt.Fprintf(os.Stderr, "Usage: go-api-demo <api-demo-name> [api name args]\n\nPossible APIs:\n\n") 44 for n := range demoFunc { 45 fmt.Fprintf(os.Stderr, " * %s\n", n) 46 } 47 os.Exit(2) 48} 49 50func main() { 51 flag.Parse() 52 if flag.NArg() == 0 { 53 usage() 54 } 55 56 name := flag.Arg(0) 57 demo, ok := demoFunc[name] 58 if !ok { 59 usage() 60 } 61 62 config := &oauth2.Config{ 63 ClientID: valueOrFileContents(*clientID, *clientIDFile), 64 ClientSecret: valueOrFileContents(*secret, *secretFile), 65 Endpoint: google.Endpoint, 66 Scopes: []string{demoScope[name]}, 67 } 68 69 ctx := context.Background() 70 if *debug { 71 ctx = context.WithValue(ctx, oauth2.HTTPClient, &http.Client{ 72 Transport: &logTransport{http.DefaultTransport}, 73 }) 74 } 75 c := newOAuthClient(ctx, config) 76 demo(c, flag.Args()[1:]) 77} 78 79var ( 80 demoFunc = make(map[string]func(*http.Client, []string)) 81 demoScope = make(map[string]string) 82) 83 84func registerDemo(name, scope string, main func(c *http.Client, argv []string)) { 85 if demoFunc[name] != nil { 86 panic(name + " already registered") 87 } 88 demoFunc[name] = main 89 demoScope[name] = scope 90} 91 92func osUserCacheDir() string { 93 switch runtime.GOOS { 94 case "darwin": 95 return filepath.Join(os.Getenv("HOME"), "Library", "Caches") 96 case "linux", "freebsd": 97 return filepath.Join(os.Getenv("HOME"), ".cache") 98 } 99 log.Printf("TODO: osUserCacheDir on GOOS %q", runtime.GOOS) 100 return "." 101} 102 103func tokenCacheFile(config *oauth2.Config) string { 104 hash := fnv.New32a() 105 hash.Write([]byte(config.ClientID)) 106 hash.Write([]byte(config.ClientSecret)) 107 hash.Write([]byte(strings.Join(config.Scopes, " "))) 108 fn := fmt.Sprintf("go-api-demo-tok%v", hash.Sum32()) 109 return filepath.Join(osUserCacheDir(), url.QueryEscape(fn)) 110} 111 112func tokenFromFile(file string) (*oauth2.Token, error) { 113 if !*cacheToken { 114 return nil, errors.New("--cachetoken is false") 115 } 116 f, err := os.Open(file) 117 if err != nil { 118 return nil, err 119 } 120 t := new(oauth2.Token) 121 err = gob.NewDecoder(f).Decode(t) 122 return t, err 123} 124 125func saveToken(file string, token *oauth2.Token) { 126 f, err := os.Create(file) 127 if err != nil { 128 log.Printf("Warning: failed to cache oauth token: %v", err) 129 return 130 } 131 defer f.Close() 132 gob.NewEncoder(f).Encode(token) 133} 134 135func newOAuthClient(ctx context.Context, config *oauth2.Config) *http.Client { 136 cacheFile := tokenCacheFile(config) 137 token, err := tokenFromFile(cacheFile) 138 if err != nil { 139 token = tokenFromWeb(ctx, config) 140 saveToken(cacheFile, token) 141 } else { 142 log.Printf("Using cached token %#v from %q", token, cacheFile) 143 } 144 145 return config.Client(ctx, token) 146} 147 148func tokenFromWeb(ctx context.Context, config *oauth2.Config) *oauth2.Token { 149 ch := make(chan string) 150 randState := fmt.Sprintf("st%d", time.Now().UnixNano()) 151 ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 152 if req.URL.Path == "/favicon.ico" { 153 http.Error(rw, "", 404) 154 return 155 } 156 if req.FormValue("state") != randState { 157 log.Printf("State doesn't match: req = %#v", req) 158 http.Error(rw, "", 500) 159 return 160 } 161 if code := req.FormValue("code"); code != "" { 162 fmt.Fprintf(rw, "<h1>Success</h1>Authorized.") 163 rw.(http.Flusher).Flush() 164 ch <- code 165 return 166 } 167 log.Printf("no code") 168 http.Error(rw, "", 500) 169 })) 170 defer ts.Close() 171 172 config.RedirectURL = ts.URL 173 authURL := config.AuthCodeURL(randState) 174 go openURL(authURL) 175 log.Printf("Authorize this app at: %s", authURL) 176 code := <-ch 177 log.Printf("Got code: %s", code) 178 179 token, err := config.Exchange(ctx, code) 180 if err != nil { 181 log.Fatalf("Token exchange error: %v", err) 182 } 183 return token 184} 185 186func openURL(url string) { 187 try := []string{"xdg-open", "google-chrome", "open"} 188 for _, bin := range try { 189 err := exec.Command(bin, url).Run() 190 if err == nil { 191 return 192 } 193 } 194 log.Printf("Error opening URL in browser.") 195} 196 197func valueOrFileContents(value string, filename string) string { 198 if value != "" { 199 return value 200 } 201 slurp, err := ioutil.ReadFile(filename) 202 if err != nil { 203 log.Fatalf("Error reading %q: %v", filename, err) 204 } 205 return strings.TrimSpace(string(slurp)) 206} 207