transfer.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. package token
  2. import (
  3. "bytes"
  4. "encoding/base64"
  5. "fmt"
  6. "io"
  7. "net/http"
  8. "net/url"
  9. "os"
  10. "time"
  11. "github.com/pkg/errors"
  12. "github.com/rs/zerolog"
  13. )
  14. const (
  15. baseStoreURL = "https://login.argotunnel.com/"
  16. clientTimeout = time.Second * 60
  17. )
  18. // RunTransfer does the transfer "dance" with the end result downloading the supported resource.
  19. // The expanded description is run is encapsulation of shared business logic needed
  20. // to request a resource (token/cert/etc) from the transfer service (loginhelper).
  21. // The "dance" we refer to is building a HTTP request, opening that in a browser waiting for
  22. // the user to complete an action, while it long polls in the background waiting for an
  23. // action to be completed to download the resource.
  24. func RunTransfer(transferURL *url.URL, resourceName, key, value string, shouldEncrypt bool, useHostOnly bool, log *zerolog.Logger) ([]byte, error) {
  25. encrypterClient, err := NewEncrypter("cloudflared_priv.pem", "cloudflared_pub.pem")
  26. if err != nil {
  27. return nil, err
  28. }
  29. requestURL, err := buildRequestURL(transferURL, key, value+encrypterClient.PublicKey(), shouldEncrypt, useHostOnly)
  30. if err != nil {
  31. return nil, err
  32. }
  33. // See AUTH-1423 for why we use stderr (the way git wraps ssh)
  34. err = OpenBrowser(requestURL)
  35. if err != nil {
  36. fmt.Fprintf(os.Stderr, "Please open the following URL and log in with your Cloudflare account:\n\n%s\n\nLeave cloudflared running to download the %s automatically.\n", requestURL, resourceName)
  37. } else {
  38. fmt.Fprintf(os.Stderr, "A browser window should have opened at the following URL:\n\n%s\n\nIf the browser failed to open, please visit the URL above directly in your browser.\n", requestURL)
  39. }
  40. var resourceData []byte
  41. if shouldEncrypt {
  42. buf, key, err := transferRequest(baseStoreURL+"transfer/"+encrypterClient.PublicKey(), log)
  43. if err != nil {
  44. return nil, err
  45. }
  46. decodedBuf, err := base64.StdEncoding.DecodeString(string(buf))
  47. if err != nil {
  48. return nil, err
  49. }
  50. decrypted, err := encrypterClient.Decrypt(decodedBuf, key)
  51. if err != nil {
  52. return nil, err
  53. }
  54. resourceData = decrypted
  55. } else {
  56. buf, _, err := transferRequest(baseStoreURL+encrypterClient.PublicKey(), log)
  57. if err != nil {
  58. return nil, err
  59. }
  60. resourceData = buf
  61. }
  62. return resourceData, nil
  63. }
  64. // BuildRequestURL creates a request suitable for a resource transfer.
  65. // it will return a constructed url based off the base url and query key/value provided.
  66. // cli will build a url for cli transfer request.
  67. func buildRequestURL(baseURL *url.URL, key, value string, cli, useHostOnly bool) (string, error) {
  68. q := baseURL.Query()
  69. q.Set(key, value)
  70. baseURL.RawQuery = q.Encode()
  71. if useHostOnly {
  72. baseURL.Path = ""
  73. }
  74. if !cli {
  75. return baseURL.String(), nil
  76. }
  77. q.Set("redirect_url", baseURL.String()) // we add the token as a query param on both the redirect_url and the main url
  78. q.Set("send_org_token", "true") // indicates that the cli endpoint should return both the org and app token
  79. baseURL.RawQuery = q.Encode() // and this actual baseURL.
  80. baseURL.Path = "cdn-cgi/access/cli"
  81. return baseURL.String(), nil
  82. }
  83. // transferRequest downloads the requested resource from the request URL
  84. func transferRequest(requestURL string, log *zerolog.Logger) ([]byte, string, error) {
  85. client := &http.Client{Timeout: clientTimeout}
  86. const pollAttempts = 10
  87. // we do "long polling" on the endpoint to get the resource.
  88. for i := 0; i < pollAttempts; i++ {
  89. buf, key, err := poll(client, requestURL, log)
  90. if err != nil {
  91. return nil, "", err
  92. } else if len(buf) > 0 {
  93. if err := putSuccess(client, requestURL); err != nil {
  94. log.Err(err).Msg("Failed to update resource success")
  95. }
  96. return buf, key, nil
  97. }
  98. }
  99. return nil, "", errors.New("Failed to fetch resource")
  100. }
  101. // poll the endpoint for the request resource, waiting for the user interaction
  102. func poll(client *http.Client, requestURL string, log *zerolog.Logger) ([]byte, string, error) {
  103. resp, err := client.Get(requestURL)
  104. if err != nil {
  105. return nil, "", err
  106. }
  107. defer resp.Body.Close()
  108. // ignore everything other than server errors as the resource
  109. // may not exist until the user does the interaction
  110. if resp.StatusCode >= 500 {
  111. return nil, "", fmt.Errorf("error on request %d", resp.StatusCode)
  112. }
  113. if resp.StatusCode != 200 {
  114. log.Info().Msg("Waiting for login...")
  115. return nil, "", nil
  116. }
  117. buf := new(bytes.Buffer)
  118. if _, err := io.Copy(buf, resp.Body); err != nil {
  119. return nil, "", err
  120. }
  121. return buf.Bytes(), resp.Header.Get("service-public-key"), nil
  122. }
  123. // putSuccess tells the server we successfully downloaded the resource
  124. func putSuccess(client *http.Client, requestURL string) error {
  125. req, err := http.NewRequest("PUT", requestURL+"/ok", nil)
  126. if err != nil {
  127. return err
  128. }
  129. resp, err := client.Do(req)
  130. if err != nil {
  131. return err
  132. }
  133. resp.Body.Close()
  134. if resp.StatusCode != 200 {
  135. return fmt.Errorf("HTTP Response Status Code: %d", resp.StatusCode)
  136. }
  137. return nil
  138. }