2021-03-08 16:46:23 +00:00
package token
2018-10-08 19:20:28 +00:00
import (
"bytes"
"encoding/base64"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
2020-11-09 03:25:35 +00:00
"github.com/pkg/errors"
2020-11-25 06:55:13 +00:00
"github.com/rs/zerolog"
2018-10-08 19:20:28 +00:00
)
const (
2019-09-04 18:21:45 +00:00
baseStoreURL = "https://login.argotunnel.com/"
2018-10-08 19:20:28 +00:00
clientTimeout = time . Second * 60
)
2021-03-08 16:46:23 +00:00
// RunTransfer does the transfer "dance" with the end result downloading the supported resource.
2018-10-08 19:20:28 +00:00
// 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.
2021-03-08 16:46:23 +00:00
func RunTransfer ( transferURL * url . URL , resourceName , key , value string , shouldEncrypt bool , useHostOnly bool , log * zerolog . Logger ) ( [ ] byte , error ) {
encrypterClient , err := NewEncrypter ( "cloudflared_priv.pem" , "cloudflared_pub.pem" )
2018-10-08 19:20:28 +00:00
if err != nil {
return nil , err
}
2020-07-17 16:08:05 +00:00
requestURL , err := buildRequestURL ( transferURL , key , value + encrypterClient . PublicKey ( ) , shouldEncrypt , useHostOnly )
2018-10-08 19:20:28 +00:00
if err != nil {
return nil , err
}
2019-01-24 20:48:37 +00:00
// See AUTH-1423 for why we use stderr (the way git wraps ssh)
2021-03-08 16:46:23 +00:00
err = OpenBrowser ( requestURL )
2018-10-08 19:20:28 +00:00
if err != nil {
2019-01-24 20:48:37 +00:00
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 )
2018-10-08 19:20:28 +00:00
} else {
2019-10-22 15:41:44 +00:00
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 )
2018-10-08 19:20:28 +00:00
}
var resourceData [ ] byte
if shouldEncrypt {
2020-11-25 06:55:13 +00:00
buf , key , err := transferRequest ( baseStoreURL + "transfer/" + encrypterClient . PublicKey ( ) , log )
2018-10-08 19:20:28 +00:00
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 {
2020-11-25 06:55:13 +00:00
buf , _ , err := transferRequest ( baseStoreURL + encrypterClient . PublicKey ( ) , log )
2018-10-08 19:20:28 +00:00
if err != nil {
return nil , err
}
resourceData = buf
}
return resourceData , nil
2020-11-09 03:25:35 +00:00
2018-10-08 19:20:28 +00:00
}
// 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.
2018-11-13 20:26:20 +00:00
// cli will build a url for cli transfer request.
2020-07-17 16:08:05 +00:00
func buildRequestURL ( baseURL * url . URL , key , value string , cli , useHostOnly bool ) ( string , error ) {
2018-10-08 19:20:28 +00:00
q := baseURL . Query ( )
q . Set ( key , value )
baseURL . RawQuery = q . Encode ( )
2020-07-17 16:08:05 +00:00
if useHostOnly {
baseURL . Path = ""
}
2018-11-13 20:26:20 +00:00
if ! cli {
2018-10-08 19:20:28 +00:00
return baseURL . String ( ) , nil
}
2020-07-17 16:08:05 +00:00
q . Set ( "redirect_url" , baseURL . String ( ) ) // we add the token as a query param on both the redirect_url and the main url
2020-11-09 03:25:35 +00:00
q . Set ( "send_org_token" , "true" ) // indicates that the cli endpoint should return both the org and app token
2020-07-17 16:08:05 +00:00
baseURL . RawQuery = q . Encode ( ) // and this actual baseURL.
2018-11-13 20:26:20 +00:00
baseURL . Path = "cdn-cgi/access/cli"
return baseURL . String ( ) , nil
2018-10-08 19:20:28 +00:00
}
// transferRequest downloads the requested resource from the request URL
2020-11-25 06:55:13 +00:00
func transferRequest ( requestURL string , log * zerolog . Logger ) ( [ ] byte , string , error ) {
2018-10-08 19:20:28 +00:00
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 ++ {
2020-11-25 06:55:13 +00:00
buf , key , err := poll ( client , requestURL , log )
2018-10-08 19:20:28 +00:00
if err != nil {
return nil , "" , err
} else if len ( buf ) > 0 {
if err := putSuccess ( client , requestURL ) ; err != nil {
2020-12-28 18:10:01 +00:00
log . Err ( err ) . Msg ( "Failed to update resource success" )
2018-10-08 19:20:28 +00:00
}
return buf , key , nil
}
}
return nil , "" , errors . New ( "Failed to fetch resource" )
}
// poll the endpoint for the request resource, waiting for the user interaction
2020-11-25 06:55:13 +00:00
func poll ( client * http . Client , requestURL string , log * zerolog . Logger ) ( [ ] byte , string , error ) {
2018-10-08 19:20:28 +00:00
resp , err := client . Get ( requestURL )
if err != nil {
return nil , "" , err
}
defer 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 {
return nil , "" , fmt . Errorf ( "error on request %d" , resp . StatusCode )
}
if resp . StatusCode != 200 {
2020-11-25 06:55:13 +00:00
log . Info ( ) . Msg ( "Waiting for login..." )
2018-10-08 19:20:28 +00:00
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
}
// putSuccess tells the server we successfully downloaded the resource
func putSuccess ( client * http . Client , requestURL string ) error {
req , err := http . NewRequest ( "PUT" , requestURL + "/ok" , nil )
if err != nil {
return err
}
resp , err := client . Do ( req )
if err != nil {
return err
}
resp . Body . Close ( )
if resp . StatusCode != 200 {
return fmt . Errorf ( "HTTP Response Status Code: %d" , resp . StatusCode )
}
return nil
}