2020-08-07 12:29:53 +00:00
package tunnel
import (
"encoding/json"
"fmt"
"os"
"strings"
2020-10-15 20:08:57 +00:00
"github.com/google/uuid"
"github.com/pkg/errors"
2020-11-25 06:55:13 +00:00
"github.com/rs/zerolog"
2020-10-15 20:08:57 +00:00
"github.com/urfave/cli/v2"
2020-08-07 12:29:53 +00:00
"github.com/cloudflare/cloudflared/certutil"
2020-10-08 10:12:26 +00:00
"github.com/cloudflare/cloudflared/connection"
2020-08-07 12:29:53 +00:00
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tunnelstore"
)
2020-11-10 18:27:52 +00:00
type errInvalidJSONCredential struct {
err error
path string
}
func ( e errInvalidJSONCredential ) Error ( ) string {
return "Invalid JSON when parsing tunnel credentials file"
}
2020-09-01 16:06:00 +00:00
// subcommandContext carries structs shared between subcommands, to reduce number of arguments needed to
// pass between subcommands, and make sure they are only initialized once
2020-08-07 12:29:53 +00:00
type subcommandContext struct {
2020-11-23 21:36:16 +00:00
c * cli . Context
2020-11-25 06:55:13 +00:00
log * zerolog . Logger
2020-11-23 21:36:16 +00:00
isUIEnabled bool
fs fileSystem
2020-08-07 12:29:53 +00:00
// These fields should be accessed using their respective Getter
tunnelstoreClient tunnelstore . Client
userCredential * userCredential
}
func newSubcommandContext ( c * cli . Context ) ( * subcommandContext , error ) {
2020-09-01 16:06:00 +00:00
isUIEnabled := c . IsSet ( uiFlag ) && c . String ( "name" ) != ""
2020-07-29 22:48:27 +00:00
2020-09-01 16:06:00 +00:00
// If UI is enabled, terminal log output should be disabled -- log should be written into a UI log window instead
2020-11-25 06:55:13 +00:00
log := logger . CreateLoggerFromContext ( c , isUIEnabled )
2020-09-01 16:06:00 +00:00
2020-08-07 12:29:53 +00:00
return & subcommandContext {
2020-09-01 16:06:00 +00:00
c : c ,
2020-11-25 06:55:13 +00:00
log : log ,
2020-09-01 16:06:00 +00:00
isUIEnabled : isUIEnabled ,
2020-11-23 21:36:16 +00:00
fs : realFileSystem { } ,
2020-08-07 12:29:53 +00:00
} , nil
}
2020-11-23 21:36:16 +00:00
// Returns something that can find the given tunnel's credentials file.
func ( sc * subcommandContext ) credentialFinder ( tunnelID uuid . UUID ) CredFinder {
if path := sc . c . String ( CredFileFlag ) ; path != "" {
return newStaticPath ( path , sc . fs )
}
2020-11-25 06:55:13 +00:00
return newSearchByID ( tunnelID , sc . c , sc . log , sc . fs )
2020-11-23 21:36:16 +00:00
}
2020-08-07 12:29:53 +00:00
type userCredential struct {
cert * certutil . OriginCert
certPath string
}
func ( sc * subcommandContext ) client ( ) ( tunnelstore . Client , error ) {
if sc . tunnelstoreClient != nil {
return sc . tunnelstoreClient , nil
}
credential , err := sc . credential ( )
if err != nil {
return nil , err
}
2020-09-09 09:34:26 +00:00
userAgent := fmt . Sprintf ( "cloudflared/%s" , version )
2020-11-25 06:55:13 +00:00
client , err := tunnelstore . NewRESTClient (
sc . c . String ( "api-url" ) ,
credential . cert . AccountID ,
credential . cert . ZoneID ,
credential . cert . ServiceKey ,
userAgent ,
sc . log ,
)
2020-08-07 12:29:53 +00:00
if err != nil {
return nil , err
}
sc . tunnelstoreClient = client
return client , nil
}
func ( sc * subcommandContext ) credential ( ) ( * userCredential , error ) {
if sc . userCredential == nil {
2020-12-28 18:10:01 +00:00
originCertPath := sc . c . String ( "origincert" )
originCertLog := sc . log . With ( ) .
Str ( LogFieldOriginCertPath , originCertPath ) .
Logger ( )
originCertPath , err := findOriginCert ( originCertPath , & originCertLog )
2020-08-07 12:29:53 +00:00
if err != nil {
return nil , errors . Wrap ( err , "Error locating origin cert" )
}
2020-12-28 18:10:01 +00:00
blocks , err := readOriginCert ( originCertPath )
2020-08-07 12:29:53 +00:00
if err != nil {
return nil , errors . Wrapf ( err , "Can't read origin cert from %s" , originCertPath )
}
cert , err := certutil . DecodeOriginCert ( blocks )
if err != nil {
return nil , errors . Wrap ( err , "Error decoding origin cert" )
}
if cert . AccountID == "" {
return nil , errors . Errorf ( ` Origin certificate needs to be refreshed before creating new tunnels.\nDelete %s and run "cloudflared login" to obtain a new cert. ` , originCertPath )
}
sc . userCredential = & userCredential {
cert : cert ,
certPath : originCertPath ,
}
}
return sc . userCredential , nil
}
2020-11-23 21:36:16 +00:00
func ( sc * subcommandContext ) readTunnelCredentials ( credFinder CredFinder ) ( connection . Credentials , error ) {
filePath , err := credFinder . Path ( )
2020-08-07 12:29:53 +00:00
if err != nil {
2020-11-23 21:36:16 +00:00
return connection . Credentials { } , err
2020-08-07 12:29:53 +00:00
}
2020-11-23 21:36:16 +00:00
body , err := sc . fs . readFile ( filePath )
2020-08-07 12:29:53 +00:00
if err != nil {
2020-11-23 21:36:16 +00:00
return connection . Credentials { } , errors . Wrapf ( err , "couldn't read tunnel credentials from %v" , filePath )
2020-08-07 12:29:53 +00:00
}
2020-11-23 21:36:16 +00:00
var credentials connection . Credentials
if err = json . Unmarshal ( body , & credentials ) ; err != nil {
2020-11-11 18:17:14 +00:00
if strings . HasSuffix ( filePath , ".pem" ) {
2020-11-23 21:36:16 +00:00
return connection . Credentials { } , fmt . Errorf ( "The tunnel credentials file should be .json but you gave a .pem. " +
"The tunnel credentials file was originally created by `cloudflared tunnel create`. " +
"You may have accidentally used the filepath to cert.pem, which is generated by `cloudflared tunnel " +
"login`." )
2020-11-11 18:17:14 +00:00
}
2020-11-23 21:36:16 +00:00
return connection . Credentials { } , errInvalidJSONCredential { path : filePath , err : err }
2020-08-07 12:29:53 +00:00
}
2020-11-23 21:36:16 +00:00
return credentials , nil
2020-08-07 12:29:53 +00:00
}
2021-02-04 17:57:50 +00:00
func ( sc * subcommandContext ) create ( name string , credentialsOutputPath string ) ( * tunnelstore . Tunnel , error ) {
2020-08-07 12:29:53 +00:00
client , err := sc . client ( )
if err != nil {
2020-08-18 21:54:05 +00:00
return nil , errors . Wrap ( err , "couldn't create client to talk to Argo Tunnel backend" )
2020-08-07 12:29:53 +00:00
}
tunnelSecret , err := generateTunnelSecret ( )
if err != nil {
2020-08-18 21:54:05 +00:00
return nil , errors . Wrap ( err , "couldn't generate the secret for your new tunnel" )
2020-08-07 12:29:53 +00:00
}
tunnel , err := client . CreateTunnel ( name , tunnelSecret )
if err != nil {
2020-08-18 21:54:05 +00:00
return nil , errors . Wrap ( err , "Create Tunnel API call failed" )
2020-08-07 12:29:53 +00:00
}
credential , err := sc . credential ( )
if err != nil {
return nil , err
}
2020-11-23 21:36:16 +00:00
tunnelCredentials := connection . Credentials {
AccountTag : credential . cert . AccountID ,
TunnelSecret : tunnelSecret ,
TunnelID : tunnel . ID ,
TunnelName : name ,
}
2021-02-04 17:57:50 +00:00
filePath , writeFileErr := writeTunnelCredentials ( credential . certPath , credentialsOutputPath , & tunnelCredentials )
2020-11-25 06:55:13 +00:00
if writeFileErr != nil {
2020-08-07 12:29:53 +00:00
var errorLines [ ] string
errorLines = append ( errorLines , fmt . Sprintf ( "Your tunnel '%v' was created with ID %v. However, cloudflared couldn't write to the tunnel credentials file at %v.json." , tunnel . Name , tunnel . ID , tunnel . ID ) )
errorLines = append ( errorLines , fmt . Sprintf ( "The file-writing error is: %v" , writeFileErr ) )
if deleteErr := client . DeleteTunnel ( tunnel . ID ) ; deleteErr != nil {
errorLines = append ( errorLines , fmt . Sprintf ( "Cloudflared tried to delete the tunnel for you, but encountered an error. You should use `cloudflared tunnel delete %v` to delete the tunnel yourself, because the tunnel can't be run without the tunnelfile." , tunnel . ID ) )
errorLines = append ( errorLines , fmt . Sprintf ( "The delete tunnel error is: %v" , deleteErr ) )
} else {
errorLines = append ( errorLines , fmt . Sprintf ( "The tunnel was deleted, because the tunnel can't be run without the tunnelfile" ) )
}
errorMsg := strings . Join ( errorLines , "\n" )
return nil , errors . New ( errorMsg )
}
2020-11-25 06:55:13 +00:00
sc . log . Info ( ) . Msgf ( "Tunnel credentials written to %v. cloudflared chose this file based on where your origin certificate was found. Keep this file secret. To revoke these credentials, delete the tunnel." , filePath )
2020-08-07 12:29:53 +00:00
if outputFormat := sc . c . String ( outputFormatFlag . Name ) ; outputFormat != "" {
return nil , renderOutput ( outputFormat , & tunnel )
}
2020-11-25 06:55:13 +00:00
sc . log . Info ( ) . Msgf ( "Created tunnel %s with id %s" , tunnel . Name , tunnel . ID )
2020-08-07 12:29:53 +00:00
return tunnel , nil
}
func ( sc * subcommandContext ) list ( filter * tunnelstore . Filter ) ( [ ] * tunnelstore . Tunnel , error ) {
client , err := sc . client ( )
if err != nil {
return nil , err
}
return client . ListTunnels ( filter )
}
func ( sc * subcommandContext ) delete ( tunnelIDs [ ] uuid . UUID ) error {
forceFlagSet := sc . c . Bool ( "force" )
client , err := sc . client ( )
if err != nil {
return err
}
for _ , id := range tunnelIDs {
tunnel , err := client . GetTunnel ( id )
if err != nil {
return errors . Wrapf ( err , "Can't get tunnel information. Please check tunnel id: %s" , tunnel . ID )
}
// Check if tunnel DeletedAt field has already been set
if ! tunnel . DeletedAt . IsZero ( ) {
return fmt . Errorf ( "Tunnel %s has already been deleted" , tunnel . ID )
}
2021-02-11 21:22:51 +00:00
if forceFlagSet {
2020-08-07 12:29:53 +00:00
if err := client . CleanupConnections ( tunnel . ID ) ; err != nil {
return errors . Wrapf ( err , "Error cleaning up connections for tunnel %s" , tunnel . ID )
}
}
if err := client . DeleteTunnel ( tunnel . ID ) ; err != nil {
return errors . Wrapf ( err , "Error deleting tunnel %s" , tunnel . ID )
}
2020-11-23 21:36:16 +00:00
credFinder := sc . credentialFinder ( id )
2020-12-04 11:06:13 +00:00
if tunnelCredentialsPath , err := credFinder . Path ( ) ; err == nil {
if err = os . Remove ( tunnelCredentialsPath ) ; err != nil {
2020-11-25 06:55:13 +00:00
sc . log . Info ( ) . Msgf ( "Tunnel %v was deleted, but we could not remove its credentials file %s: %s. Consider deleting this file manually." , id , tunnelCredentialsPath , err )
2020-12-04 11:06:13 +00:00
}
2020-08-07 12:29:53 +00:00
}
}
return nil
}
2020-11-23 21:36:16 +00:00
// findCredentials will choose the right way to find the credentials file, find it,
// and add the TunnelID into any old credentials (generated before TUN-3581 added the `TunnelID`
// field to credentials files)
func ( sc * subcommandContext ) findCredentials ( tunnelID uuid . UUID ) ( connection . Credentials , error ) {
credFinder := sc . credentialFinder ( tunnelID )
credentials , err := sc . readTunnelCredentials ( credFinder )
// This line ensures backwards compatibility with credentials files generated before
// TUN-3581. Those old credentials files don't have a TunnelID field, so we enrich the struct
// with the ID, which we have already resolved from the user input.
credentials . TunnelID = tunnelID
return credentials , err
}
2020-08-07 12:29:53 +00:00
func ( sc * subcommandContext ) run ( tunnelID uuid . UUID ) error {
2020-11-23 21:36:16 +00:00
credentials , err := sc . findCredentials ( tunnelID )
2020-08-07 12:29:53 +00:00
if err != nil {
2020-11-10 18:27:52 +00:00
if e , ok := err . ( errInvalidJSONCredential ) ; ok {
2020-11-25 06:55:13 +00:00
sc . log . Error ( ) . Msgf ( "The credentials file at %s contained invalid JSON. This is probably caused by passing the wrong filepath. Reminder: the credentials file is a .json file created via `cloudflared tunnel create`." , e . path )
sc . log . Error ( ) . Msgf ( "Invalid JSON when parsing credentials file: %s" , e . err . Error ( ) )
2020-11-10 18:27:52 +00:00
}
2020-08-07 12:29:53 +00:00
return err
}
2020-11-25 06:55:13 +00:00
2020-09-01 16:06:00 +00:00
return StartServer (
sc . c ,
version ,
2020-11-23 21:36:16 +00:00
& connection . NamedTunnelConfig { Credentials : credentials } ,
2020-11-25 06:55:13 +00:00
sc . log ,
2020-09-01 16:06:00 +00:00
sc . isUIEnabled ,
)
2020-08-07 12:29:53 +00:00
}
func ( sc * subcommandContext ) cleanupConnections ( tunnelIDs [ ] uuid . UUID ) error {
client , err := sc . client ( )
if err != nil {
return err
}
for _ , tunnelID := range tunnelIDs {
2020-11-25 06:55:13 +00:00
sc . log . Info ( ) . Msgf ( "Cleanup connection for tunnel %s" , tunnelID )
2020-08-07 12:29:53 +00:00
if err := client . CleanupConnections ( tunnelID ) ; err != nil {
2020-11-25 06:55:13 +00:00
sc . log . Error ( ) . Msgf ( "Error cleaning up connections for tunnel %v, error :%v" , tunnelID , err )
2020-08-07 12:29:53 +00:00
}
}
return nil
}
2020-09-17 20:19:47 +00:00
func ( sc * subcommandContext ) route ( tunnelID uuid . UUID , r tunnelstore . Route ) ( tunnelstore . RouteResult , error ) {
2020-08-07 12:29:53 +00:00
client , err := sc . client ( )
if err != nil {
2020-09-17 20:19:47 +00:00
return nil , err
2020-08-07 12:29:53 +00:00
}
2020-09-17 20:19:47 +00:00
return client . RouteTunnel ( tunnelID , r )
2020-08-07 12:29:53 +00:00
}
2020-11-23 21:36:16 +00:00
// Query Tunnelstore to find the active tunnel with the given name.
2020-08-07 12:29:53 +00:00
func ( sc * subcommandContext ) tunnelActive ( name string ) ( * tunnelstore . Tunnel , bool , error ) {
filter := tunnelstore . NewFilter ( )
filter . NoDeleted ( )
filter . ByName ( name )
tunnels , err := sc . list ( filter )
if err != nil {
return nil , false , err
}
if len ( tunnels ) == 0 {
return nil , false , nil
}
// There should only be 1 active tunnel for a given name
return tunnels [ 0 ] , true , nil
}
2020-08-18 21:54:05 +00:00
// findID parses the input. If it's a UUID, return the UUID.
// Otherwise, assume it's a name, and look up the ID of that tunnel.
func ( sc * subcommandContext ) findID ( input string ) ( uuid . UUID , error ) {
if u , err := uuid . Parse ( input ) ; err == nil {
return u , nil
}
2020-11-23 21:36:16 +00:00
// Look up name in the credentials file.
credFinder := newStaticPath ( sc . c . String ( CredFileFlag ) , sc . fs )
if credentials , err := sc . readTunnelCredentials ( credFinder ) ; err == nil {
if credentials . TunnelID != uuid . Nil && input == credentials . TunnelName {
return credentials . TunnelID , nil
}
}
// Fall back to querying Tunnelstore.
2020-08-18 21:54:05 +00:00
if tunnel , found , err := sc . tunnelActive ( input ) ; err != nil {
return uuid . Nil , err
} else if found {
return tunnel . ID , nil
}
return uuid . Nil , fmt . Errorf ( "%s is neither the ID nor the name of any of your tunnels" , input )
}
// findIDs is just like mapping `findID` over a slice, but it only uses
// one Tunnelstore API call.
func ( sc * subcommandContext ) findIDs ( inputs [ ] string ) ( [ ] uuid . UUID , error ) {
2021-03-15 18:30:17 +00:00
// Shortcut without Tunnelstore call if we find that all inputs are already UUIDs.
uuids , err := convertNamesToUuids ( inputs , make ( map [ string ] uuid . UUID ) )
if err == nil {
return uuids , nil
}
2020-08-18 21:54:05 +00:00
// First, look up all tunnels the user has
filter := tunnelstore . NewFilter ( )
filter . NoDeleted ( )
tunnels , err := sc . list ( filter )
if err != nil {
return nil , err
}
// Do the pure list-processing in its own function, so that it can be
// unit tested easily.
return findIDs ( tunnels , inputs )
}
func findIDs ( tunnels [ ] * tunnelstore . Tunnel , inputs [ ] string ) ( [ ] uuid . UUID , error ) {
// Put them into a dictionary for faster lookups
nameToID := make ( map [ string ] uuid . UUID , len ( tunnels ) )
for _ , tunnel := range tunnels {
nameToID [ tunnel . Name ] = tunnel . ID
}
2021-03-15 18:30:17 +00:00
return convertNamesToUuids ( inputs , nameToID )
}
func convertNamesToUuids ( inputs [ ] string , nameToID map [ string ] uuid . UUID ) ( [ ] uuid . UUID , error ) {
2020-08-18 21:54:05 +00:00
tunnelIDs := make ( [ ] uuid . UUID , len ( inputs ) )
var badInputs [ ] string
for i , input := range inputs {
if id , err := uuid . Parse ( input ) ; err == nil {
tunnelIDs [ i ] = id
} else if id , ok := nameToID [ input ] ; ok {
tunnelIDs [ i ] = id
} else {
badInputs = append ( badInputs , input )
}
}
if len ( badInputs ) > 0 {
msg := "Please specify either the ID or name of a tunnel. The following inputs were neither: %s"
return nil , fmt . Errorf ( msg , strings . Join ( badInputs , ", " ) )
}
return tunnelIDs , nil
}