594 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			594 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Go
		
	
	
	
| package access
 | |
| 
 | |
| import (
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 	"os"
 | |
| 	"os/exec"
 | |
| 	"strings"
 | |
| 	"text/template"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/getsentry/sentry-go"
 | |
| 	"github.com/pkg/errors"
 | |
| 	"github.com/rs/zerolog"
 | |
| 	"github.com/urfave/cli/v2"
 | |
| 	"golang.org/x/net/idna"
 | |
| 
 | |
| 	"github.com/cloudflare/cloudflared/carrier"
 | |
| 	"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
 | |
| 	cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
 | |
| 	"github.com/cloudflare/cloudflared/logger"
 | |
| 	"github.com/cloudflare/cloudflared/sshgen"
 | |
| 	"github.com/cloudflare/cloudflared/token"
 | |
| 	"github.com/cloudflare/cloudflared/validation"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	appURLFlag         = "app"
 | |
| 	loginQuietFlag     = "quiet"
 | |
| 	sshHostnameFlag    = "hostname"
 | |
| 	sshDestinationFlag = "destination"
 | |
| 	sshURLFlag         = "url"
 | |
| 	sshHeaderFlag      = "header"
 | |
| 	sshTokenIDFlag     = "service-token-id"
 | |
| 	sshTokenSecretFlag = "service-token-secret"
 | |
| 	sshGenCertFlag     = "short-lived-cert"
 | |
| 	sshConnectTo       = "connect-to"
 | |
| 	sshDebugStream     = "debug-stream"
 | |
| 	sshConfigTemplate  = `
 | |
| Add to your {{.Home}}/.ssh/config:
 | |
| 
 | |
| {{- if .ShortLivedCerts}}
 | |
| Match host {{.Hostname}} exec "{{.Cloudflared}} access ssh-gen --hostname %h"
 | |
|   ProxyCommand {{.Cloudflared}} access ssh --hostname %h
 | |
|   IdentityFile ~/.cloudflared/%h-cf_key
 | |
|   CertificateFile ~/.cloudflared/%h-cf_key-cert.pub
 | |
| {{- else}}
 | |
| Host {{.Hostname}}
 | |
|   ProxyCommand {{.Cloudflared}} access ssh --hostname %h
 | |
| {{end}}
 | |
| `
 | |
| )
 | |
| 
 | |
| const sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b@sentry.io/189878"
 | |
| 
 | |
| var (
 | |
| 	shutdownC chan struct{}
 | |
| 	userAgent = "DEV"
 | |
| )
 | |
| 
 | |
| // Init will initialize and store vars from the main program
 | |
| func Init(shutdown chan struct{}, version string) {
 | |
| 	shutdownC = shutdown
 | |
| 	userAgent = fmt.Sprintf("cloudflared/%s", version)
 | |
| }
 | |
| 
 | |
| // Flags return the global flags for Access related commands (hopefully none)
 | |
| func Flags() []cli.Flag {
 | |
| 	return []cli.Flag{} // no flags yet.
 | |
| }
 | |
| 
 | |
| // Commands returns all the Access related subcommands
 | |
| func Commands() []*cli.Command {
 | |
| 	return []*cli.Command{
 | |
| 		{
 | |
| 			Name:     "access",
 | |
| 			Aliases:  []string{"forward"},
 | |
| 			Category: "Access",
 | |
| 			Usage:    "access <subcommand>",
 | |
| 			Description: `Cloudflare Access protects internal resources by securing, authenticating and monitoring access
 | |
| 			per-user and by application. With Cloudflare Access, only authenticated users with the required permissions are
 | |
| 			able to reach sensitive resources. The commands provided here allow you to interact with Access protected
 | |
| 			applications from the command line.`,
 | |
| 			Subcommands: []*cli.Command{
 | |
| 				{
 | |
| 					Name:      "login",
 | |
| 					Action:    cliutil.Action(login),
 | |
| 					Usage:     "login <url of access application>",
 | |
| 					ArgsUsage: "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.
 | |
| 					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.`,
 | |
| 					Flags: []cli.Flag{
 | |
| 						&cli.BoolFlag{
 | |
| 							Name:    loginQuietFlag,
 | |
| 							Aliases: []string{"q"},
 | |
| 							Usage:   "do not print the jwt to the command line",
 | |
| 						},
 | |
| 						&cli.BoolFlag{
 | |
| 							Name:  "no-verbose",
 | |
| 							Usage: "print only the jwt to stdout",
 | |
| 						},
 | |
| 						&cli.StringFlag{
 | |
| 							Name: appURLFlag,
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 				{
 | |
| 					Name:   "curl",
 | |
| 					Action: cliutil.Action(curl),
 | |
| 					Usage:  "curl [--allow-request, -ar] <url> [<curl args>...]",
 | |
| 					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:       "allow-request will allow the curl request to continue even if the jwt is not present.",
 | |
| 					SkipFlagParsing: true,
 | |
| 				},
 | |
| 				{
 | |
| 					Name:        "token",
 | |
| 					Action:      cliutil.Action(generateToken),
 | |
| 					Usage:       "token <url of access application>",
 | |
| 					ArgsUsage:   "url of Access application",
 | |
| 					Description: `The token subcommand produces a JWT which can be used to authenticate requests.`,
 | |
| 					Flags: []cli.Flag{
 | |
| 						&cli.StringFlag{
 | |
| 							Name: appURLFlag,
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 				{
 | |
| 					Name:        "tcp",
 | |
| 					Action:      cliutil.Action(ssh),
 | |
| 					Aliases:     []string{"rdp", "ssh", "smb"},
 | |
| 					Usage:       "",
 | |
| 					ArgsUsage:   "",
 | |
| 					Description: `The tcp subcommand sends data over a proxy to the Cloudflare edge.`,
 | |
| 					Flags: []cli.Flag{
 | |
| 						&cli.StringFlag{
 | |
| 							Name:    sshHostnameFlag,
 | |
| 							Aliases: []string{"tunnel-host", "T"},
 | |
| 							Usage:   "specify the hostname of your application.",
 | |
| 							EnvVars: []string{"TUNNEL_SERVICE_HOSTNAME"},
 | |
| 						},
 | |
| 						&cli.StringFlag{
 | |
| 							Name:    sshDestinationFlag,
 | |
| 							Usage:   "specify the destination address of your SSH server.",
 | |
| 							EnvVars: []string{"TUNNEL_SERVICE_DESTINATION"},
 | |
| 						},
 | |
| 						&cli.StringFlag{
 | |
| 							Name:    sshURLFlag,
 | |
| 							Aliases: []string{"listener", "L"},
 | |
| 							Usage:   "specify the host:port to forward data to Cloudflare edge.",
 | |
| 							EnvVars: []string{"TUNNEL_SERVICE_URL"},
 | |
| 						},
 | |
| 						&cli.StringSliceFlag{
 | |
| 							Name:    sshHeaderFlag,
 | |
| 							Aliases: []string{"H"},
 | |
| 							Usage:   "specify additional headers you wish to send.",
 | |
| 						},
 | |
| 						&cli.StringFlag{
 | |
| 							Name:    sshTokenIDFlag,
 | |
| 							Aliases: []string{"id"},
 | |
| 							Usage:   "specify an Access service token ID you wish to use.",
 | |
| 							EnvVars: []string{"TUNNEL_SERVICE_TOKEN_ID"},
 | |
| 						},
 | |
| 						&cli.StringFlag{
 | |
| 							Name:    sshTokenSecretFlag,
 | |
| 							Aliases: []string{"secret"},
 | |
| 							Usage:   "specify an Access service token secret you wish to use.",
 | |
| 							EnvVars: []string{"TUNNEL_SERVICE_TOKEN_SECRET"},
 | |
| 						},
 | |
| 						&cli.StringFlag{
 | |
| 							Name:  cfdflags.LogFile,
 | |
| 							Usage: "Save application log to this file for reporting issues.",
 | |
| 						},
 | |
| 						&cli.StringFlag{
 | |
| 							Name:  cfdflags.LogDirectory,
 | |
| 							Usage: "Save application log to this directory for reporting issues.",
 | |
| 						},
 | |
| 						&cli.StringFlag{
 | |
| 							Name:    cfdflags.LogLevelSSH,
 | |
| 							Aliases: []string{"loglevel"}, //added to match the tunnel side
 | |
| 							Usage:   "Application logging level {debug, info, warn, error, fatal}. ",
 | |
| 						},
 | |
| 						&cli.StringFlag{
 | |
| 							Name:   sshConnectTo,
 | |
| 							Hidden: true,
 | |
| 							Usage:  "Connect to alternate location for testing, value is host, host:port, or sni:port:host",
 | |
| 						},
 | |
| 						&cli.Uint64Flag{
 | |
| 							Name:   sshDebugStream,
 | |
| 							Hidden: true,
 | |
| 							Usage:  "Writes up-to the max provided stream payloads to the logger as debug statements.",
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 				{
 | |
| 					Name:        "ssh-config",
 | |
| 					Action:      cliutil.Action(sshConfig),
 | |
| 					Usage:       "",
 | |
| 					Description: `Prints an example configuration ~/.ssh/config`,
 | |
| 					Flags: []cli.Flag{
 | |
| 						&cli.StringFlag{
 | |
| 							Name:  sshHostnameFlag,
 | |
| 							Usage: "specify the hostname of your application.",
 | |
| 						},
 | |
| 						&cli.BoolFlag{
 | |
| 							Name:  sshGenCertFlag,
 | |
| 							Usage: "specify if you wish to generate short lived certs.",
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 				{
 | |
| 					Name:        "ssh-gen",
 | |
| 					Action:      cliutil.Action(sshGen),
 | |
| 					Usage:       "",
 | |
| 					Description: `Generates a short lived certificate for given hostname`,
 | |
| 					Flags: []cli.Flag{
 | |
| 						&cli.StringFlag{
 | |
| 							Name:  sshHostnameFlag,
 | |
| 							Usage: "specify the hostname of your application.",
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // login pops up the browser window to do the actual login and JWT generation
 | |
| func login(c *cli.Context) error {
 | |
| 	err := sentry.Init(sentry.ClientOptions{
 | |
| 		Dsn:     sentryDSN,
 | |
| 		Release: c.App.Version,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
 | |
| 
 | |
| 	appURL, err := getAppURLFromArgs(c)
 | |
| 	if err != nil {
 | |
| 		log.Error().Msg("Please provide the url of the Access application")
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	appInfo, err := token.GetAppInfo(appURL)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if err := verifyTokenAtEdge(appURL, appInfo, c, log); err != nil {
 | |
| 		log.Err(err).Msg("Could not verify token")
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	cfdToken, err := token.GetAppTokenIfExists(appInfo)
 | |
| 	if err != nil {
 | |
| 		fmt.Fprintln(os.Stderr, "Unable to find token for provided application.")
 | |
| 		return err
 | |
| 	} else if cfdToken == "" {
 | |
| 		fmt.Fprintln(os.Stderr, "token for provided application was empty.")
 | |
| 		return errors.New("empty application token")
 | |
| 	}
 | |
| 
 | |
| 	if c.Bool(loginQuietFlag) {
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	// Chatty by default for backward compat. The new --app flag
 | |
| 	// is an implicit opt-out of the backwards-compatible chatty output.
 | |
| 	if c.Bool("no-verbose") || c.IsSet(appURLFlag) {
 | |
| 		fmt.Fprint(os.Stdout, cfdToken)
 | |
| 	} else {
 | |
| 		fmt.Fprintf(os.Stdout, "Successfully fetched your token:\n\n%s\n\n", cfdToken)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // curl provides a wrapper around curl, passing Access JWT along in request
 | |
| func curl(c *cli.Context) error {
 | |
| 	err := sentry.Init(sentry.ClientOptions{
 | |
| 		Dsn:     sentryDSN,
 | |
| 		Release: c.App.Version,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
 | |
| 
 | |
| 	args := c.Args()
 | |
| 	if args.Len() < 1 {
 | |
| 		log.Error().Msg("Please provide the access app and command you wish to run.")
 | |
| 		return errors.New("incorrect args")
 | |
| 	}
 | |
| 
 | |
| 	cmdArgs, allowRequest := parseAllowRequest(args.Slice())
 | |
| 	appURL, err := getAppURL(cmdArgs, log)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	appInfo, err := token.GetAppInfo(appURL)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Verify that the existing token is still good; if not fetch a new one
 | |
| 	if err := verifyTokenAtEdge(appURL, appInfo, c, log); err != nil {
 | |
| 		log.Err(err).Msg("Could not verify token")
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	tok, err := token.GetAppTokenIfExists(appInfo)
 | |
| 	if err != nil || tok == "" {
 | |
| 		if allowRequest {
 | |
| 			log.Info().Msg("You don't have an Access token set. Please run access token <access application> to fetch one.")
 | |
| 			return run("curl", cmdArgs...)
 | |
| 		}
 | |
| 		tok, err = token.FetchToken(appURL, appInfo, log)
 | |
| 		if err != nil {
 | |
| 			log.Err(err).Msg("Failed to refresh token")
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	cmdArgs = append(cmdArgs, "-H")
 | |
| 	cmdArgs = append(cmdArgs, fmt.Sprintf("%s: %s", carrier.CFAccessTokenHeader, tok))
 | |
| 	return run("curl", cmdArgs...)
 | |
| }
 | |
| 
 | |
| // run kicks off a shell task and pipe the results to the respective std pipes
 | |
| func run(cmd string, args ...string) error {
 | |
| 	c := exec.Command(cmd, args...)
 | |
| 	c.Stdin = os.Stdin
 | |
| 	stderr, err := c.StderrPipe()
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	go func() {
 | |
| 		_, _ = io.Copy(os.Stderr, stderr)
 | |
| 	}()
 | |
| 
 | |
| 	stdout, err := c.StdoutPipe()
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	go func() {
 | |
| 		_, _ = io.Copy(os.Stdout, stdout)
 | |
| 	}()
 | |
| 	return c.Run()
 | |
| }
 | |
| 
 | |
| func getAppURLFromArgs(c *cli.Context) (*url.URL, error) {
 | |
| 	var appURLStr string
 | |
| 	args := c.Args()
 | |
| 	if args.Len() < 1 {
 | |
| 		appURLStr = c.String(appURLFlag)
 | |
| 	} else {
 | |
| 		appURLStr = args.First()
 | |
| 	}
 | |
| 	return parseURL(appURLStr)
 | |
| }
 | |
| 
 | |
| // token dumps provided token to stdout
 | |
| func generateToken(c *cli.Context) error {
 | |
| 	err := sentry.Init(sentry.ClientOptions{
 | |
| 		Dsn:     sentryDSN,
 | |
| 		Release: c.App.Version,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	appURL, err := getAppURLFromArgs(c)
 | |
| 	if err != nil {
 | |
| 		fmt.Fprintln(os.Stderr, "Please provide a url.")
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	appInfo, err := token.GetAppInfo(appURL)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	tok, err := token.GetAppTokenIfExists(appInfo)
 | |
| 	if err != nil || tok == "" {
 | |
| 		fmt.Fprintln(os.Stderr, "Unable to find token for provided application. Please run login command to generate token.")
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if _, err := fmt.Fprint(os.Stdout, tok); err != nil {
 | |
| 		fmt.Fprintln(os.Stderr, "Failed to write token to stdout.")
 | |
| 		return err
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // sshConfig prints an example SSH config to stdout
 | |
| func sshConfig(c *cli.Context) error {
 | |
| 	genCertBool := c.Bool(sshGenCertFlag)
 | |
| 	hostname := c.String(sshHostnameFlag)
 | |
| 	if hostname == "" {
 | |
| 		hostname = "[your hostname]"
 | |
| 	}
 | |
| 
 | |
| 	type config struct {
 | |
| 		Home            string
 | |
| 		ShortLivedCerts bool
 | |
| 		Hostname        string
 | |
| 		Cloudflared     string
 | |
| 	}
 | |
| 
 | |
| 	t := template.Must(template.New("sshConfig").Parse(sshConfigTemplate))
 | |
| 	return t.Execute(os.Stdout, config{Home: os.Getenv("HOME"), ShortLivedCerts: genCertBool, Hostname: hostname, Cloudflared: cloudflaredPath()})
 | |
| }
 | |
| 
 | |
| // sshGen generates a short lived certificate for provided hostname
 | |
| func sshGen(c *cli.Context) error {
 | |
| 	log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
 | |
| 
 | |
| 	// get the hostname from the cmdline and error out if its not provided
 | |
| 	rawHostName := c.String(sshHostnameFlag)
 | |
| 	hostname, err := validation.ValidateHostname(rawHostName)
 | |
| 	if err != nil || rawHostName == "" {
 | |
| 		return cli.ShowCommandHelp(c, "ssh-gen")
 | |
| 	}
 | |
| 
 | |
| 	originURL, err := parseURL(hostname)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// this fetchToken function mutates the appURL param. We should refactor that
 | |
| 	fetchTokenURL := &url.URL{}
 | |
| 	*fetchTokenURL = *originURL
 | |
| 
 | |
| 	appInfo, err := token.GetAppInfo(fetchTokenURL)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	cfdToken, err := token.FetchTokenWithRedirect(fetchTokenURL, appInfo, log)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if err := sshgen.GenerateShortLivedCertificate(originURL, cfdToken); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // getAppURL will pull the request URL needed for fetching a user's Access token
 | |
| func getAppURL(cmdArgs []string, log *zerolog.Logger) (*url.URL, error) {
 | |
| 	if len(cmdArgs) < 1 {
 | |
| 		log.Error().Msg("Please provide a valid URL as the first argument to curl.")
 | |
| 		return nil, errors.New("not a valid url")
 | |
| 	}
 | |
| 
 | |
| 	u, err := processURL(cmdArgs[0])
 | |
| 	if err != nil {
 | |
| 		log.Error().Msg("Please provide a valid URL as the first argument to curl.")
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return u, err
 | |
| }
 | |
| 
 | |
| // parseAllowRequest will parse cmdArgs and return a copy of the args and result
 | |
| // of the allow request was present
 | |
| func parseAllowRequest(cmdArgs []string) ([]string, bool) {
 | |
| 	if len(cmdArgs) > 1 {
 | |
| 		if cmdArgs[0] == "--allow-request" || cmdArgs[0] == "-ar" {
 | |
| 			return cmdArgs[1:], true
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return cmdArgs, false
 | |
| }
 | |
| 
 | |
| // 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
 | |
| 	}
 | |
| 
 | |
| 	if u.Host == "" {
 | |
| 		return nil, errors.New("not a valid host")
 | |
| 	}
 | |
| 
 | |
| 	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
 | |
| }
 | |
| 
 | |
| // cloudflaredPath pulls the full path of cloudflared on disk
 | |
| func cloudflaredPath() string {
 | |
| 	path, err := os.Executable()
 | |
| 	if err == nil && isFileThere(path) {
 | |
| 		return path
 | |
| 	}
 | |
| 
 | |
| 	for _, p := range strings.Split(os.Getenv("PATH"), ":") {
 | |
| 		path := fmt.Sprintf("%s/%s", p, "cloudflared")
 | |
| 		if isFileThere(path) {
 | |
| 			return path
 | |
| 		}
 | |
| 	}
 | |
| 	return "cloudflared"
 | |
| }
 | |
| 
 | |
| // isFileThere will check for the presence of candidate path
 | |
| func isFileThere(candidate string) bool {
 | |
| 	fi, err := os.Stat(candidate)
 | |
| 	if err != nil || fi.IsDir() || !fi.Mode().IsRegular() {
 | |
| 		return false
 | |
| 	}
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| // verifyTokenAtEdge checks for a token on disk, or generates a new one.
 | |
| // Then makes a request to the origin with the token to ensure it is valid.
 | |
| // Returns nil if token is valid.
 | |
| func verifyTokenAtEdge(appUrl *url.URL, appInfo *token.AppInfo, c *cli.Context, log *zerolog.Logger) error {
 | |
| 	headers := parseRequestHeaders(c.StringSlice(sshHeaderFlag))
 | |
| 	if c.IsSet(sshTokenIDFlag) {
 | |
| 		headers.Add(cfAccessClientIDHeader, c.String(sshTokenIDFlag))
 | |
| 	}
 | |
| 	if c.IsSet(sshTokenSecretFlag) {
 | |
| 		headers.Add(cfAccessClientSecretHeader, c.String(sshTokenSecretFlag))
 | |
| 	}
 | |
| 	options := &carrier.StartOptions{AppInfo: appInfo, OriginURL: appUrl.String(), Headers: headers}
 | |
| 
 | |
| 	if valid, err := isTokenValid(options, log); err != nil {
 | |
| 		return err
 | |
| 	} else if valid {
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	if err := token.RemoveTokenIfExists(appInfo); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if valid, err := isTokenValid(options, log); err != nil {
 | |
| 		return err
 | |
| 	} else if !valid {
 | |
| 		return errors.New("failed to verify token")
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // isTokenValid makes a request to the origin and returns true if the response was not a 302.
 | |
| func isTokenValid(options *carrier.StartOptions, log *zerolog.Logger) (bool, error) {
 | |
| 	req, err := carrier.BuildAccessRequest(options, log)
 | |
| 	if err != nil {
 | |
| 		return false, errors.Wrap(err, "Could not create access request")
 | |
| 	}
 | |
| 	req.Header.Set("User-Agent", userAgent)
 | |
| 
 | |
| 	query := req.URL.Query()
 | |
| 	query.Set("cloudflared_token_check", "true")
 | |
| 	req.URL.RawQuery = query.Encode()
 | |
| 
 | |
| 	// Do not follow redirects
 | |
| 	client := &http.Client{
 | |
| 		CheckRedirect: func(req *http.Request, via []*http.Request) error {
 | |
| 			return http.ErrUseLastResponse
 | |
| 		},
 | |
| 		Timeout: time.Second * 5,
 | |
| 	}
 | |
| 	resp, err := client.Do(req)
 | |
| 	if err != nil {
 | |
| 		return false, err
 | |
| 	}
 | |
| 	defer resp.Body.Close()
 | |
| 
 | |
| 	// A redirect to login means the token was invalid.
 | |
| 	return !carrier.IsAccessResponse(resp), nil
 | |
| }
 |