AUTH-1136: addressing beta feedback

This commit is contained in:
Austin Cherry 2018-09-18 16:21:27 -05:00 committed by Areg Harutyunyan
parent 674eb33edc
commit 170f0acf4f
8 changed files with 119 additions and 64 deletions

2
Gopkg.lock generated
View File

@ -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

View File

@ -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 <url of access application>",
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 <args>",
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 <access application> 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
}

View File

@ -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

View File

@ -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
}

View File

@ -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()))
}

View File

@ -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

View File

@ -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",

View File

@ -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
}