2020-05-21 20:36:49 +00:00
package tunnel
import (
2020-06-15 18:33:41 +00:00
"crypto/rand"
2020-05-21 20:36:49 +00:00
"encoding/json"
"fmt"
2020-06-16 22:43:22 +00:00
"io/ioutil"
2020-05-21 20:36:49 +00:00
"os"
2020-06-15 18:33:41 +00:00
"path/filepath"
2020-06-11 23:44:39 +00:00
"sort"
"strings"
2020-05-21 20:36:49 +00:00
"time"
2020-06-25 18:25:39 +00:00
"github.com/google/uuid"
2020-07-02 09:31:12 +00:00
"github.com/mitchellh/go-homedir"
2020-05-21 20:36:49 +00:00
"github.com/pkg/errors"
"gopkg.in/urfave/cli.v2"
"gopkg.in/yaml.v2"
"github.com/cloudflare/cloudflared/certutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
2020-07-02 09:31:12 +00:00
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
2020-04-29 20:51:32 +00:00
"github.com/cloudflare/cloudflared/logger"
2020-06-17 18:33:55 +00:00
"github.com/cloudflare/cloudflared/origin"
2020-06-15 18:33:41 +00:00
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
2020-05-21 20:36:49 +00:00
"github.com/cloudflare/cloudflared/tunnelstore"
)
2020-07-02 09:31:12 +00:00
const (
credFileFlagAlias = "cred-file"
)
2020-05-21 20:36:49 +00:00
var (
2020-06-16 22:55:33 +00:00
showDeletedFlag = & cli . BoolFlag {
Name : "show-deleted" ,
Aliases : [ ] string { "d" } ,
Usage : "Include deleted tunnels in the list" ,
}
2020-05-21 20:36:49 +00:00
outputFormatFlag = & cli . StringFlag {
2020-04-29 20:51:32 +00:00
Name : "output" ,
2020-05-21 20:36:49 +00:00
Aliases : [ ] string { "o" } ,
2020-04-29 20:51:32 +00:00
Usage : "Render output using given `FORMAT`. Valid options are 'json' or 'yaml'" ,
2020-05-21 20:36:49 +00:00
}
2020-06-25 18:25:39 +00:00
forceFlag = & cli . BoolFlag {
2020-06-16 22:43:22 +00:00
Name : "force" ,
Aliases : [ ] string { "f" } ,
Usage : "By default, if a tunnel is currently being run from a cloudflared, you can't " +
"simultaneously rerun it again from a second cloudflared. The --force flag lets you " +
"overwrite the previous tunnel. If you want to use a single hostname with multiple " +
"tunnels, you can do so with Cloudflare's Load Balancer product." ,
}
2020-07-02 09:31:12 +00:00
credentialsFileFlag = & cli . StringFlag {
Name : "credentials-file" ,
Aliases : [ ] string { credFileFlagAlias } ,
Usage : "File path of tunnel credentials" ,
}
2020-05-21 20:36:49 +00:00
)
const hideSubcommands = true
func buildCreateCommand ( ) * cli . Command {
return & cli . Command {
Name : "create" ,
Action : cliutil . ErrorHandler ( createTunnel ) ,
Usage : "Create a new tunnel with given name" ,
ArgsUsage : "TUNNEL-NAME" ,
Hidden : hideSubcommands ,
Flags : [ ] cli . Flag { outputFormatFlag } ,
}
}
2020-06-15 18:33:41 +00:00
// generateTunnelSecret as an array of 32 bytes using secure random number generator
func generateTunnelSecret ( ) ( [ ] byte , error ) {
randomBytes := make ( [ ] byte , 32 )
_ , err := rand . Read ( randomBytes )
return randomBytes , err
}
2020-05-21 20:36:49 +00:00
func createTunnel ( c * cli . Context ) error {
if c . NArg ( ) != 1 {
return cliutil . UsageError ( ` "cloudflared tunnel create" requires exactly 1 argument, the name of tunnel to create. ` )
}
name := c . Args ( ) . First ( )
2020-04-29 20:51:32 +00:00
logger , err := logger . New ( )
if err != nil {
return errors . Wrap ( err , "error setting up logger" )
}
2020-06-15 18:33:41 +00:00
tunnelSecret , err := generateTunnelSecret ( )
if err != nil {
return err
}
originCertPath , err := findOriginCert ( c , logger )
if err != nil {
return errors . Wrap ( err , "Error locating origin cert" )
}
cert , err := getOriginCertFromContext ( originCertPath , logger )
2020-05-21 20:36:49 +00:00
if err != nil {
return err
}
2020-06-15 18:33:41 +00:00
client := newTunnelstoreClient ( c , cert , logger )
2020-05-21 20:36:49 +00:00
2020-06-15 18:33:41 +00:00
tunnel , err := client . CreateTunnel ( name , tunnelSecret )
2020-05-21 20:36:49 +00:00
if err != nil {
return errors . Wrap ( err , "Error creating a new tunnel" )
}
2020-06-15 18:33:41 +00:00
if writeFileErr := writeTunnelCredentials ( tunnel . ID , cert . AccountID , originCertPath , tunnelSecret , logger ) ; err != nil {
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 errors . New ( errorMsg )
}
2020-05-21 20:36:49 +00:00
if outputFormat := c . String ( outputFormatFlag . Name ) ; outputFormat != "" {
return renderOutput ( outputFormat , & tunnel )
}
logger . Infof ( "Created tunnel %s with id %s" , tunnel . Name , tunnel . ID )
return nil
}
2020-07-02 09:31:12 +00:00
func tunnelFilePath ( tunnelID , directory string ) ( string , error ) {
2020-06-15 18:33:41 +00:00
fileName := fmt . Sprintf ( "%v.json" , tunnelID )
2020-07-02 09:31:12 +00:00
filePath := filepath . Clean ( fmt . Sprintf ( "%s/%s" , directory , fileName ) )
return homedir . Expand ( filePath )
2020-06-15 18:33:41 +00:00
}
func writeTunnelCredentials ( tunnelID , accountID , originCertPath string , tunnelSecret [ ] byte , logger logger . Service ) error {
2020-07-02 09:31:12 +00:00
originCertDir := filepath . Dir ( originCertPath )
filePath , err := tunnelFilePath ( tunnelID , originCertDir )
2020-06-15 18:33:41 +00:00
if err != nil {
return err
}
body , err := json . Marshal ( pogs . TunnelAuth {
AccountTag : accountID ,
TunnelSecret : tunnelSecret ,
} )
if err != nil {
return errors . Wrap ( err , "Unable to marshal tunnel credentials to JSON" )
}
2020-06-16 22:43:22 +00:00
logger . Infof ( "Writing tunnel credentials to %v. cloudflared chose this file based on where your origin certificate was found." , filePath )
logger . Infof ( "Keep this file secret. To revoke these credentials, delete the tunnel." )
return ioutil . WriteFile ( filePath , body , 400 )
}
2020-07-02 09:31:12 +00:00
func readTunnelCredentials ( c * cli . Context , tunnelID string , logger logger . Service ) ( * pogs . TunnelAuth , error ) {
filePath , err := tunnelCredentialsPath ( c , tunnelID , logger )
2020-06-16 22:43:22 +00:00
if err != nil {
return nil , err
}
body , err := ioutil . ReadFile ( filePath )
if err != nil {
return nil , errors . Wrapf ( err , "couldn't read tunnel credentials from %v" , filePath )
}
2020-06-25 18:25:39 +00:00
var auth pogs . TunnelAuth
if err = json . Unmarshal ( body , & auth ) ; err != nil {
return nil , err
}
return & auth , nil
2020-06-15 18:33:41 +00:00
}
2020-07-02 09:31:12 +00:00
func tunnelCredentialsPath ( c * cli . Context , tunnelID string , logger logger . Service ) ( string , error ) {
if filePath := c . String ( "credentials-file" ) ; filePath != "" {
if validFilePath ( filePath ) {
return filePath , nil
}
}
// Fallback to look for tunnel credentials in the origin cert directory
if originCertPath , err := findOriginCert ( c , logger ) ; err == nil {
originCertDir := filepath . Dir ( originCertPath )
if filePath , err := tunnelFilePath ( tunnelID , originCertDir ) ; err == nil {
if validFilePath ( filePath ) {
return filePath , nil
}
}
}
// Last resort look under default config directories
for _ , configDir := range config . DefaultConfigDirs {
if filePath , err := tunnelFilePath ( tunnelID , configDir ) ; err == nil {
if validFilePath ( filePath ) {
return filePath , nil
}
}
}
return "" , fmt . Errorf ( "Tunnel credentials file not found" )
}
func validFilePath ( path string ) bool {
fileStat , err := os . Stat ( path )
if err != nil {
return false
}
return ! fileStat . IsDir ( )
}
2020-05-21 20:36:49 +00:00
func buildListCommand ( ) * cli . Command {
return & cli . Command {
Name : "list" ,
Action : cliutil . ErrorHandler ( listTunnels ) ,
Usage : "List existing tunnels" ,
ArgsUsage : " " ,
Hidden : hideSubcommands ,
2020-06-16 22:55:33 +00:00
Flags : [ ] cli . Flag { outputFormatFlag , showDeletedFlag } ,
2020-05-21 20:36:49 +00:00
}
}
func listTunnels ( c * cli . Context ) error {
2020-04-29 20:51:32 +00:00
logger , err := logger . New ( )
if err != nil {
return errors . Wrap ( err , "error setting up logger" )
}
2020-06-15 18:33:41 +00:00
originCertPath , err := findOriginCert ( c , logger )
if err != nil {
return errors . Wrap ( err , "Error locating origin cert" )
}
cert , err := getOriginCertFromContext ( originCertPath , logger )
2020-05-21 20:36:49 +00:00
if err != nil {
return err
}
2020-06-15 18:33:41 +00:00
client := newTunnelstoreClient ( c , cert , logger )
2020-05-21 20:36:49 +00:00
2020-06-16 22:55:33 +00:00
allTunnels , err := client . ListTunnels ( )
2020-05-21 20:36:49 +00:00
if err != nil {
return errors . Wrap ( err , "Error listing tunnels" )
}
2020-06-16 22:55:33 +00:00
var tunnels [ ] tunnelstore . Tunnel
if c . Bool ( "show-deleted" ) {
tunnels = allTunnels
} else {
for _ , tunnel := range allTunnels {
if tunnel . DeletedAt . IsZero ( ) {
tunnels = append ( tunnels , tunnel )
}
}
}
2020-05-21 20:36:49 +00:00
if outputFormat := c . String ( outputFormatFlag . Name ) ; outputFormat != "" {
return renderOutput ( outputFormat , tunnels )
}
if len ( tunnels ) > 0 {
2020-06-11 23:44:39 +00:00
const listFormat = "%-40s%-30s%-30s%s\n"
fmt . Printf ( listFormat , "ID" , "NAME" , "CREATED" , "CONNECTIONS" )
2020-05-21 20:36:49 +00:00
for _ , t := range tunnels {
2020-06-11 23:44:39 +00:00
fmt . Printf ( listFormat , t . ID , t . Name , t . CreatedAt . Format ( time . RFC3339 ) , fmtConnections ( t . Connections ) )
2020-05-21 20:36:49 +00:00
}
} else {
fmt . Println ( "You have no tunnels, use 'cloudflared tunnel create' to define a new tunnel" )
}
return nil
}
2020-06-11 23:44:39 +00:00
func fmtConnections ( connections [ ] tunnelstore . Connection ) string {
// Count connections per colo
numConnsPerColo := make ( map [ string ] uint , len ( connections ) )
for _ , connection := range connections {
numConnsPerColo [ connection . ColoName ] ++
}
// Get sorted list of colos
sortedColos := [ ] string { }
for coloName := range numConnsPerColo {
sortedColos = append ( sortedColos , coloName )
}
sort . Strings ( sortedColos )
// Map each colo to its frequency, combine into output string.
var output [ ] string
for _ , coloName := range sortedColos {
output = append ( output , fmt . Sprintf ( "%dx%s" , numConnsPerColo [ coloName ] , coloName ) )
}
return strings . Join ( output , ", " )
}
2020-05-21 20:36:49 +00:00
func buildDeleteCommand ( ) * cli . Command {
return & cli . Command {
Name : "delete" ,
Action : cliutil . ErrorHandler ( deleteTunnel ) ,
Usage : "Delete existing tunnel with given ID" ,
ArgsUsage : "TUNNEL-ID" ,
Hidden : hideSubcommands ,
2020-07-02 09:31:12 +00:00
Flags : [ ] cli . Flag { credentialsFileFlag } ,
2020-05-21 20:36:49 +00:00
}
}
func deleteTunnel ( c * cli . Context ) error {
if c . NArg ( ) != 1 {
return cliutil . UsageError ( ` "cloudflared tunnel delete" requires exactly 1 argument, the ID of the tunnel to delete. ` )
}
id := c . Args ( ) . First ( )
2020-04-29 20:51:32 +00:00
logger , err := logger . New ( )
if err != nil {
return errors . Wrap ( err , "error setting up logger" )
}
2020-06-15 18:33:41 +00:00
originCertPath , err := findOriginCert ( c , logger )
if err != nil {
return errors . Wrap ( err , "Error locating origin cert" )
}
cert , err := getOriginCertFromContext ( originCertPath , logger )
2020-05-21 20:36:49 +00:00
if err != nil {
return err
}
2020-06-15 18:33:41 +00:00
client := newTunnelstoreClient ( c , cert , logger )
2020-05-21 20:36:49 +00:00
if err := client . DeleteTunnel ( id ) ; err != nil {
return errors . Wrapf ( err , "Error deleting tunnel %s" , id )
}
2020-07-02 09:31:12 +00:00
tunnelCredentialsPath , err := tunnelCredentialsPath ( c , id , logger )
if err != nil {
logger . Infof ( "Cannot locate tunnel credentials to delete, error: %v. Please delete the file manually" , err )
return nil
}
if err = os . Remove ( tunnelCredentialsPath ) ; err != nil {
logger . Infof ( "Cannot delete tunnel credentials, error: %v. Please delete the file manually" , err )
}
2020-05-21 20:36:49 +00:00
return nil
}
func renderOutput ( format string , v interface { } ) error {
switch format {
case "json" :
encoder := json . NewEncoder ( os . Stdout )
encoder . SetIndent ( "" , " " )
return encoder . Encode ( v )
case "yaml" :
return yaml . NewEncoder ( os . Stdout ) . Encode ( v )
default :
return errors . Errorf ( "Unknown output format '%s'" , format )
}
}
2020-06-15 18:33:41 +00:00
func newTunnelstoreClient ( c * cli . Context , cert * certutil . OriginCert , logger logger . Service ) tunnelstore . Client {
client := tunnelstore . NewRESTClient ( c . String ( "api-url" ) , cert . AccountID , cert . ServiceKey , logger )
return client
}
func getOriginCertFromContext ( originCertPath string , logger logger . Service ) ( * certutil . OriginCert , error ) {
2020-05-21 20:36:49 +00:00
2020-04-29 20:51:32 +00:00
blocks , err := readOriginCert ( originCertPath , logger )
2020-05-21 20:36:49 +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 )
}
2020-06-15 18:33:41 +00:00
return cert , nil
2020-05-21 20:36:49 +00:00
}
2020-06-16 22:43:22 +00:00
func buildRunCommand ( ) * cli . Command {
return & cli . Command {
Name : "run" ,
Action : cliutil . ErrorHandler ( runTunnel ) ,
Usage : "Proxy a local web server by running the given tunnel" ,
ArgsUsage : "TUNNEL-ID" ,
Hidden : hideSubcommands ,
2020-07-02 09:31:12 +00:00
Flags : [ ] cli . Flag { forceFlag , credentialsFileFlag } ,
2020-06-16 22:43:22 +00:00
}
}
func runTunnel ( c * cli . Context ) error {
if c . NArg ( ) != 1 {
return cliutil . UsageError ( ` "cloudflared tunnel run" requires exactly 1 argument, the ID of the tunnel to run. ` )
}
id := c . Args ( ) . First ( )
2020-06-25 18:25:39 +00:00
tunnelID , err := uuid . Parse ( id )
if err != nil {
return errors . Wrap ( err , "error parsing tunnel ID" )
}
2020-06-16 22:43:22 +00:00
logger , err := logger . New ( )
if err != nil {
return errors . Wrap ( err , "error setting up logger" )
}
2020-07-02 09:31:12 +00:00
credentials , err := readTunnelCredentials ( c , id , logger )
2020-06-16 22:43:22 +00:00
if err != nil {
return err
}
logger . Debugf ( "Read credentials for %v" , credentials . AccountTag )
2020-06-25 18:25:39 +00:00
return StartServer ( c , version , shutdownC , graceShutdownC , & origin . NamedTunnelConfig { Auth : * credentials , ID : tunnelID } )
2020-06-16 22:43:22 +00:00
}