From b6d70764001d62bdd1bbc098e5b7bea1bc0b783c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Oliveirinha?= Date: Mon, 21 Feb 2022 11:49:13 +0000 Subject: [PATCH] TUN-5681: Add support for running tunnel using Token --- cfapi/client.go | 2 +- cfapi/tunnel.go | 13 ++++- cmd/cloudflared/tunnel/subcommand_context.go | 10 +++- cmd/cloudflared/tunnel/subcommands.go | 52 +++++++++++++++----- cmd/cloudflared/tunnel/subcommands_test.go | 25 ++++++++++ connection/connection.go | 15 ++++++ 6 files changed, 101 insertions(+), 16 deletions(-) diff --git a/cfapi/client.go b/cfapi/client.go index b4f17927..d1e794f7 100644 --- a/cfapi/client.go +++ b/cfapi/client.go @@ -5,7 +5,7 @@ import ( ) type TunnelClient interface { - CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error) + CreateTunnel(name string, tunnelSecret []byte) (*TunnelWithToken, error) GetTunnel(tunnelID uuid.UUID) (*Tunnel, error) DeleteTunnel(tunnelID uuid.UUID) error ListTunnels(filter *TunnelFilter) ([]*Tunnel, error) diff --git a/cfapi/tunnel.go b/cfapi/tunnel.go index 5d4ea298..cede09c9 100644 --- a/cfapi/tunnel.go +++ b/cfapi/tunnel.go @@ -23,6 +23,11 @@ type Tunnel struct { Connections []Connection `json:"connections"` } +type TunnelWithToken struct { + Tunnel + Token string `json:"token"` +} + type Connection struct { ColoName string `json:"colo_name"` ID uuid.UUID `json:"id"` @@ -63,7 +68,7 @@ func (cp CleanupParams) encode() string { return cp.queryParams.Encode() } -func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error) { +func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*TunnelWithToken, error) { if name == "" { return nil, errors.New("tunnel name required") } @@ -83,7 +88,11 @@ func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, er switch resp.StatusCode { case http.StatusOK: - return unmarshalTunnel(resp.Body) + var tunnel TunnelWithToken + if serdeErr := parseResponse(resp.Body, &tunnel); err != nil { + return nil, serdeErr + } + return &tunnel, nil case http.StatusConflict: return nil, ErrTunnelNameConflict } diff --git a/cmd/cloudflared/tunnel/subcommand_context.go b/cmd/cloudflared/tunnel/subcommand_context.go index 97784d0d..53609c3a 100644 --- a/cmd/cloudflared/tunnel/subcommand_context.go +++ b/cmd/cloudflared/tunnel/subcommand_context.go @@ -220,7 +220,9 @@ func (sc *subcommandContext) create(name string, credentialsFilePath string, sec } fmt.Println(" Keep this file secret. To revoke these credentials, delete the tunnel.") fmt.Printf("\nCreated tunnel %s with id %s\n", tunnel.Name, tunnel.ID) - return tunnel, nil + fmt.Printf("\nTunnel Token: %s\n", tunnel.Token) + + return &tunnel.Tunnel, nil } func (sc *subcommandContext) list(filter *cfapi.TunnelFilter) ([]*cfapi.Tunnel, error) { @@ -300,6 +302,12 @@ func (sc *subcommandContext) run(tunnelID uuid.UUID) error { return err } + return sc.runWithCredentials(credentials) +} + +func (sc *subcommandContext) runWithCredentials(credentials connection.Credentials) error { + sc.log.Info().Str(LogFieldTunnelID, credentials.TunnelID.String()).Msg("Starting tunnel") + return StartServer( sc.c, buildInfo, diff --git a/cmd/cloudflared/tunnel/subcommands.go b/cmd/cloudflared/tunnel/subcommands.go index 22dca2a4..8362d8f8 100644 --- a/cmd/cloudflared/tunnel/subcommands.go +++ b/cmd/cloudflared/tunnel/subcommands.go @@ -2,6 +2,7 @@ package tunnel import ( "crypto/rand" + "encoding/base64" "encoding/json" "fmt" "io/ioutil" @@ -34,6 +35,7 @@ const ( CredFileFlagAlias = "cred-file" CredFileFlag = "credentials-file" CredContentsFlag = "credentials-contents" + TunnelTokenFlag = "token" overwriteDNSFlagName = "overwrite-dns" LogFieldTunnelID = "tunnelID" @@ -118,6 +120,11 @@ var ( Usage: "Contents of the tunnel credentials JSON file to use. When provided along with credentials-file, this will take precedence.", EnvVars: []string{"TUNNEL_CRED_CONTENTS"}, }) + tunnelTokenFlag = altsrc.NewStringFlag(&cli.StringFlag{ + Name: TunnelTokenFlag, + Usage: "The Tunnel token. When provided along with credentials, this will take precedence.", + EnvVars: []string{"TUNNEL_TOKEN"}, + }) forceDeleteFlag = &cli.BoolFlag{ Name: "force", Aliases: []string{"f"}, @@ -597,6 +604,7 @@ func buildRunCommand() *cli.Command { credentialsContentsFlag, selectProtocolFlag, featuresFlag, + tunnelTokenFlag, } flags = append(flags, configureProxyFlags(false)...) return &cli.Command{ @@ -627,14 +635,6 @@ func runCommand(c *cli.Context) error { if c.NArg() > 1 { return cliutil.UsageError(`"cloudflared tunnel run" accepts only one argument, the ID or name of the tunnel to run.`) } - tunnelRef := c.Args().First() - if tunnelRef == "" { - // see if tunnel id was in the config file - tunnelRef = config.GetConfiguration().TunnelID - if tunnelRef == "" { - return cliutil.UsageError(`"cloudflared tunnel run" requires the ID or name of the tunnel to run as the last command line argument or in the configuration file.`) - } - } if c.String("hostname") != "" { sc.log.Warn().Msg("The property `hostname` in your configuration is ignored because you configured a Named Tunnel " + @@ -642,7 +642,38 @@ func runCommand(c *cli.Context) error { "your origin will not be reachable. You should remove the `hostname` property to avoid this warning.") } - return runNamedTunnel(sc, tunnelRef) + // Check if token is provided and if not use default tunnelID flag method + if tokenStr := c.String(TunnelTokenFlag); tokenStr != "" { + if token, err := parseToken(tokenStr); err == nil { + return sc.runWithCredentials(token.Credentials()) + } + + return cliutil.UsageError("Provided Tunnel token is not valid.") + } else { + tunnelRef := c.Args().First() + if tunnelRef == "" { + // see if tunnel id was in the config file + tunnelRef = config.GetConfiguration().TunnelID + if tunnelRef == "" { + return cliutil.UsageError(`"cloudflared tunnel run" requires the ID or name of the tunnel to run as the last command line argument or in the configuration file.`) + } + } + + return runNamedTunnel(sc, tunnelRef) + } +} + +func parseToken(tokenStr string) (*connection.TunnelToken, error) { + content, err := base64.StdEncoding.DecodeString(tokenStr) + if err != nil { + return nil, err + } + + var token connection.TunnelToken + if err := json.Unmarshal(content, &token); err != nil { + return nil, err + } + return &token, nil } func runNamedTunnel(sc *subcommandContext, tunnelRef string) error { @@ -650,9 +681,6 @@ func runNamedTunnel(sc *subcommandContext, tunnelRef string) error { if err != nil { return errors.Wrap(err, "error parsing tunnel ID") } - - sc.log.Info().Str(LogFieldTunnelID, tunnelID.String()).Msg("Starting tunnel") - return sc.run(tunnelID) } diff --git a/cmd/cloudflared/tunnel/subcommands_test.go b/cmd/cloudflared/tunnel/subcommands_test.go index 4ebbd922..81f542c7 100644 --- a/cmd/cloudflared/tunnel/subcommands_test.go +++ b/cmd/cloudflared/tunnel/subcommands_test.go @@ -1,14 +1,18 @@ package tunnel import ( + "encoding/base64" + "encoding/json" "path/filepath" "testing" "github.com/google/uuid" homedir "github.com/mitchellh/go-homedir" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/cloudflare/cloudflared/cfapi" + "github.com/cloudflare/cloudflared/connection" ) func Test_fmtConnections(t *testing.T) { @@ -177,3 +181,24 @@ func Test_validateHostname(t *testing.T) { }) } } + +func Test_TunnelToken(t *testing.T) { + token, err := parseToken("aabc") + require.Error(t, err) + require.Nil(t, token) + + expectedToken := &connection.TunnelToken{ + AccountTag: "abc", + TunnelSecret: []byte("secret"), + TunnelID: uuid.New(), + } + + tokenJsonStr, err := json.Marshal(expectedToken) + require.NoError(t, err) + + token64 := base64.StdEncoding.EncodeToString(tokenJsonStr) + + token, err = parseToken(token64) + require.NoError(t, err) + require.Equal(t, token, expectedToken) +} diff --git a/connection/connection.go b/connection/connection.go index b3a0804c..525c1a6e 100644 --- a/connection/connection.go +++ b/connection/connection.go @@ -50,6 +50,21 @@ func (c *Credentials) Auth() pogs.TunnelAuth { } } +// TunnelToken are Credentials but encoded with custom fields namings. +type TunnelToken struct { + AccountTag string `json:"a"` + TunnelSecret []byte `json:"s"` + TunnelID uuid.UUID `json:"t"` +} + +func (t TunnelToken) Credentials() Credentials { + return Credentials{ + AccountTag: t.AccountTag, + TunnelSecret: t.TunnelSecret, + TunnelID: t.TunnelID, + } +} + type ClassicTunnelProperties struct { Hostname string OriginCert []byte