2018-05-01 23:45:06 +00:00
package main
import (
"crypto/tls"
"crypto/x509"
"encoding/hex"
"fmt"
"io/ioutil"
"math/rand"
"net"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/cloudflare/cloudflared/origin"
"github.com/cloudflare/cloudflared/tlsconfig"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/cloudflare/cloudflared/validation"
"github.com/sirupsen/logrus"
"gopkg.in/urfave/cli.v2"
"gopkg.in/urfave/cli.v2/altsrc"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
)
var (
defaultConfigFiles = [ ] string { "config.yml" , "config.yaml" }
// Launchd doesn't set root env variables, so there is default
// Windows default config dir was ~/cloudflare-warp in documentation; let's keep it compatible
defaultConfigDirs = [ ] string { "~/.cloudflared" , "~/.cloudflare-warp" , "~/cloudflare-warp" , "/usr/local/etc/cloudflared" , "/etc/cloudflared" }
)
const defaultCredentialFile = "cert.pem"
func fileExists ( path string ) ( bool , error ) {
f , err := os . Open ( path )
if err != nil {
if os . IsNotExist ( err ) {
// ignore missing files
return false , nil
}
return false , err
}
f . Close ( )
return true , nil
}
// returns the first path that contains a cert.pem file. If none of the defaultConfigDirs
// (differs by OS for legacy reasons) contains a cert.pem file, return empty string
func findDefaultOriginCertPath ( ) string {
for _ , defaultConfigDir := range defaultConfigDirs {
originCertPath , _ := homedir . Expand ( filepath . Join ( defaultConfigDir , defaultCredentialFile ) )
if ok , _ := fileExists ( originCertPath ) ; ok {
return originCertPath
}
}
return ""
}
// returns the first path that contains a config file. If none of the combination of
// defaultConfigDirs (differs by OS for legacy reasons) and defaultConfigFiles
// contains a config file, return empty string
func findDefaultConfigPath ( ) string {
for _ , configDir := range defaultConfigDirs {
for _ , configFile := range defaultConfigFiles {
dirPath , err := homedir . Expand ( configDir )
if err != nil {
continue
}
path := filepath . Join ( dirPath , configFile )
if ok , _ := fileExists ( path ) ; ok {
return path
}
}
}
return ""
}
func findInputSourceContext ( context * cli . Context ) ( altsrc . InputSourceContext , error ) {
if context . String ( "config" ) != "" {
return altsrc . NewYamlSourceFromFile ( context . String ( "config" ) )
}
return nil , nil
}
func generateRandomClientID ( ) string {
r := rand . New ( rand . NewSource ( time . Now ( ) . UnixNano ( ) ) )
id := make ( [ ] byte , 32 )
r . Read ( id )
return hex . EncodeToString ( id )
}
func enoughOptionsSet ( c * cli . Context ) bool {
// For cloudflared to work, the user needs to at least provide a hostname,
// or runs as stand alone DNS proxy .
// When using sudo, use -E flag to preserve env vars
if c . NumFlags ( ) == 0 && c . NArg ( ) == 0 && os . Getenv ( "TUNNEL_HOSTNAME" ) == "" && os . Getenv ( "TUNNEL_DNS" ) == "" {
if isRunningFromTerminal ( ) {
logger . Errorf ( "No arguments were provided. You need to at least specify the hostname for this tunnel. See %s" , quickStartUrl )
logger . Infof ( "If you want to run Argo Tunnel client as a stand alone DNS proxy, run with --proxy-dns option or set TUNNEL_DNS environment variable." )
} else {
logger . Errorf ( "You need to specify all the options in a configuration file, or use environment variables. See %s and %s" , serviceUrl , argumentsUrl )
logger . Infof ( "If you want to run Argo Tunnel client as a stand alone DNS proxy, specify proxy-dns option in the configuration file, or set TUNNEL_DNS environment variable." )
}
cli . ShowAppHelp ( c )
return false
}
return true
}
func handleDeprecatedOptions ( c * cli . Context ) error {
// Fail if the user provided an old authentication method
if c . IsSet ( "api-key" ) || c . IsSet ( "api-email" ) || c . IsSet ( "api-ca-key" ) {
logger . Error ( "You don't need to give us your api-key anymore. Please use the new login method. Just run cloudflared login" )
return fmt . Errorf ( "Client provided deprecated options" )
}
return nil
}
func validateUrl ( c * cli . Context ) ( string , error ) {
var url = c . String ( "url" )
if c . NArg ( ) > 0 {
2018-07-30 21:45:15 +00:00
if ! c . IsSet ( "url" ) {
return "" , errors . New ( "Please specify an origin URL." )
2018-05-01 23:45:06 +00:00
}
}
validUrl , err := validation . ValidateUrl ( url )
return validUrl , err
}
func logClientOptions ( c * cli . Context ) {
flags := make ( map [ string ] interface { } )
for _ , flag := range c . LocalFlagNames ( ) {
flags [ flag ] = c . Generic ( flag )
}
if len ( flags ) > 0 {
logger . Infof ( "Flags %v" , flags )
}
envs := make ( map [ string ] string )
// Find env variables for Argo Tunnel
for _ , env := range os . Environ ( ) {
// All Argo Tunnel env variables start with TUNNEL_
if strings . Contains ( env , "TUNNEL_" ) {
vars := strings . Split ( env , "=" )
if len ( vars ) == 2 {
envs [ vars [ 0 ] ] = vars [ 1 ]
}
}
}
if len ( envs ) > 0 {
logger . Infof ( "Environmental variables %v" , envs )
}
}
func dnsProxyStandAlone ( c * cli . Context ) bool {
return c . IsSet ( "proxy-dns" ) && ( ! c . IsSet ( "hostname" ) && ! c . IsSet ( "tag" ) && ! c . IsSet ( "hello-world" ) )
}
func getOriginCert ( c * cli . Context ) ( [ ] byte , error ) {
if c . String ( "origincert" ) == "" {
logger . Warnf ( "Cannot determine default origin certificate path. No file %s in %v" , defaultCredentialFile , defaultConfigDirs )
if isRunningFromTerminal ( ) {
logger . Errorf ( "You need to specify the origin certificate path with --origincert option, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information." , argumentsUrl )
return nil , fmt . Errorf ( "Client didn't specify origincert path when running from terminal" )
} else {
logger . Errorf ( "You need to specify the origin certificate path by specifying the origincert option in the configuration file, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information." , serviceUrl )
return nil , fmt . Errorf ( "Client didn't specify origincert path" )
}
}
// Check that the user has acquired a certificate using the login command
originCertPath , err := homedir . Expand ( c . String ( "origincert" ) )
if err != nil {
logger . WithError ( err ) . Errorf ( "Cannot resolve path %s" , c . String ( "origincert" ) )
return nil , fmt . Errorf ( "Cannot resolve path %s" , c . String ( "origincert" ) )
}
ok , err := fileExists ( originCertPath )
if err != nil {
logger . Errorf ( "Cannot check if origin cert exists at path %s" , c . String ( "origincert" ) )
return nil , fmt . Errorf ( "Cannot check if origin cert exists at path %s" , c . String ( "origincert" ) )
}
if ! ok {
logger . Errorf ( ` Cannot find a valid certificate for your origin at the path :
% s
If the path above is wrong , specify the path with the - origincert option .
If you don ' t have a certificate signed by Cloudflare , run the command :
% s login
` , originCertPath , os . Args [ 0 ] )
return nil , fmt . Errorf ( "Cannot find a valid certificate at the path %s" , originCertPath )
}
// Easier to send the certificate as []byte via RPC than decoding it at this point
originCert , err := ioutil . ReadFile ( originCertPath )
if err != nil {
logger . WithError ( err ) . Errorf ( "Cannot read %s to load origin certificate" , originCertPath )
return nil , fmt . Errorf ( "Cannot read %s to load origin certificate" , originCertPath )
}
return originCert , nil
}
func prepareTunnelConfig ( c * cli . Context , buildInfo * origin . BuildInfo , logger , protoLogger * logrus . Logger ) ( * origin . TunnelConfig , error ) {
hostname , err := validation . ValidateHostname ( c . String ( "hostname" ) )
if err != nil {
logger . WithError ( err ) . Error ( "Invalid hostname" )
return nil , errors . Wrap ( err , "Invalid hostname" )
}
clientID := c . String ( "id" )
if ! c . IsSet ( "id" ) {
clientID = generateRandomClientID ( )
}
tags , err := NewTagSliceFromCLI ( c . StringSlice ( "tag" ) )
if err != nil {
logger . WithError ( err ) . Error ( "Tag parse failure" )
return nil , errors . Wrap ( err , "Tag parse failure" )
}
tags = append ( tags , tunnelpogs . Tag { Name : "ID" , Value : clientID } )
url , err := validateUrl ( c )
if err != nil {
logger . WithError ( err ) . Error ( "Error validating url" )
return nil , errors . Wrap ( err , "Error validating url" )
}
logger . Infof ( "Proxying tunnel requests to %s" , url )
originCert , err := getOriginCert ( c )
if err != nil {
return nil , errors . Wrap ( err , "Error getting origin cert" )
}
originCertPool , err := loadCertPool ( c , logger )
if err != nil {
logger . WithError ( err ) . Error ( "Error loading cert pool" )
return nil , errors . Wrap ( err , "Error loading cert pool" )
}
tunnelMetrics := origin . NewTunnelMetrics ( )
httpTransport := & http . Transport {
Proxy : http . ProxyFromEnvironment ,
DialContext : ( & net . Dialer {
Timeout : c . Duration ( "proxy-connect-timeout" ) ,
KeepAlive : c . Duration ( "proxy-tcp-keepalive" ) ,
DualStack : ! c . Bool ( "proxy-no-happy-eyeballs" ) ,
} ) . DialContext ,
MaxIdleConns : c . Int ( "proxy-keepalive-connections" ) ,
IdleConnTimeout : c . Duration ( "proxy-keepalive-timeout" ) ,
TLSHandshakeTimeout : c . Duration ( "proxy-tls-timeout" ) ,
ExpectContinueTimeout : 1 * time . Second ,
TLSClientConfig : & tls . Config { RootCAs : originCertPool , InsecureSkipVerify : c . IsSet ( "no-tls-verify" ) } ,
}
if ! c . IsSet ( "hello-world" ) && c . IsSet ( "origin-server-name" ) {
httpTransport . TLSClientConfig . ServerName = c . String ( "origin-server-name" )
}
return & origin . TunnelConfig {
EdgeAddrs : c . StringSlice ( "edge" ) ,
OriginUrl : url ,
Hostname : hostname ,
OriginCert : originCert ,
TlsConfig : tlsconfig . CreateTunnelConfig ( c , c . StringSlice ( "edge" ) ) ,
ClientTlsConfig : httpTransport . TLSClientConfig ,
Retries : c . Uint ( "retries" ) ,
HeartbeatInterval : c . Duration ( "heartbeat-interval" ) ,
MaxHeartbeats : c . Uint64 ( "heartbeat-count" ) ,
ClientID : clientID ,
BuildInfo : buildInfo ,
ReportedVersion : Version ,
LBPool : c . String ( "lb-pool" ) ,
Tags : tags ,
HAConnections : c . Int ( "ha-connections" ) ,
HTTPTransport : httpTransport ,
Metrics : tunnelMetrics ,
MetricsUpdateFreq : c . Duration ( "metrics-update-freq" ) ,
ProtocolLogger : protoLogger ,
Logger : logger ,
IsAutoupdated : c . Bool ( "is-autoupdated" ) ,
GracePeriod : c . Duration ( "grace-period" ) ,
RunFromTerminal : isRunningFromTerminal ( ) ,
NoChunkedEncoding : c . Bool ( "no-chunked-encoding" ) ,
CompressionQuality : c . Uint64 ( "compression-quality" ) ,
} , nil
}
func loadCertPool ( c * cli . Context , logger * logrus . Logger ) ( * x509 . CertPool , error ) {
const originCAPoolFlag = "origin-ca-pool"
originCAPoolFilename := c . String ( originCAPoolFlag )
var originCustomCAPool [ ] byte
if originCAPoolFilename != "" {
var err error
originCustomCAPool , err = ioutil . ReadFile ( originCAPoolFilename )
if err != nil {
return nil , errors . Wrap ( err , fmt . Sprintf ( "unable to read the file %s for --%s" , originCAPoolFilename , originCAPoolFlag ) )
}
}
originCertPool , err := tlsconfig . LoadOriginCertPool ( originCustomCAPool )
if err != nil {
return nil , errors . Wrap ( err , "error loading the certificate pool" )
}
// Windows users should be notified that they can use the flag
if runtime . GOOS == "windows" && originCAPoolFilename == "" {
logger . Infof ( "cloudflared does not support loading the system root certificate pool on Windows. Please use the --%s to specify it" , originCAPoolFlag )
}
return originCertPool , nil
}