Compare commits

..

26 Commits

Author SHA1 Message Date
Adam Chalmers 3b4ed70cf3 Release 2020.12.0 2020-12-08 16:17:24 -06:00
Adam Chalmers d45ca67498 TUN-3612: Upgrade to Go 1.15.6 2020-12-04 23:24:16 +00:00
Sudarsan Reddy 1c0dac77d7 TUN-3599: improved delete if credentials isnt found.
Tunnel delete is successful even if we don't find the credentials
file in the user's filesystem. We no longer "error" indicating this
is a problem. This fix also enables chaining of the delete command
by removing a pre-mature return if the credentials file is not found.
2020-12-04 11:44:13 +00:00
Adam Chalmers 38fb0b28b6 TUN-3593: /ready endpoint for k8s readiness. Move tunnel events out of UI package, into connection package. 2020-12-02 15:22:59 -06:00
cthuang bda8fe2fbe TUN-3594: Log response status at debug level 2020-11-27 12:28:20 +00:00
Adam Chalmers 6a8c7c0727 Release 2020.11.11 2020-11-25 10:09:42 -06:00
Adam Chalmers 69fd502db3 TUN-3581: Tunnels can be run by name using only --credentials-file, no
origin cert necessary.
2020-11-25 09:54:28 -06:00
Michael Borkenstein fcc393e2f0 AUTH-3221: Saves org token to disk and uses it to refresh the app token 2020-11-24 21:38:59 +00:00
Areg Harutyunyan cad58b9b57 TUN-3561: Unified logger configuration 2020-11-23 16:49:07 +00:00
Joe Groocock 78cb60b85f EDGEPLAT-2958 remove deb-compression, defaulting to gzip
dpkg does not support bzip2 compression, so fails to unpack and install
the built package. By omitting the option, fpm defaults to gzip which is
the default supported option by dpkg.

Signed-off-by: Joe Groocock <jgroocock@cloudflare.com>
2020-11-23 16:27:11 +00:00
Adam Chalmers a08a7030d1 TUN-3578: cloudflared tunnel route dns should allow wildcard subdomains 2020-11-23 09:37:46 -06:00
Adam Chalmers 87203bbe25 Release 2020.11.10 2020-11-20 12:25:44 -06:00
Joe Groocock 11acb50cf7 EDGEPLAT-2958 build cloudflared for Bullseye
Signed-off-by: Joe Groocock <jgroocock@cloudflare.com>
2020-11-20 18:24:58 +00:00
Adam Chalmers 23f2a04ed7 TUN-3562: Fix panic when using bastion mode ingress rule 2020-11-20 11:20:39 -06:00
cthuang 1805261263 Release 2020.11.9 2020-11-19 09:29:12 +00:00
Adam Chalmers 53de779a0a TUN-3544: Upgrade to Go 1.15.5 2020-11-18 16:13:54 -06:00
Adam Chalmers b7e91466f5 TUN-3558: cloudflared allows empty config files 2020-11-18 21:13:06 +00:00
Troy Varney 4c1b89576c DEVTOOLS-7936: Remove redundant chgrp from publish
This removes the redundant chgrp command from the publish step when
pushing packages to our public repositories. The directory being pushed
to has the setgid bit set on it, which means that we don't need to force
the group using this command. Further, attempting to do so resulted in
an error as the cfsync user does not have the appropriate permissions to
use the chgrp command.
2020-11-18 19:35:26 +00:00
cthuang a1a554a29d TUN-3559: Share response meta header with other packages 2020-11-18 16:51:03 +00:00
cthuang fdb1f961b3 TUN-3557: Detect SSE if content-type starts with text/event-stream 2020-11-18 15:59:41 +00:00
Adam Chalmers 293b9af4a7 Release 2020.11.8 2020-11-17 17:15:50 -06:00
Adam Chalmers 029f7e0378 TUN-3555: Single origin service should default to localhost:8080 2020-11-17 23:12:32 +00:00
Adam Chalmers 58c5e25b9a Release 2020.11.7 2020-11-16 14:13:04 -06:00
Adam Chalmers 25e72f7760 TUN-3549: Use a separate handler for each websocket proxy 2020-11-16 20:05:35 +00:00
Adam Chalmers 7613410855 TUN-3548, TUN-3547: Bastion mode can be specified as a service, doesn't
require URL.
2020-11-16 20:04:36 +00:00
cthuang c40cb7dc56 TUN-3514: Stop setting --is-autoupdated flag after autoupdate because it can break named tunnel running in k8s 2020-11-16 09:40:38 +00:00
58 changed files with 1522 additions and 488 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
# use a builder image for building cloudflare
ARG TARGET_GOOS
ARG TARGET_GOARCH
FROM golang:1.15.3 as builder
FROM golang:1.15.6 as builder
ENV GO111MODULE=on \
CGO_ENABLED=0 \
TARGET_GOOS=${TARGET_GOOS} \
+2 -2
View File
@@ -89,7 +89,7 @@ define publish_package
for HOST in $(CF_PKG_HOSTS); do \
ssh-keyscan -t rsa $$HOST >> ~/.ssh/known_hosts; \
scp -4 cloudflared*.$(1) cfsync@$$HOST:/state/cf-pkg/staging/$(2)/$(TARGET_PUBLIC_REPO)/cloudflared/; \
ssh cfsync@$$HOST 'chgrp cf /state/cf-pkg/staging/$(2)/$(TARGET_PUBLIC_REPO)/cloudflared/*.$(1) && chmod g+w /state/cf-pkg/staging/$(2)/$(TARGET_PUBLIC_REPO)/cloudflared/*.$(1)'; \
ssh cfsync@$$HOST 'chmod g+w /state/cf-pkg/staging/$(2)/$(TARGET_PUBLIC_REPO)/cloudflared/*.$(1)'; \
done
endef
@@ -105,7 +105,7 @@ define build_package
mkdir -p $(PACKAGE_DIR)
cp cloudflared $(PACKAGE_DIR)/cloudflared
cat cloudflared_man_template | sed -e 's/\$${VERSION}/$(VERSION)/; s/\$${DATE}/$(DATE)/' > $(PACKAGE_DIR)/cloudflared.1
fakeroot fpm -C $(PACKAGE_DIR) -s dir -t $(1) --$(1)-compression bzip2 \
fakeroot fpm -C $(PACKAGE_DIR) -s dir -t $(1) \
--description 'Cloudflare Argo tunnel daemon' \
--vendor 'Cloudflare' \
--license 'Cloudflare Service Agreement' \
+32
View File
@@ -1,3 +1,35 @@
2020.12.0
- 2020-12-04 TUN-3599: improved delete if credentials isnt found.
- 2020-12-04 TUN-3612: Upgrade to Go 1.15.6
- 2020-11-30 TUN-3593: /ready endpoint for k8s readiness. Move tunnel events out of UI package, into connection package.
- 2020-11-27 TUN-3594: Log response status at debug level
2020.11.11
- 2020-11-20 TUN-3578: cloudflared tunnel route dns should allow wildcard subdomains
- 2020-11-21 EDGEPLAT-2958 remove deb-compression, defaulting to gzip
- 2020-11-23 TUN-3581: Tunnels can be run by name using only --credentials-file, no origin cert necessary.
- 2020-11-15 TUN-3561: Unified logger configuration
- 2020-11-08 AUTH-3221: Saves org token to disk and uses it to refresh the app token
2020.11.10
- 2020-11-20 TUN-3562: Fix panic when using bastion mode ingress rule
- 2020-11-20 EDGEPLAT-2958 build cloudflared for Bullseye
2020.11.9
- 2020-11-18 TUN-3557: Detect SSE if content-type starts with text/event-stream
- 2020-11-18 TUN-3559: Share response meta header with other packages
- 2020-11-18 DEVTOOLS-7936: Remove redundant chgrp from publish
- 2020-11-18 TUN-3558: cloudflared allows empty config files
- 2020-11-18 TUN-3544: Upgrade to Go 1.15.5
2020.11.8
- 2020-11-17 TUN-3555: Single origin service should default to localhost:8080
2020.11.7
- 2020-11-13 TUN-3514: Stop setting --is-autoupdated flag after autoupdate because it can break named tunnel running in k8s
- 2020-11-15 TUN-3548, TUN-3547: Bastion mode can be specified as a service, doesn't require URL.
- 2020-11-16 TUN-3549: Use a separate handler for each websocket proxy
2020.11.6
- 2020-11-14 TUN-3546: Fix panic in tlsconfig.LoadOriginCA
+7 -9
View File
@@ -1,6 +1,6 @@
pinned_go: &pinned_go go=1.15.3-1
pinned_go: &pinned_go go=1.15.6-1
build_dir: &build_dir /cfsetup_build
default-flavor: stretch
default-flavor: buster
stretch: &stretch
build:
build_dir: *build_dir
@@ -239,10 +239,8 @@ stretch: &stretch
- export GOARCH=amd64
- make publish-cloudflared-junos
jessie: *stretch
buster: *stretch
bullseye: *stretch
centos-7:
publish-rpm:
build_dir: *build_dir
@@ -250,8 +248,8 @@ centos-7:
- https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
pre-cache:
- yum install -y fakeroot
- wget https://golang.org/dl/go1.15.3.linux-amd64.tar.gz -P /tmp/
- tar -C /usr/local -xzf /tmp/go1.15.3.linux-amd64.tar.gz
- wget https://golang.org/dl/go1.15.6.linux-amd64.tar.gz -P /tmp/
- tar -C /usr/local -xzf /tmp/go1.15.6.linux-amd64.tar.gz
post-cache:
- export PATH=$PATH:/usr/local/go/bin
- export GOOS=linux
@@ -262,8 +260,8 @@ centos-7:
builddeps: *el7_builddeps
pre-cache:
- yum install -y fakeroot
- wget https://golang.org/dl/go1.15.3.linux-amd64.tar.gz -P /tmp/
- tar -C /usr/local -xzf /tmp/go1.15.3.linux-amd64.tar.gz
- wget https://golang.org/dl/go1.15.6.linux-amd64.tar.gz -P /tmp/
- tar -C /usr/local -xzf /tmp/go1.15.6.linux-amd64.tar.gz
post-cache:
- export PATH=$PATH:/usr/local/go/bin
- export GOOS=linux
+2 -14
View File
@@ -11,7 +11,7 @@ import (
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/validation"
"github.com/pkg/errors"
cli "github.com/urfave/cli/v2"
"github.com/urfave/cli/v2"
)
// StartForwarder starts a client side websocket forward
@@ -52,19 +52,7 @@ func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, logger
// useful for proxying other protocols (like ssh) over websockets
// (which you can put Access in front of)
func ssh(c *cli.Context) error {
logDirectory, logLevel := config.FindLogSettings()
flagLogDirectory := c.String(sshLogDirectoryFlag)
if flagLogDirectory != "" {
logDirectory = flagLogDirectory
}
flagLogLevel := c.String(sshLogLevelFlag)
if flagLogLevel != "" {
logLevel = flagLogLevel
}
logger, err := logger.New(logger.DefaultFile(logDirectory), logger.LogLevelString(logLevel))
logger, err := logger.CreateSSHLoggerFromContext(c, logger.EnableTerminalLog)
if err != nil {
return cliutil.PrintLoggerSetupError("error setting up logger", err)
}
+16 -18
View File
@@ -25,16 +25,14 @@ import (
)
const (
sshHostnameFlag = "hostname"
sshDestinationFlag = "destination"
sshURLFlag = "url"
sshHeaderFlag = "header"
sshTokenIDFlag = "service-token-id"
sshTokenSecretFlag = "service-token-secret"
sshGenCertFlag = "short-lived-cert"
sshLogDirectoryFlag = "log-directory"
sshLogLevelFlag = "log-level"
sshConfigTemplate = `
sshHostnameFlag = "hostname"
sshDestinationFlag = "destination"
sshURLFlag = "url"
sshHeaderFlag = "header"
sshTokenIDFlag = "service-token-id"
sshTokenSecretFlag = "service-token-secret"
sshGenCertFlag = "short-lived-cert"
sshConfigTemplate = `
Add to your {{.Home}}/.ssh/config:
Host {{.Hostname}}
@@ -157,12 +155,12 @@ func Commands() []*cli.Command {
Usage: "specify an Access service token secret you wish to use.",
},
&cli.StringFlag{
Name: sshLogDirectoryFlag,
Name: logger.LogSSHDirectoryFlag,
Aliases: []string{"logfile"}, //added to match the tunnel side
Usage: "Save application log to this directory for reporting issues.",
},
&cli.StringFlag{
Name: sshLogLevelFlag,
Name: logger.LogSSHLevelFlag,
Aliases: []string{"loglevel"}, //added to match the tunnel side
Usage: "Application logging level {fatal, error, info, debug}. ",
},
@@ -207,7 +205,7 @@ func login(c *cli.Context) error {
return err
}
logger, err := logger.New()
logger, err := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
@@ -224,7 +222,7 @@ func login(c *cli.Context) error {
return err
}
cfdToken, err := token.GetTokenIfExists(appURL)
cfdToken, err := token.GetAppTokenIfExists(appURL)
if err != nil {
fmt.Fprintln(os.Stderr, "Unable to find token for provided application.")
return err
@@ -252,7 +250,7 @@ func curl(c *cli.Context) error {
if err := raven.SetDSN(sentryDSN); err != nil {
return err
}
logger, err := logger.New()
logger, err := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
@@ -269,7 +267,7 @@ func curl(c *cli.Context) error {
return err
}
tok, err := token.GetTokenIfExists(appURL)
tok, err := token.GetAppTokenIfExists(appURL)
if err != nil || tok == "" {
if allowRequest {
logger.Info("You don't have an Access token set. Please run access token <access application> to fetch one.")
@@ -297,7 +295,7 @@ func generateToken(c *cli.Context) error {
fmt.Fprintln(os.Stderr, "Please provide a url.")
return err
}
tok, err := token.GetTokenIfExists(appURL)
tok, err := token.GetAppTokenIfExists(appURL)
if err != nil || tok == "" {
fmt.Fprintln(os.Stderr, "Unable to find token for provided application. Please run token command to generate token.")
return err
@@ -331,7 +329,7 @@ func sshConfig(c *cli.Context) error {
// sshGen generates a short lived certificate for provided hostname
func sshGen(c *cli.Context) error {
logger, err := logger.New()
logger, err := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
+1
View File
@@ -37,6 +37,7 @@ func (s *AppService) Run() error {
func (s *AppService) Shutdown() error {
s.configManager.Shutdown()
s.shutdownC <- struct{}{}
return nil
}
+5 -29
View File
@@ -2,6 +2,7 @@ package config
import (
"fmt"
"io"
"net/url"
"os"
"path/filepath"
@@ -150,35 +151,6 @@ func FindOrCreateConfigPath() string {
return path
}
// FindLogSettings gets the log directory and level from the config file
func FindLogSettings() (string, string) {
configPath := FindOrCreateConfigPath()
defaultDirectory := DefaultLogDirectory()
defaultLevel := "info"
file, err := os.Open(configPath)
if err != nil {
return defaultDirectory, defaultLevel
}
defer file.Close()
var config Root
if err := yaml.NewDecoder(file).Decode(&config); err != nil {
return defaultDirectory, defaultLevel
}
directory := defaultDirectory
if config.LogDirectory != "" {
directory = config.LogDirectory
}
level := defaultLevel
if config.LogLevel != "" {
level = config.LogLevel
}
return directory, level
}
// ValidateUnixSocket ensures --unix-socket param is used exclusively
// i.e. it fails if a user specifies both --url and --unix-socket
func ValidateUnixSocket(c *cli.Context) (string, error) {
@@ -392,6 +364,10 @@ func ReadConfigFile(c *cli.Context, log logger.Service) (*configFileSettings, er
}
defer file.Close()
if err := yaml.NewDecoder(file).Decode(&configuration); err != nil {
if err == io.EOF {
log.Errorf("Configuration file %s was empty", configFile)
return &configuration, nil
}
return nil, errors.Wrap(err, "error parsing YAML in config file at "+configFile)
}
configuration.sourceFile = configFile
+8 -3
View File
@@ -1,6 +1,7 @@
package config
import (
"io"
"os"
"github.com/cloudflare/cloudflared/logger"
@@ -27,7 +28,7 @@ type FileManager struct {
notifier Notifier
configPath string
logger logger.Service
ReadConfig func(string) (Root, error)
ReadConfig func(string, logger.Service) (Root, error)
}
// NewFileManager creates a config manager
@@ -59,7 +60,7 @@ func (m *FileManager) Start(notifier Notifier) error {
// GetConfig reads the yaml file from the disk
func (m *FileManager) GetConfig() (Root, error) {
return m.ReadConfig(m.configPath)
return m.ReadConfig(m.configPath, m.logger)
}
// Shutdown stops the watcher
@@ -67,7 +68,7 @@ func (m *FileManager) Shutdown() {
m.watcher.Shutdown()
}
func readConfigFromPath(configPath string) (Root, error) {
func readConfigFromPath(configPath string, log logger.Service) (Root, error) {
if configPath == "" {
return Root{}, errors.New("unable to find config file")
}
@@ -80,6 +81,10 @@ func readConfigFromPath(configPath string) (Root, error) {
var config Root
if err := yaml.NewDecoder(file).Decode(&config); err != nil {
if err == io.EOF {
log.Errorf("Configuration file %s was empty", configPath)
return Root{}, nil
}
return Root{}, errors.Wrap(err, "error parsing YAML in config file at "+configPath)
}
+1 -1
View File
@@ -57,7 +57,7 @@ func TestConfigChanged(t *testing.T) {
},
},
}
configRead := func(configPath string) (Root, error) {
configRead := func(configPath string, log logger.Service) (Root, error) {
return *c, nil
}
wait := make(chan struct{})
+4 -3
View File
@@ -12,6 +12,7 @@ import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
"github.com/cloudflare/cloudflared/logger"
)
@@ -211,7 +212,7 @@ func copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile str
}
func installLinuxService(c *cli.Context) error {
logger, err := logger.New()
logger, err := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
@@ -250,7 +251,7 @@ func installLinuxService(c *cli.Context) error {
val, err := src.String(s)
return err == nil && val != ""
}
if src.TunnelID == "" || !configPresent("credentials-file") {
if src.TunnelID == "" || !configPresent(tunnel.CredFileFlag) {
return fmt.Errorf(`Configuration file %s must contain entries for the tunnel to run and its associated credentials:
tunnel: TUNNEL-UUID
credentials-file: CREDENTIALS-FILE
@@ -325,7 +326,7 @@ func installSysv(templateArgs *ServiceTemplateArgs, logger logger.Service) error
}
func uninstallLinuxService(c *cli.Context) error {
logger, err := logger.New()
logger, err := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
+4 -4
View File
@@ -6,11 +6,11 @@ import (
"fmt"
"os"
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
)
const (
@@ -107,7 +107,7 @@ func stderrPath() (string, error) {
}
func installLaunchd(c *cli.Context) error {
logger, err := logger.New()
logger, err := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
@@ -163,7 +163,7 @@ func installLaunchd(c *cli.Context) error {
}
func uninstallLaunchd(c *cli.Context) error {
logger, err := logger.New()
logger, err := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
+6 -9
View File
@@ -15,10 +15,9 @@ import (
"github.com/cloudflare/cloudflared/overwatch"
"github.com/cloudflare/cloudflared/tunneldns"
"github.com/cloudflare/cloudflared/watcher"
raven "github.com/getsentry/raven-go"
homedir "github.com/mitchellh/go-homedir"
cli "github.com/urfave/cli/v2"
"github.com/getsentry/raven-go"
"github.com/mitchellh/go-homedir"
"github.com/urfave/cli/v2"
"github.com/pkg/errors"
)
@@ -147,7 +146,7 @@ func isEmptyInvocation(c *cli.Context) bool {
func action(version string, shutdownC, graceShutdownC chan struct{}) cli.ActionFunc {
return cliutil.ErrorHandler(func(c *cli.Context) (err error) {
if isEmptyInvocation(c) {
return handleServiceMode(shutdownC)
return handleServiceMode(c, shutdownC)
}
tags := make(map[string]string)
tags["hostname"] = c.String("hostname")
@@ -184,15 +183,13 @@ func captureError(err error) {
}
// cloudflared was started without any flags
func handleServiceMode(shutdownC chan struct{}) error {
func handleServiceMode(c *cli.Context, shutdownC chan struct{}) error {
defer log.SharedWriteManager.Shutdown()
logDirectory, logLevel := config.FindLogSettings()
logger, err := log.New(log.DefaultFile(logDirectory), log.LogLevelString(logLevel))
logger, err := log.CreateLoggerFromContext(c, log.DisableTerminalLog)
if err != nil {
return cliutil.PrintLoggerSetupError("error setting up logger", err)
}
logger.Infof("logging to directory: %s", logDirectory)
// start the main run loop that reads from the config file
f, err := watcher.NewFile()
+22 -7
View File
@@ -11,8 +11,27 @@ import (
"github.com/mitchellh/go-homedir"
)
// GenerateFilePathFromURL will return a filepath for given access application url
func GenerateFilePathFromURL(url *url.URL, suffix string) (string, error) {
// GenerateAppTokenFilePathFromURL will return a filepath for given Access org token
func GenerateAppTokenFilePathFromURL(url *url.URL, suffix string) (string, error) {
configPath, err := getConfigPath()
if err != nil {
return "", err
}
name := strings.Replace(fmt.Sprintf("%s%s-%s", url.Hostname(), url.EscapedPath(), suffix), "/", "-", -1)
return filepath.Join(configPath, name), nil
}
// GenerateOrgTokenFilePathFromURL will return a filepath for given Access application token
func GenerateOrgTokenFilePathFromURL(authDomain string) (string, error) {
configPath, err := getConfigPath()
if err != nil {
return "", err
}
name := strings.Replace(fmt.Sprintf("%s-org-token", authDomain), "/", "-", -1)
return filepath.Join(configPath, name), nil
}
func getConfigPath() (string, error) {
configPath, err := homedir.Expand(config.DefaultConfigSearchDirectories()[0])
if err != nil {
return "", err
@@ -22,9 +41,5 @@ func GenerateFilePathFromURL(url *url.URL, suffix string) (string, error) {
// create config directory if doesn't already exist
err = os.Mkdir(configPath, 0700)
}
if err != nil {
return "", err
}
name := strings.Replace(fmt.Sprintf("%s%s-%s", url.Hostname(), url.EscapedPath(), suffix), "/", "-", -1)
return filepath.Join(configPath, name), nil
return configPath, err
}
+200 -32
View File
@@ -5,9 +5,11 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"os/signal"
"strings"
"syscall"
"time"
@@ -17,10 +19,12 @@ import (
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/origin"
"github.com/coreos/go-oidc/jose"
"github.com/pkg/errors"
)
const (
keyName = "token"
keyName = "token"
tokenHeader = "CF_Authorization"
)
type lock struct {
@@ -34,7 +38,7 @@ type signalHandler struct {
signals []os.Signal
}
type jwtPayload struct {
type appJWTPayload struct {
Aud []string `json:"aud"`
Email string `json:"email"`
Exp int `json:"exp"`
@@ -45,7 +49,17 @@ type jwtPayload struct {
Subt string `json:"sub"`
}
func (p jwtPayload) isExpired() bool {
type orgJWTPayload struct {
appJWTPayload
Aud string `json:"aud"`
}
type transferServiceResponse struct {
AppToken string `json:"app_token"`
OrgToken string `json:"org_token"`
}
func (p appJWTPayload) isExpired() bool {
return int(time.Now().Unix()) > p.Exp
}
@@ -141,55 +155,173 @@ func FetchToken(appURL *url.URL, logger logger.Service) (string, error) {
// getToken will either load a stored token or generate a new one
func getToken(appURL *url.URL, useHostOnly bool, logger logger.Service) (string, error) {
if token, err := GetTokenIfExists(appURL); token != "" && err == nil {
if token, err := GetAppTokenIfExists(appURL); token != "" && err == nil {
return token, nil
}
path, err := path.GenerateFilePathFromURL(appURL, keyName)
appTokenPath, err := path.GenerateAppTokenFilePathFromURL(appURL, keyName)
if err != nil {
return "", err
return "", errors.Wrap(err, "failed to generate app token file path")
}
fileLock := newLock(path)
err = fileLock.Acquire()
if err != nil {
return "", err
fileLockAppToken := newLock(appTokenPath)
if err = fileLockAppToken.Acquire(); err != nil {
return "", errors.Wrap(err, "failed to acquire app token lock")
}
defer fileLock.Release()
defer fileLockAppToken.Release()
// check to see if another process has gotten a token while we waited for the lock
if token, err := GetTokenIfExists(appURL); token != "" && err == nil {
if token, err := GetAppTokenIfExists(appURL); token != "" && err == nil {
return token, nil
}
// If an app token couldnt be found on disk, check for an org token and attempt to exchange it for an app token.
var orgTokenPath string
// Get auth domain to format into org token file path
if authDomain, err := getAuthDomain(appURL); err != nil {
logger.Errorf("failed to get auth domain: %s", err)
} else {
orgToken, err := GetOrgTokenIfExists(authDomain)
if err != nil {
orgTokenPath, err = path.GenerateOrgTokenFilePathFromURL(authDomain)
if err != nil {
return "", errors.Wrap(err, "failed to generate org token file path")
}
fileLockOrgToken := newLock(orgTokenPath)
if err = fileLockOrgToken.Acquire(); err != nil {
return "", errors.Wrap(err, "failed to acquire org token lock")
}
defer fileLockOrgToken.Release()
// check if an org token has been created since the lock was acquired
orgToken, err = GetOrgTokenIfExists(authDomain)
}
if err == nil {
if appToken, err := exchangeOrgToken(appURL, orgToken); err != nil {
logger.Debugf("failed to exchange org token for app token: %s", err)
} else {
if err := ioutil.WriteFile(appTokenPath, []byte(appToken), 0600); err != nil {
return "", errors.Wrap(err, "failed to write app token to disk")
}
return appToken, nil
}
}
}
return getTokensFromEdge(appURL, appTokenPath, orgTokenPath, useHostOnly, logger)
}
// getTokensFromEdge will attempt to use the transfer service to retrieve an app and org token, save them to disk,
// and return the app token.
func getTokensFromEdge(appURL *url.URL, appTokenPath, orgTokenPath string, useHostOnly bool, logger logger.Service) (string, error) {
// If no org token exists or if it couldnt be exchanged for an app token, then run the transfer service flow.
// this weird parameter is the resource name (token) and the key/value
// we want to send to the transfer service. the key is token and the value
// is blank (basically just the id generated in the transfer service)
token, err := transfer.Run(appURL, keyName, keyName, "", path, true, useHostOnly, logger)
resourceData, err := transfer.Run(appURL, keyName, keyName, "", true, useHostOnly, logger)
if err != nil {
return "", err
return "", errors.Wrap(err, "failed to run transfer service")
}
var resp transferServiceResponse
if err = json.Unmarshal(resourceData, &resp); err != nil {
return "", errors.Wrap(err, "failed to marshal transfer service response")
}
return string(token), nil
// If we were able to get the auth domain and generate an org token path, lets write it to disk.
if orgTokenPath != "" {
if err := ioutil.WriteFile(orgTokenPath, []byte(resp.OrgToken), 0600); err != nil {
return "", errors.Wrap(err, "failed to write org token to disk")
}
}
if err := ioutil.WriteFile(appTokenPath, []byte(resp.AppToken), 0600); err != nil {
return "", errors.Wrap(err, "failed to write app token to disk")
}
return resp.AppToken, nil
}
// GetTokenIfExists will return the token from local storage if it exists and not expired
func GetTokenIfExists(url *url.URL) (string, error) {
path, err := path.GenerateFilePathFromURL(url, keyName)
if err != nil {
return "", err
}
content, err := ioutil.ReadFile(path)
if err != nil {
return "", err
}
token, err := jose.ParseJWT(string(content))
if err != nil {
return "", err
// getAuthDomain makes a request to the appURL and stops at the first redirect. The 302 location header will contain the
// auth domain
func getAuthDomain(appURL *url.URL) (string, error) {
client := &http.Client{
// do not follow redirects
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
Timeout: time.Second * 7,
}
var payload jwtPayload
authDomainReq, err := http.NewRequest("HEAD", appURL.String(), nil)
if err != nil {
return "", errors.Wrap(err, "failed to create auth domain request")
}
resp, err := client.Do(authDomainReq)
if err != nil {
return "", errors.Wrap(err, "failed to get auth domain")
}
resp.Body.Close()
location, err := resp.Location()
if err != nil {
return "", fmt.Errorf("failed to get auth domain. Received status code %d from %s", resp.StatusCode, appURL.String())
}
return location.Hostname(), nil
}
// exchangeOrgToken attaches an org token to a request to the appURL and returns an app token. This uses the Access SSO
// flow to automatically generate and return an app token without the login page.
func exchangeOrgToken(appURL *url.URL, orgToken string) (string, error) {
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
// attach org token to login request
if strings.Contains(req.URL.Path, "cdn-cgi/access/login") {
req.AddCookie(&http.Cookie{Name: tokenHeader, Value: orgToken})
}
// stop after hitting authorized endpoint since it will contain the app token
if strings.Contains(via[len(via)-1].URL.Path, "cdn-cgi/access/authorized") {
return http.ErrUseLastResponse
}
return nil
},
Timeout: time.Second * 7,
}
appTokenRequest, err := http.NewRequest("HEAD", appURL.String(), nil)
if err != nil {
return "", errors.Wrap(err, "failed to create app token request")
}
resp, err := client.Do(appTokenRequest)
if err != nil {
return "", errors.Wrap(err, "failed to get app token")
}
resp.Body.Close()
var appToken string
for _, c := range resp.Cookies() {
if c.Name == tokenHeader {
appToken = c.Value
break
}
}
if len(appToken) > 0 {
return appToken, nil
}
return "", fmt.Errorf("response from %s did not contain app token", resp.Request.URL.String())
}
func GetOrgTokenIfExists(authDomain string) (string, error) {
path, err := path.GenerateOrgTokenFilePathFromURL(authDomain)
if err != nil {
return "", err
}
token, err := getTokenIfExists(path)
if err != nil {
return "", err
}
var payload orgJWTPayload
err = json.Unmarshal(token.Payload, &payload)
if err != nil {
return "", err
@@ -199,13 +331,49 @@ func GetTokenIfExists(url *url.URL) (string, error) {
err := os.Remove(path)
return "", err
}
return token.Encode(), nil
}
func GetAppTokenIfExists(url *url.URL) (string, error) {
path, err := path.GenerateAppTokenFilePathFromURL(url, keyName)
if err != nil {
return "", err
}
token, err := getTokenIfExists(path)
if err != nil {
return "", err
}
var payload appJWTPayload
err = json.Unmarshal(token.Payload, &payload)
if err != nil {
return "", err
}
if payload.isExpired() {
err := os.Remove(path)
return "", err
}
return token.Encode(), nil
}
// GetTokenIfExists will return the token from local storage if it exists and not expired
func getTokenIfExists(path string) (*jose.JWT, error) {
content, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
token, err := jose.ParseJWT(string(content))
if err != nil {
return nil, err
}
return &token, nil
}
// RemoveTokenIfExists removes the a token from local storage if it exists
func RemoveTokenIfExists(url *url.URL) error {
path, err := path.GenerateFilePathFromURL(url, keyName)
path, err := path.GenerateAppTokenFilePathFromURL(url, keyName)
if err != nil {
return err
}
+4 -7
View File
@@ -3,10 +3,8 @@ package transfer
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
@@ -15,6 +13,7 @@ import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/encrypter"
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
)
const (
@@ -28,7 +27,7 @@ const (
// The "dance" we refer to is building a HTTP request, opening that in a browser waiting for
// the user to complete an action, while it long polls in the background waiting for an
// action to be completed to download the resource.
func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncrypt bool, useHostOnly bool, logger logger.Service) ([]byte, error) {
func Run(transferURL *url.URL, resourceName, key, value string, shouldEncrypt bool, useHostOnly bool, logger logger.Service) ([]byte, error) {
encrypterClient, err := encrypter.New("cloudflared_priv.pem", "cloudflared_pub.pem")
if err != nil {
return nil, err
@@ -72,11 +71,8 @@ func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncr
resourceData = buf
}
if err := ioutil.WriteFile(path, resourceData, 0600); err != nil {
return nil, err
}
return resourceData, nil
}
// BuildRequestURL creates a request suitable for a resource transfer.
@@ -93,6 +89,7 @@ func buildRequestURL(baseURL *url.URL, key, value string, cli, useHostOnly bool)
return baseURL.String(), nil
}
q.Set("redirect_url", baseURL.String()) // we add the token as a query param on both the redirect_url and the main url
q.Set("send_org_token", "true") // indicates that the cli endpoint should return both the org and app token
baseURL.RawQuery = q.Encode() // and this actual baseURL.
baseURL.Path = "cdn-cgi/access/cli"
return baseURL.String(), nil
+27 -56
View File
@@ -77,8 +77,6 @@ const (
// uiFlag is to enable launching cloudflared in interactive UI mode
uiFlag = "ui"
logDirectoryFlag = "log-directory"
debugLevelWarning = "At debug level, request URL, method, protocol, content legnth and header will be logged. " +
"Response status, content length and header will also be logged in debug level."
)
@@ -209,40 +207,6 @@ func routeFromFlag(c *cli.Context) (tunnelstore.Route, bool) {
return nil, false
}
func createLogger(c *cli.Context, isTransport bool, disableTerminal bool) (*logger.OutputWriter, error) {
var loggerOpts []logger.Option
logPath := c.String("logfile")
if logPath == "" {
logPath = c.String(logDirectoryFlag)
}
if logPath != "" {
loggerOpts = append(loggerOpts, logger.DefaultFile(logPath))
}
logLevel := c.String("loglevel")
if isTransport {
logLevel = c.String("transport-loglevel")
if logLevel == "" {
logLevel = "fatal"
}
}
loggerOpts = append(loggerOpts, logger.LogLevelString(logLevel))
if disableTerminal {
disableOption := logger.DisableTerminal(true)
loggerOpts = append(loggerOpts, disableOption)
}
l, err := logger.New(loggerOpts...)
if err != nil {
return nil, err
}
return l, nil
}
func StartServer(
c *cli.Context,
version string,
@@ -307,18 +271,6 @@ func StartServer(
// Wait for proxy-dns to come up (if used)
<-dnsReadySignal
metricsListener, err := listeners.Listen("tcp", c.String("metrics"))
if err != nil {
generalLogger.Errorf("Error opening metrics server listener: %s", err)
return errors.Wrap(err, "Error opening metrics server listener")
}
defer metricsListener.Close()
wg.Add(1)
go func() {
defer wg.Done()
errC <- metrics.ServeMetrics(metricsListener, shutdownC, generalLogger)
}()
go notifySystemd(connectedSignal)
if c.IsSet("pidfile") {
go writePidFile(connectedSignal, c.String("pidfile"), generalLogger)
@@ -362,16 +314,35 @@ func StartServer(
return fmt.Errorf(errText)
}
transportLogger, err := createLogger(c, true, isUIEnabled)
transportLogger, err := logger.CreateTransportLoggerFromContext(c, isUIEnabled)
if err != nil {
return errors.Wrap(err, "error setting up transport logger")
}
tunnelConfig, ingressRules, err := prepareTunnelConfig(c, buildInfo, version, generalLogger, transportLogger, namedTunnel, isUIEnabled)
readinessCh := make(chan connection.Event, 16)
uiCh := make(chan connection.Event, 16)
eventChannels := []chan connection.Event{
readinessCh,
uiCh,
}
tunnelConfig, ingressRules, err := prepareTunnelConfig(c, buildInfo, version, generalLogger, transportLogger, namedTunnel, isUIEnabled, eventChannels)
if err != nil {
generalLogger.Errorf("Couldn't start tunnel: %v", err)
return err
}
metricsListener, err := listeners.Listen("tcp", c.String("metrics"))
if err != nil {
generalLogger.Errorf("Error opening metrics server listener: %s", err)
return errors.Wrap(err, "Error opening metrics server listener")
}
defer metricsListener.Close()
wg.Add(1)
go func() {
defer wg.Done()
errC <- metrics.ServeMetrics(metricsListener, shutdownC, readinessCh, generalLogger)
}()
ingressRules.StartOrigins(&wg, generalLogger, shutdownC, errC)
reconnectCh := make(chan origin.ReconnectSignal, 1)
@@ -398,7 +369,7 @@ func StartServer(
if err != nil {
return err
}
tunnelInfo.LaunchUI(ctx, generalLogger, transportLogger, logLevels, tunnelConfig.TunnelEventChan)
tunnelInfo.LaunchUI(ctx, generalLogger, transportLogger, logLevels, uiCh)
}
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, c.Duration("grace-period"), generalLogger)
@@ -415,7 +386,7 @@ func forceSetFlag(c *cli.Context, name, value string) {
func SetFlagsFromConfigFile(c *cli.Context) error {
const exitCode = 1
log, err := createLogger(c, false, false)
log, err := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
if err != nil {
return cliutil.PrintLoggerSetupError("error setting up logger", err)
}
@@ -973,14 +944,14 @@ func sshFlags(shouldHide bool) []cli.Flag {
func configureLoggingFlags(shouldHide bool) []cli.Flag {
return []cli.Flag{
altsrc.NewStringFlag(&cli.StringFlag{
Name: "loglevel",
Name: logger.LogLevelFlag,
Value: "info",
Usage: "Application logging level {fatal, error, info, debug}. " + debugLevelWarning,
EnvVars: []string{"TUNNEL_LOGLEVEL"},
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "transport-loglevel",
Name: logger.LogTransportLevelFlag,
Aliases: []string{"proto-loglevel"}, // This flag used to be called proto-loglevel
Value: "info",
Usage: "Transport logging level(previously called protocol logging level) {fatal, error, info, debug}",
@@ -988,13 +959,13 @@ func configureLoggingFlags(shouldHide bool) []cli.Flag {
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "logfile",
Name: logger.LogFileFlag,
Usage: "Save application log to this file for reporting issues.",
EnvVars: []string{"TUNNEL_LOGFILE"},
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: logDirectoryFlag,
Name: logger.LogDirectoryFlag,
Usage: "Save application log to this directory for reporting issues.",
EnvVars: []string{"TUNNEL_LOGDIRECTORY"},
Hidden: shouldHide,
+4 -9
View File
@@ -10,7 +10,6 @@ import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/cmd/cloudflared/ui"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/edgediscovery"
"github.com/cloudflare/cloudflared/h2mux"
@@ -160,7 +159,8 @@ func prepareTunnelConfig(
logger logger.Service,
transportLogger logger.Service,
namedTunnel *connection.NamedTunnelConfig,
uiIsEnabled bool,
isUIEnabled bool,
eventChans []chan connection.Event,
) (*origin.TunnelConfig, ingress.Ingress, error) {
isNamedTunnel := namedTunnel != nil
@@ -261,11 +261,6 @@ func prepareTunnelConfig(
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
}
var tunnelEventChan chan ui.TunnelEvent
if uiIsEnabled {
tunnelEventChan = make(chan ui.TunnelEvent, 16)
}
return &origin.TunnelConfig{
ConnectionConfig: connectionConfig,
BuildInfo: buildInfo,
@@ -278,14 +273,14 @@ func prepareTunnelConfig(
LBPool: c.String("lb-pool"),
Tags: tags,
Logger: logger,
Observer: connection.NewObserver(transportLogger, tunnelEventChan),
Observer: connection.NewObserver(transportLogger, eventChans, isUIEnabled),
ReportedVersion: version,
Retries: c.Uint("retries"),
RunFromTerminal: isRunningFromTerminal(),
NamedTunnel: namedTunnel,
ClassicTunnel: classicTunnel,
MuxerConfig: muxerConfig,
TunnelEventChan: tunnelEventChan,
TunnelEventChans: eventChans,
ProtocolSelector: protocolSelector,
EdgeTLSConfigs: edgeTLSConfigs,
}, ingressRules, nil
@@ -0,0 +1,78 @@
package tunnel
import (
"fmt"
"path/filepath"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
"github.com/google/uuid"
"github.com/urfave/cli/v2"
)
// CredFinder can find the tunnel credentials file.
type CredFinder interface {
Path() (string, error)
}
// Implements CredFinder and looks for the credentials file at the given
// filepath.
type staticPath struct {
filePath string
fs fileSystem
}
func newStaticPath(filePath string, fs fileSystem) CredFinder {
return staticPath{
filePath: filePath,
fs: fs,
}
}
func (a staticPath) Path() (string, error) {
if a.filePath != "" && a.fs.validFilePath(a.filePath) {
return a.filePath, nil
}
return "", fmt.Errorf("Tunnel credentials file '%s' doesn't exist or is not a file", a.filePath)
}
// Implements CredFinder and looks for the credentials file in several directories
// searching for a file named <id>.json
type searchByID struct {
id uuid.UUID
c *cli.Context
logger logger.Service
fs fileSystem
}
func newSearchByID(id uuid.UUID, c *cli.Context, logger logger.Service, fs fileSystem) CredFinder {
return searchByID{
id: id,
c: c,
logger: logger,
fs: fs,
}
}
func (s searchByID) Path() (string, error) {
// Fallback to look for tunnel credentials in the origin cert directory
if originCertPath, err := findOriginCert(s.c, s.logger); err == nil {
originCertDir := filepath.Dir(originCertPath)
if filePath, err := tunnelFilePath(s.id, originCertDir); err == nil {
if s.fs.validFilePath(filePath) {
return filePath, nil
}
}
}
// Last resort look under default config directories
for _, configDir := range config.DefaultConfigSearchDirectories() {
if filePath, err := tunnelFilePath(s.id, configDir); err == nil {
if s.fs.validFilePath(filePath) {
return filePath, nil
}
}
}
return "", fmt.Errorf("Tunnel credentials file not found")
}
+27
View File
@@ -0,0 +1,27 @@
package tunnel
import (
"io/ioutil"
"os"
)
// Abstract away details of reading files, so that SubcommandContext can read
// from either the real filesystem, or a mock (when running unit tests).
type fileSystem interface {
readFile(filePath string) ([]byte, error)
validFilePath(path string) bool
}
type realFileSystem struct{}
func (fs realFileSystem) validFilePath(path string) bool {
fileStat, err := os.Stat(path)
if err != nil {
return false
}
return !fileStat.IsDir()
}
func (fs realFileSystem) readFile(filePath string) ([]byte, error) {
return ioutil.ReadFile(filePath)
}
+7 -2
View File
@@ -2,6 +2,7 @@ package tunnel
import (
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
@@ -39,7 +40,7 @@ func buildLoginSubcommand(hidden bool) *cli.Command {
}
func login(c *cli.Context) error {
logger, err := logger.New()
logger, err := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
@@ -58,12 +59,16 @@ func login(c *cli.Context) error {
return err
}
_, err = transfer.Run(loginURL, "cert", "callback", callbackStoreURL, path, false, false, logger)
resourceData, err := transfer.Run(loginURL, "cert", "callback", callbackStoreURL, false, false, logger)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to write the certificate due to the following error:\n%v\n\nYour browser will download the certificate instead. You will have to manually\ncopy it to the following path:\n\n%s\n", err, path)
return err
}
if err := ioutil.WriteFile(path, resourceData, 0600); err != nil {
return errors.Wrap(err, fmt.Sprintf("error writing cert to %s", path))
}
fmt.Fprintf(os.Stdout, "You have successfully logged in.\nIf you wish to copy your credentials to a server, they have been saved to:\n%s\n", path)
return nil
}
+66 -63
View File
@@ -3,9 +3,7 @@ package tunnel
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/google/uuid"
@@ -13,10 +11,8 @@ import (
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/certutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/cloudflare/cloudflared/tunnelstore"
)
@@ -32,21 +28,21 @@ func (e errInvalidJSONCredential) Error() string {
// subcommandContext carries structs shared between subcommands, to reduce number of arguments needed to
// pass between subcommands, and make sure they are only initialized once
type subcommandContext struct {
c *cli.Context
logger logger.Service
c *cli.Context
logger logger.Service
isUIEnabled bool
fs fileSystem
// These fields should be accessed using their respective Getter
tunnelstoreClient tunnelstore.Client
userCredential *userCredential
isUIEnabled bool
}
func newSubcommandContext(c *cli.Context) (*subcommandContext, error) {
isUIEnabled := c.IsSet(uiFlag) && c.String("name") != ""
// If UI is enabled, terminal log output should be disabled -- log should be written into a UI log window instead
logger, err := createLogger(c, false, isUIEnabled)
logger, err := logger.CreateLoggerFromContext(c, isUIEnabled)
if err != nil {
return nil, errors.Wrap(err, "error setting up logger")
}
@@ -55,9 +51,18 @@ func newSubcommandContext(c *cli.Context) (*subcommandContext, error) {
c: c,
logger: logger,
isUIEnabled: isUIEnabled,
fs: realFileSystem{},
}, nil
}
// 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)
}
return newSearchByID(tunnelID, sc.c, sc.logger, sc.fs)
}
type userCredential struct {
cert *certutil.OriginCert
certPath string
@@ -108,56 +113,27 @@ func (sc *subcommandContext) credential() (*userCredential, error) {
return sc.userCredential, nil
}
func (sc *subcommandContext) readTunnelCredentials(tunnelID uuid.UUID) (*pogs.TunnelAuth, error) {
filePath, err := sc.tunnelCredentialsPath(tunnelID)
func (sc *subcommandContext) readTunnelCredentials(credFinder CredFinder) (connection.Credentials, error) {
filePath, err := credFinder.Path()
if err != nil {
return nil, err
return connection.Credentials{}, err
}
body, err := ioutil.ReadFile(filePath)
body, err := sc.fs.readFile(filePath)
if err != nil {
return nil, errors.Wrapf(err, "couldn't read tunnel credentials from %v", filePath)
return connection.Credentials{}, errors.Wrapf(err, "couldn't read tunnel credentials from %v", filePath)
}
var auth pogs.TunnelAuth
if err = json.Unmarshal(body, &auth); err != nil {
var credentials connection.Credentials
if err = json.Unmarshal(body, &credentials); err != nil {
if strings.HasSuffix(filePath, ".pem") {
return nil, 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` and named %s.json."+
"You may have accidentally used the filepath to cert.pem, which is generated by `cloudflared tunnel "+
"login`.", tunnelID)
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`.")
}
return nil, errInvalidJSONCredential{path: filePath, err: err}
return connection.Credentials{}, errInvalidJSONCredential{path: filePath, err: err}
}
return &auth, nil
}
func (sc *subcommandContext) tunnelCredentialsPath(tunnelID uuid.UUID) (string, error) {
if filePath := sc.c.String("credentials-file"); filePath != "" {
if validFilePath(filePath) {
return filePath, nil
}
return "", fmt.Errorf("Tunnel credentials file %s doesn't exist or is not a file", filePath)
}
// Fallback to look for tunnel credentials in the origin cert directory
if originCertPath, err := findOriginCert(sc.c, sc.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.DefaultConfigSearchDirectories() {
if filePath, err := tunnelFilePath(tunnelID, configDir); err == nil {
if validFilePath(filePath) {
return filePath, nil
}
}
}
return "", fmt.Errorf("Tunnel credentials file not found")
return credentials, nil
}
func (sc *subcommandContext) create(name string) (*tunnelstore.Tunnel, error) {
@@ -180,7 +156,14 @@ func (sc *subcommandContext) create(name string) (*tunnelstore.Tunnel, error) {
if err != nil {
return nil, err
}
if writeFileErr := writeTunnelCredentials(tunnel.ID, credential.cert.AccountID, credential.certPath, tunnelSecret, sc.logger); err != nil {
tunnelCredentials := connection.Credentials{
AccountTag: credential.cert.AccountID,
TunnelSecret: tunnelSecret,
TunnelID: tunnel.ID,
TunnelName: name,
}
filePath, writeFileErr := writeTunnelCredentials(credential.certPath, &tunnelCredentials)
if 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))
@@ -193,6 +176,7 @@ func (sc *subcommandContext) create(name string) (*tunnelstore.Tunnel, error) {
errorMsg := strings.Join(errorLines, "\n")
return nil, errors.New(errorMsg)
}
sc.logger.Infof("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)
if outputFormat := sc.c.String(outputFormatFlag.Name); outputFormat != "" {
return nil, renderOutput(outputFormat, &tunnel)
@@ -243,21 +227,31 @@ func (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error {
return errors.Wrapf(err, "Error deleting tunnel %s", tunnel.ID)
}
tunnelCredentialsPath, err := sc.tunnelCredentialsPath(tunnel.ID)
if err != nil {
sc.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 {
sc.logger.Infof("Cannot delete tunnel credentials, error: %v. Please delete the file manually", err)
credFinder := sc.credentialFinder(id)
if tunnelCredentialsPath, err := credFinder.Path(); err == nil {
if err = os.Remove(tunnelCredentialsPath); err != nil {
sc.logger.Infof("Tunnel %v was deleted, but we could not remove its credentials file %s: %s. Consider deleting this file manually.", id, tunnelCredentialsPath, err)
}
}
}
return nil
}
// 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
}
func (sc *subcommandContext) run(tunnelID uuid.UUID) error {
credentials, err := sc.readTunnelCredentials(tunnelID)
credentials, err := sc.findCredentials(tunnelID)
if err != nil {
if e, ok := err.(errInvalidJSONCredential); ok {
sc.logger.Errorf("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)
@@ -265,13 +259,12 @@ func (sc *subcommandContext) run(tunnelID uuid.UUID) error {
}
return err
}
return StartServer(
sc.c,
version,
shutdownC,
graceShutdownC,
&connection.NamedTunnelConfig{Auth: *credentials, ID: tunnelID},
&connection.NamedTunnelConfig{Credentials: credentials},
sc.logger,
sc.isUIEnabled,
)
@@ -300,6 +293,7 @@ func (sc *subcommandContext) route(tunnelID uuid.UUID, r tunnelstore.Route) (tun
return client.RouteTunnel(tunnelID, r)
}
// Query Tunnelstore to find the active tunnel with the given name.
func (sc *subcommandContext) tunnelActive(name string) (*tunnelstore.Tunnel, bool, error) {
filter := tunnelstore.NewFilter()
filter.NoDeleted()
@@ -322,6 +316,15 @@ func (sc *subcommandContext) findID(input string) (uuid.UUID, error) {
return u, nil
}
// 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.
if tunnel, found, err := sc.tunnelActive(input); err != nil {
return uuid.Nil, err
} else if found {
@@ -1,11 +1,20 @@
package tunnel
import (
"encoding/base64"
"flag"
"fmt"
"reflect"
"testing"
"time"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tunnelstore"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
)
func Test_findIDs(t *testing.T) {
@@ -80,3 +89,266 @@ func Test_findIDs(t *testing.T) {
})
}
}
type mockFileSystem struct {
rf func(string) ([]byte, error)
vfp func(string) bool
}
func (fs mockFileSystem) validFilePath(path string) bool {
return fs.vfp(path)
}
func (fs mockFileSystem) readFile(filePath string) ([]byte, error) {
return fs.rf(filePath)
}
func Test_subcommandContext_findCredentials(t *testing.T) {
type fields struct {
c *cli.Context
logger logger.Service
isUIEnabled bool
fs fileSystem
tunnelstoreClient tunnelstore.Client
userCredential *userCredential
}
type args struct {
tunnelID uuid.UUID
}
oldCertPath := "old_cert.json"
newCertPath := "new_cert.json"
accountTag := "0000d4d14e84bd4ae5a6a02e0000ac63"
secret := []byte{211, 79, 177, 245, 179, 194, 152, 127, 140, 71, 18, 46, 183, 209, 10, 24, 192, 150, 55, 249, 211, 16, 167, 30, 113, 51, 152, 168, 72, 100, 205, 144}
secretB64 := base64.StdEncoding.EncodeToString(secret)
tunnelID := uuid.MustParse("df5ed608-b8b4-4109-89f3-9f2cf199df64")
name := "mytunnel"
fs := mockFileSystem{
rf: func(filePath string) ([]byte, error) {
if filePath == oldCertPath {
// An old credentials file created before TUN-3581 added the new fields
return []byte(fmt.Sprintf(`{"AccountTag":"%s","TunnelSecret":"%s"}`, accountTag, secretB64)), nil
}
if filePath == newCertPath {
// A new credentials file created after TUN-3581 with its new fields.
return []byte(fmt.Sprintf(`{"AccountTag":"%s","TunnelSecret":"%s","TunnelID":"%s","TunnelName":"%s"}`, accountTag, secretB64, tunnelID, name)), nil
}
return nil, errors.New("file not found")
},
vfp: func(string) bool { return true },
}
logger, err := logger.New()
require.NoError(t, err)
tests := []struct {
name string
fields fields
args args
want connection.Credentials
wantErr bool
}{
{
name: "Filepath given leads to old credentials file",
fields: fields{
logger: logger,
fs: fs,
c: func() *cli.Context {
flagSet := flag.NewFlagSet("test0", flag.PanicOnError)
flagSet.String(CredFileFlag, oldCertPath, "")
c := cli.NewContext(cli.NewApp(), flagSet, nil)
err = c.Set(CredFileFlag, oldCertPath)
return c
}(),
},
args: args{
tunnelID: tunnelID,
},
want: connection.Credentials{
AccountTag: accountTag,
TunnelID: tunnelID,
TunnelSecret: secret,
},
},
{
name: "Filepath given leads to new credentials file",
fields: fields{
logger: logger,
fs: fs,
c: func() *cli.Context {
flagSet := flag.NewFlagSet("test0", flag.PanicOnError)
flagSet.String(CredFileFlag, newCertPath, "")
c := cli.NewContext(cli.NewApp(), flagSet, nil)
err = c.Set(CredFileFlag, newCertPath)
return c
}(),
},
args: args{
tunnelID: tunnelID,
},
want: connection.Credentials{
AccountTag: accountTag,
TunnelID: tunnelID,
TunnelSecret: secret,
TunnelName: name,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sc := &subcommandContext{
c: tt.fields.c,
logger: tt.fields.logger,
isUIEnabled: tt.fields.isUIEnabled,
fs: tt.fields.fs,
tunnelstoreClient: tt.fields.tunnelstoreClient,
userCredential: tt.fields.userCredential,
}
got, err := sc.findCredentials(tt.args.tunnelID)
if (err != nil) != tt.wantErr {
t.Errorf("subcommandContext.findCredentials() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("subcommandContext.findCredentials() = %v, want %v", got, tt.want)
}
})
}
}
type deleteMockTunnelStore struct {
tunnelstore.Client
mockTunnels map[uuid.UUID]mockTunnelBehaviour
deletedTunnelIDs []uuid.UUID
}
type mockTunnelBehaviour struct {
tunnel tunnelstore.Tunnel
deleteErr error
cleanupErr error
}
func newDeleteMockTunnelStore(tunnels ...mockTunnelBehaviour) *deleteMockTunnelStore {
mockTunnels := make(map[uuid.UUID]mockTunnelBehaviour)
for _, tunnel := range tunnels {
mockTunnels[tunnel.tunnel.ID] = tunnel
}
return &deleteMockTunnelStore{
mockTunnels: mockTunnels,
deletedTunnelIDs: make([]uuid.UUID, 0),
}
}
func (d *deleteMockTunnelStore) GetTunnel(tunnelID uuid.UUID) (*tunnelstore.Tunnel, error) {
tunnel, ok := d.mockTunnels[tunnelID]
if !ok {
return nil, fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
}
return &tunnel.tunnel, nil
}
func (d *deleteMockTunnelStore) DeleteTunnel(tunnelID uuid.UUID) error {
tunnel, ok := d.mockTunnels[tunnelID]
if !ok {
return fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
}
if tunnel.deleteErr != nil {
return tunnel.deleteErr
}
d.deletedTunnelIDs = append(d.deletedTunnelIDs, tunnelID)
tunnel.tunnel.DeletedAt = time.Now()
delete(d.mockTunnels, tunnelID)
return nil
}
func (d *deleteMockTunnelStore) CleanupConnections(tunnelID uuid.UUID) error {
tunnel, ok := d.mockTunnels[tunnelID]
if !ok {
return fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
}
return tunnel.cleanupErr
}
func Test_subcommandContext_Delete(t *testing.T) {
type fields struct {
c *cli.Context
logger logger.Service
isUIEnabled bool
fs fileSystem
tunnelstoreClient *deleteMockTunnelStore
userCredential *userCredential
}
type args struct {
tunnelIDs []uuid.UUID
}
newCertPath := "new_cert.json"
tunnelID1 := uuid.MustParse("df5ed608-b8b4-4109-89f3-9f2cf199df64")
tunnelID2 := uuid.MustParse("af5ed608-b8b4-4109-89f3-9f2cf199df64")
logger, err := logger.New()
require.NoError(t, err)
var tests = []struct {
name string
fields fields
args args
want []uuid.UUID
wantErr bool
}{
{
name: "clean up continues if credentials are not found",
fields: fields{
logger: logger,
fs: mockFileSystem{
rf: func(filePath string) ([]byte, error) {
return nil, errors.New("file not found")
},
vfp: func(string) bool { return true },
},
c: func() *cli.Context {
flagSet := flag.NewFlagSet("test0", flag.PanicOnError)
flagSet.String(CredFileFlag, newCertPath, "")
c := cli.NewContext(cli.NewApp(), flagSet, nil)
err = c.Set(CredFileFlag, newCertPath)
return c
}(),
tunnelstoreClient: newDeleteMockTunnelStore(
mockTunnelBehaviour{
tunnel: tunnelstore.Tunnel{ID: tunnelID1},
},
mockTunnelBehaviour{
tunnel: tunnelstore.Tunnel{ID: tunnelID2},
},
),
},
args: args{
tunnelIDs: []uuid.UUID{tunnelID1, tunnelID2},
},
want: []uuid.UUID{tunnelID1, tunnelID2},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sc := &subcommandContext{
c: tt.fields.c,
logger: tt.fields.logger,
isUIEnabled: tt.fields.isUIEnabled,
fs: tt.fields.fs,
tunnelstoreClient: tt.fields.tunnelstoreClient,
userCredential: tt.fields.userCredential,
}
err := sc.delete(tt.args.tunnelIDs)
if (err != nil) != tt.wantErr {
t.Errorf("subcommandContext.findCredentials() error = %v, wantErr %v", err, tt.wantErr)
return
}
got := tt.fields.tunnelstoreClient.deletedTunnelIDs
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("subcommandContext.findCredentials() = %v, want %v", got, tt.want)
return
}
})
}
}
+31 -37
View File
@@ -24,13 +24,12 @@ import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/cloudflare/cloudflared/tunnelstore"
)
const (
credFileFlagAlias = "cred-file"
CredFileFlagAlias = "cred-file"
CredFileFlag = "credentials-file"
)
var (
@@ -75,8 +74,8 @@ var (
"tunnels, you can do so with Cloudflare's Load Balancer product.",
})
credentialsFileFlag = altsrc.NewStringFlag(&cli.StringFlag{
Name: "credentials-file",
Aliases: []string{credFileFlagAlias},
Name: CredFileFlag,
Aliases: []string{CredFileFlagAlias},
Usage: "File path of tunnel credentials",
EnvVars: []string{"TUNNEL_CRED_FILE"},
})
@@ -103,7 +102,7 @@ func buildCreateCommand() *cli.Command {
Usage: "Create a new tunnel with given name",
UsageText: "cloudflared tunnel [tunnel command options] create [subcommand options] NAME",
Description: `Creates a tunnel, registers it with Cloudflare edge and generates credential file used to run this tunnel.
Use "cloudflared tunnel route" subcommand to map a DNS name to this tunnel and "cloudflared tunnel run" to start the connection.
Use "cloudflared tunnel route" subcommand to map a DNS name to this tunnel and "cloudflared tunnel run" to start the connection.
For example, to create a tunnel named 'my-tunnel' run:
@@ -141,30 +140,21 @@ func tunnelFilePath(tunnelID uuid.UUID, directory string) (string, error) {
return homedir.Expand(filePath)
}
func writeTunnelCredentials(tunnelID uuid.UUID, accountID, originCertPath string, tunnelSecret []byte, logger logger.Service) error {
func writeTunnelCredentials(
originCertPath string,
credentials *connection.Credentials,
) (filePath string, err error) {
originCertDir := filepath.Dir(originCertPath)
filePath, err := tunnelFilePath(tunnelID, originCertDir)
filePath, err = tunnelFilePath(credentials.TunnelID, originCertDir)
if err != nil {
return err
return "", err
}
body, err := json.Marshal(pogs.TunnelAuth{
AccountTag: accountID,
TunnelSecret: tunnelSecret,
})
// Write the name and ID to the file too
body, err := json.Marshal(credentials)
if err != nil {
return errors.Wrap(err, "Unable to marshal tunnel credentials to JSON")
return "", errors.Wrap(err, "Unable to marshal tunnel credentials to JSON")
}
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)
}
func validFilePath(path string) bool {
fileStat, err := os.Stat(path)
if err != nil {
return false
}
return !fileStat.IsDir()
return filePath, ioutil.WriteFile(filePath, body, 400)
}
func buildListCommand() *cli.Command {
@@ -329,13 +319,13 @@ func buildRunCommand() *cli.Command {
Before: SetFlagsFromConfigFile,
Usage: "Proxy a local web server by running the given tunnel",
UsageText: "cloudflared tunnel [tunnel command options] run [subcommand options] [TUNNEL]",
Description: `Runs the tunnel identified by name or UUUD, creating highly available connections
Description: `Runs the tunnel identified by name or UUUD, creating highly available connections
between your server and the Cloudflare edge. You can provide name or UUID of tunnel to run either as the
last command line argument or in the configuration file using "tunnel: TUNNEL".
This command requires the tunnel credentials file created when "cloudflared tunnel create" was run,
This command requires the tunnel credentials file created when "cloudflared tunnel create" was run,
however it does not need access to cert.pem from "cloudflared login" if you identify the tunnel by UUID.
If you experience other problems running the tunnel, "cloudflared tunnel cleanup" may help by removing
If you experience other problems running the tunnel, "cloudflared tunnel cleanup" may help by removing
any old connection records.
`,
Flags: flags,
@@ -431,7 +421,7 @@ func dnsRouteFromArg(c *cli.Context) (tunnelstore.Route, error) {
userHostname := c.Args().Get(userHostnameIndex)
if userHostname == "" {
return nil, cliutil.UsageError("The third argument should be the hostname")
} else if !validateHostname(userHostname) {
} else if !validateHostname(userHostname, true) {
return nil, errors.Errorf("%s is not a valid hostname", userHostname)
}
return tunnelstore.NewDNSRoute(userHostname), nil
@@ -449,14 +439,14 @@ func lbRouteFromArg(c *cli.Context) (tunnelstore.Route, error) {
lbName := c.Args().Get(lbNameIndex)
if lbName == "" {
return nil, cliutil.UsageError("The third argument should be the load balancer name")
} else if !validateHostname(lbName) {
} else if !validateHostname(lbName, true) {
return nil, errors.Errorf("%s is not a valid load balancer name", lbName)
}
lbPool := c.Args().Get(lbPoolIndex)
if lbPool == "" {
return nil, cliutil.UsageError("The fourth argument should be the pool name")
} else if !validateName(lbPool) {
} else if !validateName(lbPool, false) {
return nil, errors.Errorf("%s is not a valid pool name", lbPool)
}
@@ -464,19 +454,23 @@ func lbRouteFromArg(c *cli.Context) (tunnelstore.Route, error) {
}
var nameRegex = regexp.MustCompile("^[_a-zA-Z0-9][-_.a-zA-Z0-9]*$")
var hostNameRegex = regexp.MustCompile("^[*_a-zA-Z0-9][-_.a-zA-Z0-9]*$")
func validateName(s string) bool {
func validateName(s string, allowWildcardSubdomain bool) bool {
if allowWildcardSubdomain {
return hostNameRegex.MatchString(s)
}
return nameRegex.MatchString(s)
}
func validateHostname(s string) bool {
func validateHostname(s string, allowWildcardSubdomain bool) bool {
// Slightly stricter than PunyCodeProfile
idnaProfile := idna.New(
idna.ValidateLabels(true),
idna.VerifyDNSLength(true))
puny, err := idnaProfile.ToASCII(s)
return err == nil && validateName(puny)
return err == nil && validateName(puny, allowWildcardSubdomain)
}
func routeCommand(c *cli.Context) error {
@@ -535,13 +529,13 @@ func commandHelpTemplate() string {
}
const template = `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{.UsageText}}
DESCRIPTION:
{{.Description}}
TUNNEL COMMAND OPTIONS:
%s
SUBCOMMAND OPTIONS:
+59 -4
View File
@@ -6,9 +6,8 @@ import (
"testing"
"github.com/cloudflare/cloudflared/tunnelstore"
"github.com/mitchellh/go-homedir"
"github.com/google/uuid"
"github.com/mitchellh/go-homedir"
"github.com/stretchr/testify/assert"
)
@@ -117,8 +116,64 @@ func TestValidateName(t *testing.T) {
{name: "_ab_c.-d-ef", want: true},
}
for _, tt := range tests {
if got := validateName(tt.name); got != tt.want {
if got := validateName(tt.name, false); got != tt.want {
t.Errorf("validateName() = %v, want %v", got, tt.want)
}
}
}
}
func Test_validateHostname(t *testing.T) {
type args struct {
s string
allowWildcardSubdomain bool
}
tests := []struct {
name string
args args
want bool
}{
{
name: "Normal",
args: args{
s: "example.com",
allowWildcardSubdomain: true,
},
want: true,
},
{
name: "wildcard subdomain for TUN-358",
args: args{
s: "*.ehrig.io",
allowWildcardSubdomain: true,
},
want: true,
},
{
name: "Misplaced wildcard",
args: args{
s: "subdomain.*.ehrig.io",
allowWildcardSubdomain: true,
},
},
{
name: "Invalid domain",
args: args{
s: "..",
allowWildcardSubdomain: true,
},
},
{
name: "Invalid domain",
args: args{
s: "..",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := validateHostname(tt.args.s, tt.args.allowWildcardSubdomain); got != tt.want {
t.Errorf("validateHostname() = %v, want %v", got, tt.want)
}
})
}
}
+20 -36
View File
@@ -6,6 +6,7 @@ import (
"strings"
"time"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/logger"
@@ -15,24 +16,7 @@ import (
type connState struct {
location string
state status
}
type status int
const (
Disconnected status = iota
Connected
Reconnecting
SetUrl
RegisteringTunnel
)
type TunnelEvent struct {
Index uint8
EventType status
Location string
Url string
state connection.Status
}
type uiModel struct {
@@ -69,7 +53,7 @@ func (data *uiModel) LaunchUI(
ctx context.Context,
generalLogger, transportLogger logger.Service,
logLevels []logger.Level,
tunnelEventChan <-chan TunnelEvent,
tunnelEventChan <-chan connection.Event,
) {
// Configure the logger to stream logs into the textview
@@ -138,14 +122,14 @@ func (data *uiModel) LaunchUI(
return
case event := <-tunnelEventChan:
switch event.EventType {
case Connected:
case connection.Connected:
data.setConnTableCell(event, connTable, palette)
case Disconnected, Reconnecting:
case connection.Disconnected, connection.Reconnecting:
data.changeConnStatus(event, connTable, generalLogger, palette)
case SetUrl:
tunnelHostText.SetText(event.Url)
data.edgeURL = event.Url
case RegisteringTunnel:
case connection.SetURL:
tunnelHostText.SetText(event.URL)
data.edgeURL = event.URL
case connection.RegisteringTunnel:
if data.edgeURL == "" {
tunnelHostText.SetText("Registering tunnel...")
}
@@ -175,7 +159,7 @@ func handleNewText(app *tview.Application, logTextView *tview.TextView) func() {
}
}
func (data *uiModel) changeConnStatus(event TunnelEvent, table *tview.Table, logger logger.Service, palette palette) {
func (data *uiModel) changeConnStatus(event connection.Event, table *tview.Table, logger logger.Service, palette palette) {
index := int(event.Index)
// Get connection location and state
connState := data.getConnState(index)
@@ -187,10 +171,10 @@ func (data *uiModel) changeConnStatus(event TunnelEvent, table *tview.Table, log
locationState := event.Location
if event.EventType == Disconnected {
connState.state = Disconnected
} else if event.EventType == Reconnecting {
connState.state = Reconnecting
if event.EventType == connection.Disconnected {
connState.state = connection.Disconnected
} else if event.EventType == connection.Reconnecting {
connState.state = connection.Reconnecting
locationState = "Reconnecting..."
}
@@ -211,12 +195,12 @@ func (data *uiModel) getConnState(connID int) *connState {
return nil
}
func (data *uiModel) setConnTableCell(event TunnelEvent, table *tview.Table, palette palette) {
func (data *uiModel) setConnTableCell(event connection.Event, table *tview.Table, palette palette) {
index := int(event.Index)
connectionNum := index + 1
// Update slice to keep track of connection location and state in UI table
data.connections[index].state = Connected
data.connections[index].state = connection.Connected
data.connections[index].location = event.Location
// Update text in table cell to show disconnected state
@@ -225,18 +209,18 @@ func (data *uiModel) setConnTableCell(event TunnelEvent, table *tview.Table, pal
table.SetCell(index, 0, cell)
}
func newCellText(palette palette, connectionNum int, location string, connectedStatus status) string {
func newCellText(palette palette, connectionNum int, location string, connectedStatus connection.Status) string {
// HA connection indicator formatted as: "• #<CONNECTION_INDEX>: <COLO>",
// where the left middle dot's color depends on the status of the connection
const connFmtString = "[%s]\u2022[%s] #%d: %s"
var dotColor string
switch connectedStatus {
case Connected:
case connection.Connected:
dotColor = palette.connected
case Disconnected:
case connection.Disconnected:
dotColor = palette.disconnected
case Reconnecting:
case connection.Reconnecting:
dotColor = palette.reconnecting
}
+1 -2
View File
@@ -114,7 +114,7 @@ func checkForUpdateAndApply(options updateOptions) UpdateOutcome {
// Update is the handler for the update command from the command line
func Update(c *cli.Context) error {
logger, err := logger.New()
logger, err := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
@@ -202,7 +202,6 @@ func (a *AutoUpdater) Run(ctx context.Context) error {
if a.configurable.enabled {
updateOutcome := loggedUpdate(a.logger, updateOptions{})
if updateOutcome.Updated {
os.Args = append(os.Args, "--is-autoupdated=true")
if IsSysV() {
// SysV doesn't have a mechanism to keep service alive, we have to restart the process
a.logger.Info("Restarting service managed by SysV...")
+2 -2
View File
@@ -173,7 +173,7 @@ func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeReques
}
func installWindowsService(c *cli.Context) error {
logger, err := logger.New()
logger, err := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
@@ -219,7 +219,7 @@ func installWindowsService(c *cli.Context) error {
}
func uninstallWindowsService(c *cli.Context) error {
logger, err := logger.New()
logger, err := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
+24 -7
View File
@@ -22,9 +22,23 @@ type Config struct {
}
type NamedTunnelConfig struct {
Auth pogs.TunnelAuth
ID uuid.UUID
Client pogs.ClientInfo
Credentials Credentials
Client pogs.ClientInfo
}
// Credentials are stored in the credentials file and contain all info needed to run a tunnel.
type Credentials struct {
AccountTag string
TunnelSecret []byte
TunnelID uuid.UUID
TunnelName string
}
func (c *Credentials) Auth() pogs.TunnelAuth {
return pogs.TunnelAuth{
AccountTag: c.AccountTag,
TunnelSecret: c.TunnelSecret,
}
}
type ClassicTunnelConfig struct {
@@ -53,10 +67,13 @@ type ConnectedFuse interface {
IsConnected() bool
}
func IsServerSentEvent(headers http.Header) bool {
if contentType := headers.Get("content-type"); contentType != "" {
return strings.HasPrefix(strings.ToLower(contentType), "text/event-stream")
}
return false
}
func uint8ToString(input uint8) string {
return strconv.FormatUint(uint64(input), 10)
}
func isServerSentEvent(headers http.Header) bool {
return strings.ToLower(headers.Get("content-type")) == "text/event-stream"
}
+42 -3
View File
@@ -5,11 +5,12 @@ import (
"io"
"net/http"
"net/url"
"testing"
"time"
"github.com/cloudflare/cloudflared/cmd/cloudflared/ui"
"github.com/cloudflare/cloudflared/logger"
"github.com/gobwas/ws/wsutil"
"github.com/stretchr/testify/assert"
)
const (
@@ -26,11 +27,12 @@ var (
Scheme: "https",
Host: "connectiontest.argotunnel.com",
}
testTunnelEventChan = make(chan ui.TunnelEvent)
testTunnelEventChan = make(chan Event)
testObserver = &Observer{
testLogger,
m,
testTunnelEventChan,
[]chan Event{testTunnelEventChan},
false,
}
testLargeResp = make([]byte, largeFileSize)
)
@@ -111,3 +113,40 @@ func (mcf mockConnectedFuse) Connected() {}
func (mcf mockConnectedFuse) IsConnected() bool {
return true
}
func TestIsEventStream(t *testing.T) {
tests := []struct {
headers http.Header
isEventStream bool
}{
{
headers: newHeader("Content-Type", "text/event-stream"),
isEventStream: true,
},
{
headers: newHeader("content-type", "text/event-stream"),
isEventStream: true,
},
{
headers: newHeader("Content-Type", "text/event-stream; charset=utf-8"),
isEventStream: true,
},
{
headers: newHeader("Content-Type", "application/json"),
isEventStream: false,
},
{
headers: http.Header{},
isEventStream: false,
},
}
for _, test := range tests {
assert.Equal(t, test.isEventStream, IsServerSentEvent(test.headers))
}
}
func newHeader(key, value string) http.Header {
header := http.Header{}
header.Add(key, value)
return header
}
+25
View File
@@ -0,0 +1,25 @@
package connection
// Event is something that happened to a connection, e.g. disconnection or registration.
type Event struct {
Index uint8
EventType Status
Location string
URL string
}
// Status is the status of a connection.
type Status int
const (
// Disconnected means the connection to the edge was broken.
Disconnected Status = iota
// Connected means the connection to the edge was successfully established.
Connected
// Reconnecting means the connection to the edge is being re-established.
Reconnecting
// SetURL means this connection's tunnel was given a URL by the edge. Used for free tunnels.
SetURL
// RegisteringTunnel means the non-named tunnel is registering its connection.
RegisteringTunnel
)
+2 -2
View File
@@ -207,14 +207,14 @@ type h2muxRespWriter struct {
func (rp *h2muxRespWriter) WriteRespHeaders(resp *http.Response) error {
headers := h2mux.H1ResponseToH2ResponseHeaders(resp)
headers = append(headers, h2mux.Header{Name: responseMetaHeaderField, Value: responseMetaHeaderOrigin})
headers = append(headers, h2mux.Header{Name: ResponseMetaHeaderField, Value: responseMetaHeaderOrigin})
return rp.WriteHeaders(headers)
}
func (rp *h2muxRespWriter) WriteErrorResponse() {
rp.WriteHeaders([]h2mux.Header{
{Name: ":status", Value: "502"},
{Name: responseMetaHeaderField, Value: responseMetaHeaderCfd},
{Name: ResponseMetaHeaderField, Value: responseMetaHeaderCfd},
})
rp.Write([]byte("502 Bad Gateway"))
}
+4 -4
View File
@@ -105,9 +105,9 @@ func TestServeStreamHTTP(t *testing.T) {
require.True(t, hasHeader(stream, ":status", strconv.Itoa(test.expectedStatus)))
if test.isProxyError {
assert.True(t, hasHeader(stream, responseMetaHeaderField, responseMetaHeaderCfd))
assert.True(t, hasHeader(stream, ResponseMetaHeaderField, responseMetaHeaderCfd))
} else {
assert.True(t, hasHeader(stream, responseMetaHeaderField, responseMetaHeaderOrigin))
assert.True(t, hasHeader(stream, ResponseMetaHeaderField, responseMetaHeaderOrigin))
body := make([]byte, len(test.expectedBody))
_, err = stream.Read(body)
require.NoError(t, err)
@@ -154,7 +154,7 @@ func TestServeStreamWS(t *testing.T) {
require.NoError(t, err)
require.True(t, hasHeader(stream, ":status", strconv.Itoa(http.StatusSwitchingProtocols)))
assert.True(t, hasHeader(stream, responseMetaHeaderField, responseMetaHeaderOrigin))
assert.True(t, hasHeader(stream, ResponseMetaHeaderField, responseMetaHeaderOrigin))
data := []byte("test websocket")
err = wsutil.WriteClientText(writePipe, data)
@@ -209,7 +209,7 @@ func benchmarkServeStreamHTTPSimple(b *testing.B, test testRequest) {
b.StopTimer()
require.NoError(b, openstreamErr)
assert.True(b, hasHeader(stream, responseMetaHeaderField, responseMetaHeaderOrigin))
assert.True(b, hasHeader(stream, ResponseMetaHeaderField, responseMetaHeaderOrigin))
require.True(b, hasHeader(stream, ":status", strconv.Itoa(http.StatusOK)))
require.NoError(b, readBodyErr)
require.Equal(b, test.expectedBody, body)
+2 -2
View File
@@ -8,12 +8,12 @@ import (
)
const (
responseMetaHeaderField = "cf-cloudflared-response-meta"
ResponseMetaHeaderField = "cf-cloudflared-response-meta"
)
var (
canonicalResponseUserHeadersField = http.CanonicalHeaderKey(h2mux.ResponseUserHeadersField)
canonicalResponseMetaHeaderField = http.CanonicalHeaderKey(responseMetaHeaderField)
canonicalResponseMetaHeaderField = http.CanonicalHeaderKey(ResponseMetaHeaderField)
responseMetaHeaderCfd = mustInitRespMetaHeader("cloudflared")
responseMetaHeaderOrigin = mustInitRespMetaHeader("origin")
)
+1 -1
View File
@@ -167,7 +167,7 @@ func (rp *http2RespWriter) WriteRespHeaders(resp *http.Response) error {
status = http.StatusOK
}
rp.w.WriteHeader(status)
if isServerSentEvent(resp.Header) {
if IsServerSentEvent(resp.Header) {
rp.shouldFlush = true
}
if rp.shouldFlush {
+3 -3
View File
@@ -100,9 +100,9 @@ func TestServeHTTP(t *testing.T) {
require.Equal(t, test.expectedBody, respBody)
}
if test.isProxyError {
require.Equal(t, responseMetaHeaderCfd, resp.Header.Get(responseMetaHeaderField))
require.Equal(t, responseMetaHeaderCfd, resp.Header.Get(ResponseMetaHeaderField))
} else {
require.Equal(t, responseMetaHeaderOrigin, resp.Header.Get(responseMetaHeaderField))
require.Equal(t, responseMetaHeaderOrigin, resp.Header.Get(ResponseMetaHeaderField))
}
}
cancel()
@@ -202,7 +202,7 @@ func TestServeWS(t *testing.T) {
resp := respWriter.Result()
// http2RespWriter should rewrite status 101 to 200
require.Equal(t, http.StatusOK, resp.StatusCode)
require.Equal(t, responseMetaHeaderOrigin, resp.Header.Get(responseMetaHeaderField))
require.Equal(t, responseMetaHeaderOrigin, resp.Header.Get(ResponseMetaHeaderField))
wg.Wait()
}
+24 -18
View File
@@ -5,37 +5,35 @@ import (
"net/url"
"strings"
"github.com/cloudflare/cloudflared/cmd/cloudflared/ui"
"github.com/cloudflare/cloudflared/logger"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
type Observer struct {
logger.Service
metrics *tunnelMetrics
tunnelEventChan chan<- ui.TunnelEvent
metrics *tunnelMetrics
tunnelEventChans []chan Event
uiEnabled bool
}
func NewObserver(logger logger.Service, tunnelEventChan chan<- ui.TunnelEvent) *Observer {
func NewObserver(logger logger.Service, tunnelEventChans []chan Event, uiEnabled bool) *Observer {
return &Observer{
logger,
newTunnelMetrics(),
tunnelEventChan,
tunnelEventChans,
uiEnabled,
}
}
func (o *Observer) logServerInfo(connIndex uint8, location, msg string) {
// If launch-ui flag is set, send connect msg
if o.tunnelEventChan != nil {
o.tunnelEventChan <- ui.TunnelEvent{Index: connIndex, EventType: ui.Connected, Location: location}
}
o.sendEvent(Event{Index: connIndex, EventType: Connected, Location: location})
o.Infof(msg)
o.metrics.registerServerLocation(uint8ToString(connIndex), location)
}
func (o *Observer) logTrialHostname(registration *tunnelpogs.TunnelRegistration) error {
// Print out the user's trial zone URL in a nice box (if they requested and got one and UI flag is not set)
if o.tunnelEventChan == nil {
if !o.uiEnabled {
if registrationURL, err := url.Parse(registration.Url); err == nil {
for _, line := range asciiBox(trialZoneMsg(registrationURL.String()), 2) {
o.Info(line)
@@ -81,19 +79,27 @@ func trialZoneMsg(url string) []string {
}
func (o *Observer) sendRegisteringEvent() {
if o.tunnelEventChan != nil {
o.tunnelEventChan <- ui.TunnelEvent{EventType: ui.RegisteringTunnel}
}
o.sendEvent(Event{EventType: RegisteringTunnel})
}
func (o *Observer) sendConnectedEvent(connIndex uint8, location string) {
if o.tunnelEventChan != nil {
o.tunnelEventChan <- ui.TunnelEvent{Index: connIndex, EventType: ui.Connected, Location: location}
}
o.sendEvent(Event{Index: connIndex, EventType: Connected, Location: location})
}
func (o *Observer) sendURL(url string) {
if o.tunnelEventChan != nil {
o.tunnelEventChan <- ui.TunnelEvent{EventType: ui.SetUrl, Url: url}
o.sendEvent(Event{EventType: SetURL, URL: url})
}
func (o *Observer) SendReconnect(connIndex uint8) {
o.sendEvent(Event{Index: connIndex, EventType: Reconnecting})
}
func (o *Observer) SendDisconnect(connIndex uint8) {
o.sendEvent(Event{Index: connIndex, EventType: Disconnected})
}
func (o *Observer) sendEvent(e Event) {
for _, ch := range o.tunnelEventChans {
ch <- e
}
}
+1 -1
View File
@@ -165,7 +165,7 @@ func NewProtocolSelector(protocolFlag string, namedTunnel *NamedTunnelConfig, fe
if protocolFlag != autoSelectFlag {
return nil, fmt.Errorf("Unknown protocol %s, %s", protocolFlag, AvailableProtocolFlagMessage)
}
threshold := switchThreshold(namedTunnel.Auth.AccountTag)
threshold := switchThreshold(namedTunnel.Credentials.AccountTag)
if threshold < http2Percentage {
return newAutoProtocolSelector(HTTP2, threshold, fetchFunc, ttl, logger), nil
}
+1 -2
View File
@@ -6,7 +6,6 @@ import (
"time"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/stretchr/testify/assert"
)
@@ -16,7 +15,7 @@ const (
var (
testNamedTunnelConfig = &NamedTunnelConfig{
Auth: pogs.TunnelAuth{
Credentials: Credentials{
AccountTag: "testAccountTag",
},
}
+2 -2
View File
@@ -92,8 +92,8 @@ func (rsc *registrationServerClient) RegisterConnection(
) error {
conn, err := rsc.client.RegisterConnection(
ctx,
config.Auth,
config.ID,
config.Credentials.Auth(),
config.Credentials.TunnelID,
connIndex,
options,
)
+1 -1
View File
@@ -43,7 +43,7 @@ func NewInsecureProxy(ctx context.Context, origin string) (*Proxy, error) {
return nil, errors.Wrap(err, "could not connect to the database")
}
logger, err := logger.New()
logger, err := logger.New() // TODO: Does not obey log configuration
if err != nil {
return nil, errors.Wrap(err, "error setting up logger")
}
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.15.3 as builder
FROM golang:1.15.6 as builder
ENV GO111MODULE=on \
CGO_ENABLED=0
WORKDIR /go/src/github.com/cloudflare/cloudflared/
+1 -1
View File
@@ -189,7 +189,7 @@ func websocketHandler(logger logger.Service, upgrader websocket.Upgrader) http.H
func sseHandler(logger logger.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
flusher, ok := w.(http.Flusher)
if !ok {
w.WriteHeader(http.StatusInternalServerError)
+11 -3
View File
@@ -89,7 +89,7 @@ func parseSingleOriginService(c *cli.Context, allowURLFromArgs bool) (OriginServ
if c.IsSet("hello-world") {
return new(helloWorld), nil
}
if c.IsSet("url") {
if c.IsSet("url") || c.IsSet(config.BastionFlag) {
originURL, err := config.ValidateUrl(c, allowURLFromArgs)
if err != nil {
return nil, errors.Wrap(err, "Error validating origin URL")
@@ -103,7 +103,8 @@ func parseSingleOriginService(c *cli.Context, allowURLFromArgs bool) (OriginServ
}
return &unixSocketPath{path: path}, nil
}
return nil, errors.New("You must either set ingress rules in your config file, or use --url, or use --unix-socket")
u, err := url.Parse("http://localhost:8080")
return &localService{URL: u, RootURL: u}, err
}
// IsEmpty checks if there are any ingress rules.
@@ -128,6 +129,7 @@ func (ing Ingress) CatchAll() *Rule {
func validate(ingress []config.UnvalidatedIngressRule, defaults OriginRequestConfig) (Ingress, error) {
rules := make([]Rule, len(ingress))
for i, r := range ingress {
cfg := setConfig(defaults, r.OriginRequest)
var service OriginService
if prefix := "unix:"; strings.HasPrefix(r.Service, prefix) {
@@ -143,6 +145,12 @@ func validate(ingress []config.UnvalidatedIngressRule, defaults OriginRequestCon
service = &srv
} else if r.Service == "hello_world" || r.Service == "hello-world" || r.Service == "helloworld" {
service = new(helloWorld)
} else if r.Service == "bastion" || cfg.BastionMode {
// Bastion mode will always start a Websocket proxy server, which will
// overwrite the localService.URL field when `start` is called. So,
// leave the URL field empty for now.
cfg.BastionMode = true
service = new(localService)
} else {
// Validate URL services
u, err := url.Parse(r.Service)
@@ -178,7 +186,7 @@ func validate(ingress []config.UnvalidatedIngressRule, defaults OriginRequestCon
Hostname: r.Hostname,
Service: service,
Path: pathRegex,
Config: setConfig(defaults, r.OriginRequest),
Config: cfg,
}
}
return Ingress{Rules: rules, defaults: defaults}, nil
+42
View File
@@ -35,6 +35,7 @@ func Test_parseIngress(t *testing.T) {
fourOhFour := newStatusCode(404)
defaultConfig := setConfig(originRequestFromYAML(config.OriginRequestConfig{}), config.OriginRequestConfig{})
require.Equal(t, defaultKeepAliveConnections, defaultConfig.KeepAliveConnections)
tr := true
type args struct {
rawYAML string
}
@@ -209,6 +210,47 @@ ingress:
},
},
},
{
name: "URL isn't necessary if using bastion",
args: args{rawYAML: `
ingress:
- hostname: bastion.foo.com
originRequest:
bastionMode: true
- service: http_status:404
`},
want: []Rule{
{
Hostname: "bastion.foo.com",
Service: &localService{},
Config: setConfig(originRequestFromYAML(config.OriginRequestConfig{}), config.OriginRequestConfig{BastionMode: &tr}),
},
{
Service: &fourOhFour,
Config: defaultConfig,
},
},
},
{
name: "Bastion service",
args: args{rawYAML: `
ingress:
- hostname: bastion.foo.com
service: bastion
- service: http_status:404
`},
want: []Rule{
{
Hostname: "bastion.foo.com",
Service: &localService{},
Config: setConfig(originRequestFromYAML(config.OriginRequestConfig{}), config.OriginRequestConfig{BastionMode: &tr}),
},
{
Service: &fourOhFour,
Config: defaultConfig,
},
},
},
{
name: "Hostname contains port",
args: args{rawYAML: `
+13 -7
View File
@@ -84,10 +84,6 @@ func (o *localService) Dial(reqURL *url.URL, headers http.Header) (*gws.Conn, *h
return d.Dial(reqURL.String(), headers)
}
func (o *localService) address() string {
return o.URL.String()
}
func (o *localService) start(wg *sync.WaitGroup, log logger.Service, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error {
transport, err := newHTTPTransport(o, cfg, log)
if err != nil {
@@ -96,8 +92,7 @@ func (o *localService) start(wg *sync.WaitGroup, log logger.Service, shutdownC <
o.transport = transport
// Start a proxy if one is needed
staticHost := o.staticHost()
if originRequiresProxy(staticHost, cfg) {
if staticHost := o.staticHost(); originRequiresProxy(staticHost, cfg) {
if err := o.startProxy(staticHost, wg, log, shutdownC, errC, cfg); err != nil {
return err
}
@@ -152,7 +147,14 @@ func (o *localService) startProxy(staticHost string, wg *sync.WaitGroup, log log
}
func (o *localService) String() string {
return o.address()
if o.isBastion() {
return "Bastion"
}
return o.URL.String()
}
func (o *localService) isBastion() bool {
return o.URL == nil
}
func (o *localService) RoundTrip(req *http.Request) (*http.Response, error) {
@@ -164,6 +166,10 @@ func (o *localService) RoundTrip(req *http.Request) (*http.Response, error) {
func (o *localService) staticHost() string {
if o.URL == nil {
return ""
}
addPortIfMissing := func(uri *url.URL, port int) string {
if uri.Port() != "" {
return uri.Host
+118
View File
@@ -0,0 +1,118 @@
package logger
var defaultConfig = createDefaultConfig()
// Logging configuration
type Config struct {
ConsoleConfig *ConsoleConfig // If nil, the logger will not log into the console
FileConfig *FileConfig // If nil, the logger will not use an individual log file
RollingConfig *RollingConfig // If nil, the logger will not use a rolling log
MinLevel string // debug | info | error | fatal
}
type ConsoleConfig struct {
noColor bool
}
type FileConfig struct {
Filepath string
}
type RollingConfig struct {
Directory string
Filename string
maxSize int // megabytes
maxBackups int // files
maxAge int // days
}
func createDefaultConfig() Config {
const minLevel = "fatal"
const RollingMaxSize = 1 // Mb
const RollingMaxBackups = 5 // files
const RollingMaxAge = 0 // Keep forever
const rollingLogFilename = "cloudflared.log"
return Config{
ConsoleConfig: &ConsoleConfig{
noColor: false,
},
FileConfig: &FileConfig{
Filepath: "",
},
RollingConfig: &RollingConfig{
Directory: "",
Filename: rollingLogFilename,
maxSize: RollingMaxSize,
maxBackups: RollingMaxBackups,
maxAge: RollingMaxAge,
},
MinLevel: minLevel,
}
}
func CreateConfig(
minLevel string,
disableTerminal bool,
rollingLogPath, nonRollingLogFilePath string,
) *Config {
var console *ConsoleConfig
if !disableTerminal {
console = createConsoleConfig()
}
var file *FileConfig
if nonRollingLogFilePath != "" {
file = createFileConfig(nonRollingLogFilePath)
}
var rolling *RollingConfig
if rollingLogPath != "" {
rolling = createRollingConfig(rollingLogPath)
}
if minLevel == "" {
minLevel = defaultConfig.MinLevel
}
return &Config{
ConsoleConfig: console,
FileConfig: file,
RollingConfig: rolling,
MinLevel: minLevel,
}
}
func createConsoleConfig() *ConsoleConfig {
return &ConsoleConfig{
noColor: false,
}
}
func createFileConfig(filepath string) *FileConfig {
if filepath == "" {
filepath = defaultConfig.FileConfig.Filepath
}
return &FileConfig{
Filepath: filepath,
}
}
func createRollingConfig(directory string) *RollingConfig {
if directory == "" {
directory = defaultConfig.RollingConfig.Directory
}
return &RollingConfig{
Directory: directory,
Filename: defaultConfig.RollingConfig.Filename,
maxSize: defaultConfig.RollingConfig.maxSize,
maxBackups: defaultConfig.RollingConfig.maxBackups,
maxAge: defaultConfig.RollingConfig.maxAge,
}
}
+71
View File
@@ -8,6 +8,20 @@ import (
"time"
"github.com/alecthomas/units"
"github.com/urfave/cli/v2"
)
const (
EnableTerminalLog = false
DisableTerminalLog = true
LogLevelFlag = "loglevel"
LogFileFlag = "logfile"
LogDirectoryFlag = "log-directory"
LogTransportLevelFlag = "transport-loglevel"
LogSSHDirectoryFlag = "log-directory"
LogSSHLevelFlag = "log-level"
)
// Option is to encaspulate actions that will be called by Parse and run later to build an Options struct
@@ -127,6 +141,63 @@ func New(opts ...Option) (*OutputWriter, error) {
return l, nil
}
func NewInHouse(loggerConfig *Config) (*OutputWriter, error) {
var loggerOpts []Option
var logPath string
if loggerConfig.FileConfig != nil {
logPath = loggerConfig.FileConfig.Filepath
}
if logPath == "" && loggerConfig.RollingConfig != nil {
logPath = loggerConfig.RollingConfig.Directory
}
if logPath != "" {
loggerOpts = append(loggerOpts, DefaultFile(logPath))
}
loggerOpts = append(loggerOpts, LogLevelString(loggerConfig.MinLevel))
if loggerConfig.ConsoleConfig == nil {
disableOption := DisableTerminal(true)
loggerOpts = append(loggerOpts, disableOption)
}
l, err := New(loggerOpts...)
if err != nil {
return nil, err
}
return l, nil
}
func CreateTransportLoggerFromContext(c *cli.Context, disableTerminal bool) (*OutputWriter, error) {
return createFromContext(c, LogTransportLevelFlag, LogDirectoryFlag, disableTerminal)
}
func CreateLoggerFromContext(c *cli.Context, disableTerminal bool) (*OutputWriter, error) {
return createFromContext(c, LogLevelFlag, LogDirectoryFlag, disableTerminal)
}
func CreateSSHLoggerFromContext(c *cli.Context, disableTerminal bool) (*OutputWriter, error) {
return createFromContext(c, LogSSHLevelFlag, LogSSHDirectoryFlag, disableTerminal)
}
func createFromContext(
c *cli.Context,
logLevelFlagName,
logDirectoryFlagName string,
disableTerminal bool,
) (*OutputWriter, error) {
logLevel := c.String(logLevelFlagName)
logFile := c.String(LogFileFlag)
logDirectory := c.String(logDirectoryFlagName)
loggerConfig := CreateConfig(logLevel, disableTerminal, logDirectory, logFile)
return NewInHouse(loggerConfig)
}
// ParseLevelString returns the expected log levels based on the cmd flag
func ParseLevelString(lvl string) ([]Level, error) {
switch strings.ToLower(lvl) {
+20 -6
View File
@@ -12,6 +12,7 @@ import (
"golang.org/x/net/trace"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/logger"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
@@ -22,22 +23,35 @@ const (
startupTime = time.Millisecond * 500
)
func ServeMetrics(l net.Listener, shutdownC <-chan struct{}, logger logger.Service) (err error) {
func newMetricsHandler(connectionEvents <-chan connection.Event, log logger.Service) *http.ServeMux {
readyServer := NewReadyServer(connectionEvents, log)
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())
mux.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "OK\n")
})
mux.Handle("/ready", readyServer)
return mux
}
func ServeMetrics(
l net.Listener,
shutdownC <-chan struct{},
connectionEvents <-chan connection.Event,
logger logger.Service,
) (err error) {
var wg sync.WaitGroup
// Metrics port is privileged, so no need for further access control
trace.AuthRequest = func(*http.Request) (bool, bool) { return true, true }
// TODO: parameterize ReadTimeout and WriteTimeout. The maximum time we can
// profile CPU usage depends on WriteTimeout
h := newMetricsHandler(connectionEvents, logger)
server := &http.Server{
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
Handler: h,
}
http.Handle("/metrics", promhttp.Handler())
http.Handle("/healthcheck", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "OK\n")
}))
wg.Add(1)
go func() {
defer wg.Done()
+80
View File
@@ -0,0 +1,80 @@
package metrics
import (
"encoding/json"
"fmt"
"net/http"
"sync"
conn "github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/logger"
)
// ReadyServer serves HTTP 200 if the tunnel can serve traffic. Intended for k8s readiness checks.
type ReadyServer struct {
sync.RWMutex
isConnected map[int]bool
log logger.Service
}
// NewReadyServer initializes a ReadyServer and starts listening for dis/connection events.
func NewReadyServer(connectionEvents <-chan conn.Event, log logger.Service) *ReadyServer {
rs := ReadyServer{
isConnected: make(map[int]bool, 0),
log: log,
}
go func() {
for c := range connectionEvents {
switch c.EventType {
case conn.Connected:
rs.Lock()
rs.isConnected[int(c.Index)] = true
rs.Unlock()
case conn.Disconnected, conn.Reconnecting, conn.RegisteringTunnel:
rs.Lock()
rs.isConnected[int(c.Index)] = false
rs.Unlock()
case conn.SetURL:
continue
default:
rs.log.Errorf("Unknown connection event case %v", c)
}
}
}()
return &rs
}
type body struct {
Status int `json:"status"`
ReadyConnections int `json:"readyConnections"`
}
// ServeHTTP responds with HTTP 200 if the tunnel is connected to the edge.
func (rs *ReadyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
statusCode, readyConnections := rs.makeResponse()
w.WriteHeader(statusCode)
body := body{
Status: statusCode,
ReadyConnections: readyConnections,
}
msg, err := json.Marshal(body)
if err != nil {
fmt.Fprintf(w, `{"error": "%s"}`, err)
}
w.Write(msg)
}
// This is the bulk of the logic for ServeHTTP, broken into its own pure function
// to make unit testing easy.
func (rs *ReadyServer) makeResponse() (statusCode, readyConnections int) {
statusCode = http.StatusServiceUnavailable
rs.RLock()
defer rs.RUnlock()
for _, connected := range rs.isConnected {
if connected {
statusCode = http.StatusOK
readyConnections++
}
}
return statusCode, readyConnections
}
+58
View File
@@ -0,0 +1,58 @@
package metrics
import (
"net/http"
"testing"
)
func TestReadyServer_makeResponse(t *testing.T) {
type fields struct {
isConnected map[int]bool
}
tests := []struct {
name string
fields fields
wantOK bool
wantReadyConnections int
}{
{
name: "One connection online => HTTP 200",
fields: fields{
isConnected: map[int]bool{
0: false,
1: false,
2: true,
3: false,
},
},
wantOK: true,
wantReadyConnections: 1,
},
{
name: "No connections online => no HTTP 200",
fields: fields{
isConnected: map[int]bool{
0: false,
1: false,
2: false,
3: false,
},
},
wantReadyConnections: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rs := &ReadyServer{
isConnected: tt.fields.isConnected,
}
gotStatusCode, gotReadyConnections := rs.makeResponse()
if tt.wantOK && gotStatusCode != http.StatusOK {
t.Errorf("ReadyServer.makeResponse() gotStatusCode = %v, want ok = %v", gotStatusCode, tt.wantOK)
}
if gotReadyConnections != tt.wantReadyConnections {
t.Errorf("ReadyServer.makeResponse() gotReadyConnections = %v, want %v", gotReadyConnections, tt.wantReadyConnections)
}
})
}
}
+2 -13
View File
@@ -96,7 +96,7 @@ func (c *client) proxyHTTP(w connection.ResponseWriter, req *http.Request, rule
if err != nil {
return nil, errors.Wrap(err, "Error writing response header")
}
if isEventStream(resp) {
if connection.IsServerSentEvent(resp.Header) {
c.logger.Debug("Detected Server-Side Events from Origin")
c.writeEventStream(w, resp.Body)
} else {
@@ -190,7 +190,7 @@ func (c *client) logRequest(r *http.Request, cfRay string, lbProbe bool, ruleNum
func (c *client) logOriginResponse(r *http.Response, cfRay string, lbProbe bool, ruleNum int) {
responseByCode.WithLabelValues(strconv.Itoa(r.StatusCode)).Inc()
if cfRay != "" {
c.logger.Infof("CF-RAY: %s Status: %s served by ingress %d", cfRay, r.Status, ruleNum)
c.logger.Debugf("CF-RAY: %s Status: %s served by ingress %d", cfRay, r.Status, ruleNum)
} else if lbProbe {
c.logger.Debugf("Response to Load Balancer health check %s", r.Status)
} else {
@@ -222,14 +222,3 @@ func findCfRayHeader(req *http.Request) string {
func isLBProbeRequest(req *http.Request) bool {
return strings.HasPrefix(req.UserAgent(), lbProbeUserAgentPrefix)
}
func uint8ToString(input uint8) string {
return strconv.FormatUint(uint64(input), 10)
}
func isEventStream(response *http.Response) bool {
if response.Header.Get("content-type") == "text/event-stream" {
return true
}
return false
}
+3 -12
View File
@@ -16,7 +16,6 @@ import (
"golang.org/x/sync/errgroup"
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
"github.com/cloudflare/cloudflared/cmd/cloudflared/ui"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/edgediscovery"
"github.com/cloudflare/cloudflared/h2mux"
@@ -65,7 +64,7 @@ type TunnelConfig struct {
NamedTunnel *connection.NamedTunnelConfig
ClassicTunnel *connection.ClassicTunnelConfig
MuxerConfig *connection.MuxerConfig
TunnelEventChan chan ui.TunnelEvent
TunnelEventChans []chan connection.Event
ProtocolSelector connection.ProtocolSelector
EdgeTLSConfigs map[connection.Protocol]*tls.Config
}
@@ -235,10 +234,7 @@ func waitForBackoff(
return err
}
if config.TunnelEventChan != nil {
config.TunnelEventChan <- ui.TunnelEvent{Index: connIndex, EventType: ui.Reconnecting}
}
config.Observer.SendReconnect(connIndex)
config.Logger.Infof("Retrying connection %d in %s seconds, error %v", connIndex, duration, err)
protobackoff.Backoff(ctx)
@@ -288,12 +284,7 @@ func ServeTunnel(
}
}()
// If launch-ui flag is set, send disconnect msg
if config.TunnelEventChan != nil {
defer func() {
config.TunnelEventChan <- ui.TunnelEvent{Index: connIndex, EventType: ui.Disconnected}
}()
}
defer config.Observer.SendDisconnect(connIndex)
edgeConn, err := edgediscovery.DialEdge(ctx, dialTimeout, config.EdgeTLSConfigs[protocol], addr)
if err != nil {
+2 -2
View File
@@ -8,7 +8,6 @@ import (
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/stretchr/testify/assert"
)
@@ -36,7 +35,7 @@ func TestWaitForBackoffFallback(t *testing.T) {
assert.NoError(t, err)
resolveTTL := time.Duration(0)
namedTunnel := &connection.NamedTunnelConfig{
Auth: pogs.TunnelAuth{
Credentials: connection.Credentials{
AccountTag: "test-account",
},
}
@@ -48,6 +47,7 @@ func TestWaitForBackoffFallback(t *testing.T) {
config := &TunnelConfig{
Logger: logger,
ProtocolSelector: protocolSelector,
Observer: connection.NewObserver(nil, nil, false),
}
connIndex := uint8(1)
+1 -1
View File
@@ -52,7 +52,7 @@ var mockRequest func(url, contentType string, body io.Reader) (*http.Response, e
// GenerateShortLivedCertificate generates and stores a keypair for short lived certs
func GenerateShortLivedCertificate(appURL *url.URL, token string) error {
fullName, err := cfpath.GenerateFilePathFromURL(appURL, keyName)
fullName, err := cfpath.GenerateAppTokenFilePathFromURL(appURL, keyName)
if err != nil {
return err
}
+1 -1
View File
@@ -35,7 +35,7 @@ func TestCertGenSuccess(t *testing.T) {
url, _ := url.Parse("https://cf-test-access.com/testpath")
token := tokenGenerator()
fullName, err := cfpath.GenerateFilePathFromURL(url, keyName)
fullName, err := cfpath.GenerateAppTokenFilePathFromURL(url, keyName)
assert.NoError(t, err)
pubKeyName := fullName + ".pub"
+2 -4
View File
@@ -9,7 +9,6 @@ import (
"syscall"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/metrics"
@@ -71,8 +70,7 @@ func Command(hidden bool) *cli.Command {
// Run implements a foreground runner
func Run(c *cli.Context) error {
logDirectory, logLevel := config.FindLogSettings()
logger, err := logger.New(logger.DefaultFile(logDirectory), logger.LogLevelString(logLevel))
logger, err := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
if err != nil {
return cliutil.PrintLoggerSetupError("error setting up logger", err)
}
@@ -82,7 +80,7 @@ func Run(c *cli.Context) error {
logger.Fatalf("Failed to open the metrics listener: %s", err)
}
go metrics.ServeMetrics(metricsListener, nil, logger)
go metrics.ServeMetrics(metricsListener, nil, nil, logger)
listener, err := CreateListener(c.String("address"), uint16(c.Uint("port")), c.StringSlice("upstream"), c.StringSlice("bootstrap"), logger)
if err != nil {
+55 -41
View File
@@ -147,56 +147,70 @@ func StartProxyServer(logger logger.Service, listener net.Listener, staticHost s
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
h := handler{
upgrader: upgrader,
logger: logger,
staticHost: staticHost,
streamHandler: streamHandler,
}
httpServer := &http.Server{Addr: listener.Addr().String(), Handler: nil}
httpServer := &http.Server{Addr: listener.Addr().String(), Handler: &h}
go func() {
<-shutdownC
httpServer.Close()
}()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// If remote is an empty string, get the destination from the client.
finalDestination := staticHost
if finalDestination == "" {
if jumpDestination := r.Header.Get(h2mux.CFJumpDestinationHeader); jumpDestination == "" {
logger.Error("Did not receive final destination from client. The --destination flag is likely not set")
return
} else {
finalDestination = jumpDestination
}
}
stream, err := net.Dial("tcp", finalDestination)
if err != nil {
logger.Errorf("Cannot connect to remote: %s", err)
return
}
defer stream.Close()
if !websocket.IsWebSocketUpgrade(r) {
w.Write(nonWebSocketRequestPage())
return
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
logger.Errorf("failed to upgrade: %s", err)
return
}
conn.SetReadDeadline(time.Now().Add(pongWait))
conn.SetPongHandler(func(string) error { conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
done := make(chan struct{})
go pinger(logger, conn, done)
defer func() {
done <- struct{}{}
conn.Close()
}()
streamHandler(&Conn{conn}, stream, r.Header)
})
return httpServer.Serve(listener)
}
// HTTP handler for the websocket proxy.
type handler struct {
logger logger.Service
staticHost string
upgrader websocket.Upgrader
streamHandler func(wsConn *Conn, remoteConn net.Conn, requestHeaders http.Header)
}
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// If remote is an empty string, get the destination from the client.
finalDestination := h.staticHost
if finalDestination == "" {
if jumpDestination := r.Header.Get(h2mux.CFJumpDestinationHeader); jumpDestination == "" {
h.logger.Error("Did not receive final destination from client. The --destination flag is likely not set")
return
} else {
finalDestination = jumpDestination
}
}
stream, err := net.Dial("tcp", finalDestination)
if err != nil {
h.logger.Errorf("Cannot connect to remote: %s", err)
return
}
defer stream.Close()
if !websocket.IsWebSocketUpgrade(r) {
w.Write(nonWebSocketRequestPage())
return
}
conn, err := h.upgrader.Upgrade(w, r, nil)
if err != nil {
h.logger.Errorf("failed to upgrade: %s", err)
return
}
conn.SetReadDeadline(time.Now().Add(pongWait))
conn.SetPongHandler(func(string) error { conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
done := make(chan struct{})
go pinger(h.logger, conn, done)
defer func() {
done <- struct{}{}
conn.Close()
}()
h.streamHandler(&Conn{conn}, stream, r.Header)
}
// SendSSHPreamble sends the final SSH destination address to the cloudflared SSH proxy
// The destination is preceded by its length
// Not part of sshserver module to fix compilation for incompatible operating systems