mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-06-23 04:10:20 +00:00
da81fb02ec
Check / check (1.22.x, macos-latest) (push) Has been cancelled
Check / check (1.22.x, ubuntu-latest) (push) Has been cancelled
Check / check (1.22.x, windows-latest) (push) Has been cancelled
Semgrep config / semgrep/ci (push) Has been cancelled
Replace the lock file mechanism with PID+start-time based stale detection so that no cleanup is required on process death. When both org and app token locks were held, the first signal handler to call os.Exit() would kill the process before the second handler could delete its lock file. The orphaned lock file then caused the next invocation to wait ~128 seconds in an exponential backoff loop before forcibly deleting it. The same issue occurred on SIGKILL, OOM, or any non-signal death. Lock files now contain the holder's PID and process start time as JSON. On acquisition, if a lock file already exists, the recorded process is checked for liveness via gopsutil. Stale locks are reclaimed immediately with no backoff. Atomic O_CREATE|O_EXCL prevents races between concurrent acquirers. Also adds a companion .url file so processes waiting on an active lock can print the auth URL for the user.
167 lines
5.4 KiB
Go
167 lines
5.4 KiB
Go
package token
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
const (
|
|
baseStoreURL = "https://login.cloudflareaccess.org/"
|
|
fedStoreURL = "https://login.fed.cloudflareaccess.org/"
|
|
clientTimeout = time.Second * 60
|
|
)
|
|
|
|
// RunTransfer does the transfer "dance" with the end result downloading the supported resource.
|
|
// The expanded description is run is encapsulation of shared business logic needed
|
|
// to request a resource (token/cert/etc) from the transfer service (loginhelper).
|
|
// The "dance" we refer to is building a HTTP request, opening that in a browser waiting for
|
|
// the user to complete an action, while it long polls in the background waiting for an
|
|
// action to be completed to download the resource.
|
|
//
|
|
// If urlFilePath is non-empty, the generated auth URL is written to that path so
|
|
// other waiting processes can display it to the user. Pass "" to skip.
|
|
func RunTransfer(transferURL *url.URL, appAUD, resourceName, key, value string, shouldEncrypt bool, useHostOnly bool, autoClose bool, fedramp bool, log *zerolog.Logger, urlFilePath string) ([]byte, error) {
|
|
encrypterClient, err := NewEncrypter("cloudflared_priv.pem", "cloudflared_pub.pem")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
requestURL, err := buildRequestURL(transferURL, appAUD, key, value+encrypterClient.PublicKey(), shouldEncrypt, useHostOnly, autoClose)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// write auth URL to companion file so other waiting processes can display it
|
|
if urlFilePath != "" {
|
|
_ = os.WriteFile(urlFilePath, []byte(requestURL), 0600) // nolint: gosec
|
|
}
|
|
|
|
// See AUTH-1423 for why we use stderr (the way git wraps ssh)
|
|
err = OpenBrowser(requestURL)
|
|
if err != nil {
|
|
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)
|
|
} else {
|
|
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)
|
|
}
|
|
|
|
var resourceData []byte
|
|
|
|
storeURL := baseStoreURL
|
|
|
|
if fedramp {
|
|
storeURL = fedStoreURL
|
|
}
|
|
|
|
if shouldEncrypt {
|
|
buf, key, err := transferRequest(storeURL+"transfer/"+encrypterClient.PublicKey(), log)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
decodedBuf, err := base64.StdEncoding.DecodeString(string(buf))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
decrypted, err := encrypterClient.Decrypt(decodedBuf, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resourceData = decrypted
|
|
} else {
|
|
buf, _, err := transferRequest(storeURL+encrypterClient.PublicKey(), log)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resourceData = buf
|
|
}
|
|
|
|
return resourceData, nil
|
|
}
|
|
|
|
// BuildRequestURL creates a request suitable for a resource transfer.
|
|
// it will return a constructed url based off the base url and query key/value provided.
|
|
// cli will build a url for cli transfer request.
|
|
func buildRequestURL(baseURL *url.URL, appAUD string, key, value string, cli, useHostOnly bool, autoClose bool) (string, error) {
|
|
q := baseURL.Query()
|
|
q.Set(key, value)
|
|
q.Set("aud", appAUD)
|
|
baseURL.RawQuery = q.Encode()
|
|
if useHostOnly {
|
|
baseURL.Path = ""
|
|
}
|
|
// TODO: pass arg for tunnel login
|
|
if !cli {
|
|
return baseURL.String(), nil
|
|
}
|
|
q.Set("redirect_url", baseURL.String()) // we add the token as a query param on both the redirect_url and the main url
|
|
q.Set("send_org_token", "true") // indicates that the cli endpoint should return both the org and app token
|
|
q.Set("edge_token_transfer", "true") // use new LoginHelper service built on workers
|
|
if autoClose {
|
|
q.Set("close_interstitial", "true") // Automatically close the success window.
|
|
}
|
|
|
|
baseURL.RawQuery = q.Encode() // and this actual baseURL.
|
|
baseURL.Path = "cdn-cgi/access/cli"
|
|
return baseURL.String(), nil
|
|
}
|
|
|
|
// transferRequest downloads the requested resource from the request URL
|
|
func transferRequest(requestURL string, log *zerolog.Logger) ([]byte, string, error) {
|
|
client := &http.Client{Timeout: clientTimeout}
|
|
const pollAttempts = 10
|
|
// we do "long polling" on the endpoint to get the resource.
|
|
for i := 0; i < pollAttempts; i++ {
|
|
buf, key, err := poll(client, requestURL, log)
|
|
if err != nil {
|
|
return nil, "", err
|
|
} else if len(buf) > 0 {
|
|
return buf, key, nil
|
|
}
|
|
}
|
|
return nil, "", errors.New("Failed to fetch resource")
|
|
}
|
|
|
|
// poll the endpoint for the request resource, waiting for the user interaction
|
|
func poll(client *http.Client, requestURL string, log *zerolog.Logger) ([]byte, string, error) {
|
|
req, err := http.NewRequest(http.MethodGet, requestURL, nil)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
req.Header.Set("User-Agent", userAgent)
|
|
resp, err := client.Do(req) // nolint: gosec
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
// ignore everything other than server errors as the resource
|
|
// may not exist until the user does the interaction
|
|
if resp.StatusCode >= 500 {
|
|
buf := new(bytes.Buffer)
|
|
if _, err := io.Copy(buf, resp.Body); err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
return nil, "", fmt.Errorf("error on request %d: %s", resp.StatusCode, buf.String())
|
|
}
|
|
if resp.StatusCode != 200 {
|
|
log.Info().Msg("Waiting for login...")
|
|
return nil, "", nil
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
if _, err := io.Copy(buf, resp.Body); err != nil {
|
|
return nil, "", err
|
|
}
|
|
return buf.Bytes(), resp.Header.Get("service-public-key"), nil
|
|
}
|