2018-10-08 19:20:28 +00:00
package transfer
import (
"bytes"
"encoding/base64"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
"github.com/cloudflare/cloudflared/cmd/cloudflared/encrypter"
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
2020-04-29 20:51:32 +00:00
"github.com/cloudflare/cloudflared/logger"
2020-11-09 03:25:35 +00:00
"github.com/pkg/errors"
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
)
// Run 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.
2020-11-09 03:25:35 +00:00
func Run ( transferURL * url . URL , resourceName , key , value string , shouldEncrypt bool , useHostOnly bool , logger logger . Service ) ( [ ] byte , error ) {
2018-10-08 19:20:28 +00:00
encrypterClient , err := encrypter . New ( "cloudflared_priv.pem" , "cloudflared_pub.pem" )
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)
2018-10-08 19:20:28 +00:00
err = shell . OpenBrowser ( requestURL )
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-04-29 20:51:32 +00:00
buf , key , err := transferRequest ( baseStoreURL + "transfer/" + encrypterClient . PublicKey ( ) , logger )
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-04-29 20:51:32 +00:00
buf , _ , err := transferRequest ( baseStoreURL + encrypterClient . PublicKey ( ) , logger )
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-04-29 20:51:32 +00:00
func transferRequest ( requestURL string , logger logger . Service ) ( [ ] 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-04-29 20:51:32 +00:00
buf , key , err := poll ( client , requestURL , logger )
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-04-29 20:51:32 +00:00
logger . Errorf ( "Failed to update resource success: %s" , err )
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-04-29 20:51:32 +00:00
func poll ( client * http . Client , requestURL string , logger logger . Service ) ( [ ] 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 {
logger . Info ( "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
}
// 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
}