本站源代码
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.

202 lines
6.1KB

  1. // Copyright 2015 The Go Authors. 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 google
  5. import (
  6. "bufio"
  7. "context"
  8. "encoding/json"
  9. "errors"
  10. "fmt"
  11. "io"
  12. "net/http"
  13. "os"
  14. "os/user"
  15. "path/filepath"
  16. "runtime"
  17. "strings"
  18. "time"
  19. "golang.org/x/oauth2"
  20. )
  21. type sdkCredentials struct {
  22. Data []struct {
  23. Credential struct {
  24. ClientID string `json:"client_id"`
  25. ClientSecret string `json:"client_secret"`
  26. AccessToken string `json:"access_token"`
  27. RefreshToken string `json:"refresh_token"`
  28. TokenExpiry *time.Time `json:"token_expiry"`
  29. } `json:"credential"`
  30. Key struct {
  31. Account string `json:"account"`
  32. Scope string `json:"scope"`
  33. } `json:"key"`
  34. }
  35. }
  36. // An SDKConfig provides access to tokens from an account already
  37. // authorized via the Google Cloud SDK.
  38. type SDKConfig struct {
  39. conf oauth2.Config
  40. initialToken *oauth2.Token
  41. }
  42. // NewSDKConfig creates an SDKConfig for the given Google Cloud SDK
  43. // account. If account is empty, the account currently active in
  44. // Google Cloud SDK properties is used.
  45. // Google Cloud SDK credentials must be created by running `gcloud auth`
  46. // before using this function.
  47. // The Google Cloud SDK is available at https://cloud.google.com/sdk/.
  48. func NewSDKConfig(account string) (*SDKConfig, error) {
  49. configPath, err := sdkConfigPath()
  50. if err != nil {
  51. return nil, fmt.Errorf("oauth2/google: error getting SDK config path: %v", err)
  52. }
  53. credentialsPath := filepath.Join(configPath, "credentials")
  54. f, err := os.Open(credentialsPath)
  55. if err != nil {
  56. return nil, fmt.Errorf("oauth2/google: failed to load SDK credentials: %v", err)
  57. }
  58. defer f.Close()
  59. var c sdkCredentials
  60. if err := json.NewDecoder(f).Decode(&c); err != nil {
  61. return nil, fmt.Errorf("oauth2/google: failed to decode SDK credentials from %q: %v", credentialsPath, err)
  62. }
  63. if len(c.Data) == 0 {
  64. return nil, fmt.Errorf("oauth2/google: no credentials found in %q, run `gcloud auth login` to create one", credentialsPath)
  65. }
  66. if account == "" {
  67. propertiesPath := filepath.Join(configPath, "properties")
  68. f, err := os.Open(propertiesPath)
  69. if err != nil {
  70. return nil, fmt.Errorf("oauth2/google: failed to load SDK properties: %v", err)
  71. }
  72. defer f.Close()
  73. ini, err := parseINI(f)
  74. if err != nil {
  75. return nil, fmt.Errorf("oauth2/google: failed to parse SDK properties %q: %v", propertiesPath, err)
  76. }
  77. core, ok := ini["core"]
  78. if !ok {
  79. return nil, fmt.Errorf("oauth2/google: failed to find [core] section in %v", ini)
  80. }
  81. active, ok := core["account"]
  82. if !ok {
  83. return nil, fmt.Errorf("oauth2/google: failed to find %q attribute in %v", "account", core)
  84. }
  85. account = active
  86. }
  87. for _, d := range c.Data {
  88. if account == "" || d.Key.Account == account {
  89. if d.Credential.AccessToken == "" && d.Credential.RefreshToken == "" {
  90. return nil, fmt.Errorf("oauth2/google: no token available for account %q", account)
  91. }
  92. var expiry time.Time
  93. if d.Credential.TokenExpiry != nil {
  94. expiry = *d.Credential.TokenExpiry
  95. }
  96. return &SDKConfig{
  97. conf: oauth2.Config{
  98. ClientID: d.Credential.ClientID,
  99. ClientSecret: d.Credential.ClientSecret,
  100. Scopes: strings.Split(d.Key.Scope, " "),
  101. Endpoint: Endpoint,
  102. RedirectURL: "oob",
  103. },
  104. initialToken: &oauth2.Token{
  105. AccessToken: d.Credential.AccessToken,
  106. RefreshToken: d.Credential.RefreshToken,
  107. Expiry: expiry,
  108. },
  109. }, nil
  110. }
  111. }
  112. return nil, fmt.Errorf("oauth2/google: no such credentials for account %q", account)
  113. }
  114. // Client returns an HTTP client using Google Cloud SDK credentials to
  115. // authorize requests. The token will auto-refresh as necessary. The
  116. // underlying http.RoundTripper will be obtained using the provided
  117. // context. The returned client and its Transport should not be
  118. // modified.
  119. func (c *SDKConfig) Client(ctx context.Context) *http.Client {
  120. return &http.Client{
  121. Transport: &oauth2.Transport{
  122. Source: c.TokenSource(ctx),
  123. },
  124. }
  125. }
  126. // TokenSource returns an oauth2.TokenSource that retrieve tokens from
  127. // Google Cloud SDK credentials using the provided context.
  128. // It will returns the current access token stored in the credentials,
  129. // and refresh it when it expires, but it won't update the credentials
  130. // with the new access token.
  131. func (c *SDKConfig) TokenSource(ctx context.Context) oauth2.TokenSource {
  132. return c.conf.TokenSource(ctx, c.initialToken)
  133. }
  134. // Scopes are the OAuth 2.0 scopes the current account is authorized for.
  135. func (c *SDKConfig) Scopes() []string {
  136. return c.conf.Scopes
  137. }
  138. func parseINI(ini io.Reader) (map[string]map[string]string, error) {
  139. result := map[string]map[string]string{
  140. "": {}, // root section
  141. }
  142. scanner := bufio.NewScanner(ini)
  143. currentSection := ""
  144. for scanner.Scan() {
  145. line := strings.TrimSpace(scanner.Text())
  146. if strings.HasPrefix(line, ";") {
  147. // comment.
  148. continue
  149. }
  150. if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
  151. currentSection = strings.TrimSpace(line[1 : len(line)-1])
  152. result[currentSection] = map[string]string{}
  153. continue
  154. }
  155. parts := strings.SplitN(line, "=", 2)
  156. if len(parts) == 2 && parts[0] != "" {
  157. result[currentSection][strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
  158. }
  159. }
  160. if err := scanner.Err(); err != nil {
  161. return nil, fmt.Errorf("error scanning ini: %v", err)
  162. }
  163. return result, nil
  164. }
  165. // sdkConfigPath tries to guess where the gcloud config is located.
  166. // It can be overridden during tests.
  167. var sdkConfigPath = func() (string, error) {
  168. if runtime.GOOS == "windows" {
  169. return filepath.Join(os.Getenv("APPDATA"), "gcloud"), nil
  170. }
  171. homeDir := guessUnixHomeDir()
  172. if homeDir == "" {
  173. return "", errors.New("unable to get current user home directory: os/user lookup failed; $HOME is empty")
  174. }
  175. return filepath.Join(homeDir, ".config", "gcloud"), nil
  176. }
  177. func guessUnixHomeDir() string {
  178. // Prefer $HOME over user.Current due to glibc bug: golang.org/issue/13470
  179. if v := os.Getenv("HOME"); v != "" {
  180. return v
  181. }
  182. // Else, fall back to user.Current:
  183. if u, err := user.Current(); err == nil {
  184. return u.HomeDir
  185. }
  186. return ""
  187. }
上海开阖软件有限公司 沪ICP备12045867号-1