From 170f0acf4f4006d17a03450cfe00bd2dd9c0e0fd Mon Sep 17 00:00:00 2001 From: Austin Cherry Date: Tue, 18 Sep 2018 16:21:27 -0500 Subject: [PATCH] AUTH-1136: addressing beta feedback --- Gopkg.lock | 2 +- cmd/cloudflared/access/cmd.go | 104 ++++++++++++++++++--------- cmd/cloudflared/access/token.go | 16 +++-- cmd/cloudflared/encrypter/encrypt.go | 8 +-- cmd/cloudflared/main.go | 12 ++++ cmd/cloudflared/transfer/transfer.go | 28 ++++---- cmd/cloudflared/tunnel/cmd.go | 2 +- cmd/cloudflared/tunnel/login.go | 11 +-- 8 files changed, 119 insertions(+), 64 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index cbcc7445..fa63d8f3 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -461,6 +461,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "4270289ba5f418a18a65373d2914d463a52aabd200eae3d8898914b027c59803" + inputs-digest = "0907565b36c43c6a1c461648f1e69d0367864b61f112e01948f7b008e44598e5" solver-name = "gps-cdcl" solver-version = 1 diff --git a/cmd/cloudflared/access/cmd.go b/cmd/cloudflared/access/cmd.go index 241d97e0..978077a3 100644 --- a/cmd/cloudflared/access/cmd.go +++ b/cmd/cloudflared/access/cmd.go @@ -7,13 +7,14 @@ import ( "os" "github.com/cloudflare/cloudflared/cmd/cloudflared/shell" + "golang.org/x/net/idna" "github.com/cloudflare/cloudflared/log" raven "github.com/getsentry/raven-go" cli "gopkg.in/urfave/cli.v2" ) -const sentryDSN = "https://5a81ca98270b4aee89d4d9913b259fec:583d2c118b384712aa8b91afbdabde81@sentry.cfops.it/170" // we probably need a public accessable url. +const sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b@sentry.io/189878" // Flags return the global flags for Access related commands (hopefully none) func Flags() []cli.Flag { @@ -37,7 +38,7 @@ func Commands() []*cli.Command { Action: login, Usage: "login ", Description: `The login subcommand initiates an authentication flow with your identity provider. - The subcommand will launch a browser. For headless systems, a URL is provided. + The subcommand will launch a browser. For headless systems, a url is provided. Once authenticated with your identity provider, the login command will generate a JSON Web Token (JWT) scoped to your identity, the application you intend to reach, and valid for a session duration set by your administrator. cloudflared stores the token in local storage.`, @@ -52,15 +53,10 @@ func Commands() []*cli.Command { Name: "curl", Action: curl, Usage: "curl ", - Description: `The curl subcommand wraps curl and automatically injects the JWT into a cf-jwt-access-assertion + Description: `The curl subcommand wraps curl and automatically injects the JWT into a cf-access-token header when using curl to reach an application behind Access.`, - ArgsUsage: "nojwt will allow the curl request to continue even if the jwt is not present.", - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "allow-request", - Aliases: []string{"ar"}, - }, - }, + ArgsUsage: "allow-request will allow the curl request to continue even if the jwt is not present.", + SkipFlagParsing: true, }, { Name: "token", @@ -86,10 +82,10 @@ func login(c *cli.Context) error { args := c.Args() appURL, err := url.Parse(args.First()) if args.Len() < 1 || err != nil { - logger.Errorf("Please provide the URL of the Access application\n") + logger.Errorf("Please provide the url of the Access application\n") return err } - if err := fetchToken(c, appURL); err != nil { + if _, err := fetchToken(c, appURL); err != nil { logger.Errorf("Failed to fetch token: %s\n", err) return err } @@ -107,36 +103,26 @@ func curl(c *cli.Context) error { return errors.New("incorrect args") } - var appURL *url.URL - cmdArgs := args.Slice() - for _, arg := range cmdArgs { - u, err := url.ParseRequestURI(arg) - if err != nil { - continue - } - appURL = u - break + cmdArgs, appURL, allowRequest, err := buildCurlCmdArgs(args.Slice()) + if err != nil { + return err } token, err := getTokenIfExists(appURL) if err != nil || token == "" { - if !c.Bool("nojwt") { - if err := fetchToken(c, appURL); err != nil { - logger.Errorf("Failed to refresh token: %s\n", err) - return err - } - token, err = getTokenIfExists(appURL) - if err != nil { - logger.Errorf("Failed pull existing token: %s\n", err) - return err - } - } else { + if allowRequest { logger.Warn("You don't have an Access token set. Please run access token to fetch one.") + return shell.Run("curl", cmdArgs...) + } + token, err = fetchToken(c, appURL) + if err != nil { + logger.Error("Failed to refresh token: ", err) + return err } } cmdArgs = append(cmdArgs, "-H") - cmdArgs = append(cmdArgs, fmt.Sprintf("cf-jwt-access-assertion=%s", token)) + cmdArgs = append(cmdArgs, fmt.Sprintf("cf-access-token: %s", token)) return shell.Run("curl", cmdArgs...) } @@ -145,7 +131,7 @@ func token(c *cli.Context) error { raven.SetDSN(sentryDSN) appURL, err := url.Parse(c.String("app")) if err != nil || c.NumFlags() < 1 { - fmt.Fprintln(os.Stderr, "Please provide access application.") + fmt.Fprintln(os.Stderr, "Please provide a url.") return err } token, err := getTokenIfExists(appURL) @@ -160,3 +146,53 @@ func token(c *cli.Context) error { } return nil } + +// processURL will preprocess the string (parse to a url, convert to punycode, etc). +func processURL(s string) (*url.URL, error) { + u, err := url.ParseRequestURI(s) + if err != nil { + return nil, err + } + host, err := idna.ToASCII(u.Hostname()) + if err != nil { // we fail to convert to punycode, just return the url we parsed. + return u, nil + } + if u.Port() != "" { + u.Host = fmt.Sprintf("%s:%s", host, u.Port()) + } else { + u.Host = host + } + + return u, nil +} + +// buildCurlCmdArgs will build the curl cmd args +func buildCurlCmdArgs(cmdArgs []string) ([]string, *url.URL, bool, error) { + allowRequest, iAllowRequest := false, 0 + var appURL *url.URL + for i, arg := range cmdArgs { + if arg == "-allow-request" || arg == "-ar" { + iAllowRequest = i + allowRequest = true + } + + u, err := processURL(arg) + if err == nil { + appURL = u + cmdArgs[i] = appURL.String() + } + } + + if appURL == nil { + logger.Error("Please provide a valid URL.") + return cmdArgs, appURL, allowRequest, errors.New("invalid url") + } + + if allowRequest { + // remove from cmdArgs + cmdArgs[iAllowRequest] = cmdArgs[len(cmdArgs)-1] + cmdArgs = cmdArgs[:len(cmdArgs)-1] + } + + return cmdArgs, appURL, allowRequest, nil +} diff --git a/cmd/cloudflared/access/token.go b/cmd/cloudflared/access/token.go index 85677dd8..943fe402 100644 --- a/cmd/cloudflared/access/token.go +++ b/cmd/cloudflared/access/token.go @@ -21,24 +21,28 @@ import ( var logger = log.CreateLogger() // fetchToken will either load a stored token or generate a new one -func fetchToken(c *cli.Context, appURL *url.URL) error { +func fetchToken(c *cli.Context, appURL *url.URL) (string, error) { if token, err := getTokenIfExists(appURL); token != "" && err == nil { fmt.Fprintf(os.Stdout, "You have an existing token:\n\n%s\n\n", token) - return nil + return token, nil } path, err := generateFilePathForTokenURL(appURL) if err != nil { - return err + return "", err } - token, err := transfer.Run(c, appURL, "token", path, true) + // this weird parameter is the resource name (token) and the key/value + // we want to send to the transfer service. the key is token and the value + // is blank (basically just the id generated in the transfer service) + const resourceName, key, value = "token", "token", "" + token, err := transfer.Run(c, appURL, resourceName, key, value, path, true) if err != nil { - return err + return "", err } fmt.Fprintf(os.Stdout, "Successfully fetched your token:\n\n%s\n\n", string(token)) - return nil + return string(token), nil } // getTokenIfExists will return the token from local storage if it exists diff --git a/cmd/cloudflared/encrypter/encrypt.go b/cmd/cloudflared/encrypter/encrypt.go index 938bf32c..a9c4725b 100644 --- a/cmd/cloudflared/encrypter/encrypt.go +++ b/cmd/cloudflared/encrypter/encrypt.go @@ -47,7 +47,7 @@ type Encrypter struct { // New returns a new encrypter with initialized keypair func New(privateKey, publicKey string) (*Encrypter, error) { e := &Encrypter{} - key, pubKey, err := e.fetchOrGenerateKeys(privateKey, publicKey) + pubKey, key, err := e.fetchOrGenerateKeys(privateKey, publicKey) if err != nil { return nil, err } @@ -57,7 +57,7 @@ func New(privateKey, publicKey string) (*Encrypter, error) { // PublicKey returns a base64 encoded public key. Useful for transport (like in HTTP requests) func (e *Encrypter) PublicKey() string { - return base64.StdEncoding.EncodeToString(e.publicKey[:]) + return base64.URLEncoding.EncodeToString(e.publicKey[:]) } // Decrypt data that was encrypted using our publicKey. It will use our privateKey and the sender's publicKey to decrypt @@ -122,7 +122,7 @@ func (e *Encrypter) fetchOrGenerateKeys(privateKey, publicKey string) (*[32]byte } else if err != nil { return nil, nil, err } - return key, pub, nil + return pub, key, nil } // writeKey will write a key to disk in DER format (it's a standard pem key) @@ -166,7 +166,7 @@ func (e *Encrypter) fetchKey(filename string) (*[32]byte, error) { // decodePublicKey will base64 decode the provided key to the box representation func (e *Encrypter) decodePublicKey(key string) (*[32]byte, error) { - pub, err := base64.StdEncoding.DecodeString(key) + pub, err := base64.URLEncoding.DecodeString(key) if err != nil { return nil, err } diff --git a/cmd/cloudflared/main.go b/cmd/cloudflared/main.go index b6e4d8f5..0fc33848 100644 --- a/cmd/cloudflared/main.go +++ b/cmd/cloudflared/main.go @@ -2,8 +2,11 @@ package main import ( "fmt" + "os" "time" + "golang.org/x/crypto/ssh/terminal" + "github.com/cloudflare/cloudflared/cmd/cloudflared/access" "github.com/cloudflare/cloudflared/cmd/cloudflared/config" "github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel" @@ -84,6 +87,11 @@ func flags() []cli.Flag { func action(version string, shutdownC, graceShutdownC chan struct{}) cli.ActionFunc { return func(c *cli.Context) (err error) { + if isRunningFromTerminal() { + logger.Error("Use of cloudflared without commands is deprecated.") + cli.ShowAppHelp(c) + return nil + } tags := make(map[string]string) tags["hostname"] = c.String("hostname") raven.SetTagsContext(tags) @@ -125,3 +133,7 @@ func userHomeDir() (string, error) { } return homeDir, nil } + +func isRunningFromTerminal() bool { + return terminal.IsTerminal(int(os.Stdout.Fd())) +} diff --git a/cmd/cloudflared/transfer/transfer.go b/cmd/cloudflared/transfer/transfer.go index 90e15c27..0b786189 100644 --- a/cmd/cloudflared/transfer/transfer.go +++ b/cmd/cloudflared/transfer/transfer.go @@ -20,7 +20,7 @@ import ( ) const ( - baseStoreURL = "https://login.cloudflarewarp.com" + baseStoreURL = "https://login.cloudflarewarp.com/" clientTimeout = time.Second * 60 ) @@ -32,12 +32,12 @@ var logger = log.CreateLogger() // 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. -func Run(c *cli.Context, transferURL *url.URL, resourceName, path string, shouldEncrypt bool) ([]byte, error) { +func Run(c *cli.Context, transferURL *url.URL, resourceName, key, value, path string, shouldEncrypt bool) ([]byte, error) { encrypterClient, err := encrypter.New("cloudflared_priv.pem", "cloudflared_pub.pem") if err != nil { return nil, err } - requestURL, err := buildRequestURL(transferURL, resourceName, encrypterClient.PublicKey(), true) + requestURL, err := buildRequestURL(transferURL, key, value+encrypterClient.PublicKey(), shouldEncrypt) if err != nil { return nil, err } @@ -58,19 +58,23 @@ func Run(c *cli.Context, transferURL *url.URL, resourceName, path string, should var resourceData []byte if shouldEncrypt { - buf, key, err := transferRequest(filepath.Join(baseURL, "transfer", encrypterClient.PublicKey())) + buf, key, err := transferRequest(baseURL + filepath.Join("transfer", encrypterClient.PublicKey())) if err != nil { return nil, err } - decrypted, err := encrypterClient.Decrypt(buf, key) + 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(filepath.Join(baseURL, encrypterClient.PublicKey())) + buf, _, err := transferRequest(baseURL + filepath.Join(encrypterClient.PublicKey())) if err != nil { return nil, err } @@ -84,7 +88,7 @@ func Run(c *cli.Context, transferURL *url.URL, resourceName, path string, should return resourceData, nil } -// buildRequestURL creates a request suitable for a resource transfer. +// 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. // follow will follow redirects. func buildRequestURL(baseURL *url.URL, key, value string, follow bool) (string, error) { @@ -106,8 +110,9 @@ func buildRequestURL(baseURL *url.URL, key, value string, follow bool) (string, // transferRequest downloads the requested resource from the request URL func transferRequest(requestURL string) ([]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 < 20; i++ { + for i := 0; i < pollAttempts; i++ { buf, key, err := poll(client, requestURL) if err != nil { return nil, "", err @@ -143,12 +148,7 @@ func poll(client *http.Client, requestURL string) ([]byte, string, error) { if _, err := io.Copy(buf, resp.Body); err != nil { return nil, "", err } - - decodedBuf, err := base64.StdEncoding.DecodeString(string(buf.Bytes())) - if err != nil { - return nil, "", err - } - return decodedBuf, resp.Header.Get("service-public-key"), nil + return buf.Bytes(), resp.Header.Get("service-public-key"), nil } // putSuccess tells the server we successfully downloaded the resource diff --git a/cmd/cloudflared/tunnel/cmd.go b/cmd/cloudflared/tunnel/cmd.go index a25964d2..042ed503 100644 --- a/cmd/cloudflared/tunnel/cmd.go +++ b/cmd/cloudflared/tunnel/cmd.go @@ -98,7 +98,7 @@ func Commands() []*cli.Command { }, }, ArgsUsage: " ", // can't be the empty string or we get the default output - Hidden: true, + Hidden: false, }, { Name: "db", diff --git a/cmd/cloudflared/tunnel/login.go b/cmd/cloudflared/tunnel/login.go index f2fcc3ca..f3e96f78 100644 --- a/cmd/cloudflared/tunnel/login.go +++ b/cmd/cloudflared/tunnel/login.go @@ -13,12 +13,15 @@ import ( cli "gopkg.in/urfave/cli.v2" ) -const baseLoginURL = "https://dash.cloudflare.com/argotunnel" +const ( + baseLoginURL = "https://dash.cloudflare.com/argotunnel" + callbackStoreURL = "https://login.cloudflarewarp.com/" +) func login(c *cli.Context) error { path, ok, err := checkForExistingCert() if ok { - fmt.Fprintf(os.Stdout, "You have an existing certificate at %s which login would overwrite.\nIf this is intentional, please move or delete that file then run this command again.", path) + fmt.Fprintf(os.Stdout, "You have an existing certificate at %s which login would overwrite.\nIf this is intentional, please move or delete that file then run this command again.\n", path) return nil } else if err != nil { return err @@ -30,9 +33,9 @@ func login(c *cli.Context) error { return err } - _, err = transfer.Run(c, loginURL, "cert", path, false) + _, err = transfer.Run(c, loginURL, "cert", "callback", callbackStoreURL, path, false) if err != nil { - fmt.Fprintf(os.Stderr, "Failed to write the certificate due to the following error:\n%v\n\nYour browser will download the certificate instead. You will have to manually\ncopy it to the following path:\n\n%s", err, path) + fmt.Fprintf(os.Stderr, "Failed to write the certificate due to the following error:\n%v\n\nYour browser will download the certificate instead. You will have to manually\ncopy it to the following path:\n\n%s\n", err, path) return err }