You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

207 lines
5.1 KiB

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