mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-08-07 15:24:46 +00:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b9d6aaebbe | |||
| a99780ed9d | |||
| 92765b4261 | |||
| 8b88b4a403 | |||
| 2ce6720a6e | |||
| 772ccc9607 | |||
| 7724ff8176 | |||
| 2a3d486126 | |||
| 932e383051 | |||
| dbe3516204 | |||
| 6fc9f1b405 | |||
| 370c17e48c | |||
| 92da73aa9d | |||
| 0c65daaa7d | |||
| b46acd7f63 | |||
| e3a9aa4296 | |||
| 3886021ba5 | |||
| 4d3ebaf984 | |||
| 9131e842a5 | |||
| 4f9cfa6542 | |||
| a1a8645294 | |||
| b95b289a8c | |||
| 425554077f | |||
| 7b2f286210 | |||
| 0f893fab47 | |||
| 12c615e2e6 | |||
| 6e5ccd7c85 | |||
| 3ec500bdbb | |||
| 8f75feac94 | |||
| 448a7798f7 | |||
| dc3a228d51 | |||
| 554c97a8cb | |||
| fd1941dfbe | |||
| 5fd348dcfc | |||
| 1a6403b2fd | |||
| 55acf7283c | |||
| acb7d604fd | |||
| 6e761cb7ae | |||
| ae8d784e36 | |||
| c716dd273c | |||
| eb3c4a7a9f | |||
| 97a901a229 | |||
| e054c5bb7b | |||
| 0d87279b2f | |||
| f8638839c0 | |||
| 2f70b05c64 | |||
| e02d09a731 | |||
| 0ff491905f | |||
| 8c59254488 | |||
| 9e76e42e3c | |||
| e376a13025 | |||
| 3a086e9cc2 | |||
| fb82b2ced5 | |||
| be0514c5c9 | |||
| 046be63253 | |||
| a908453aa4 | |||
| 7a77ead423 | |||
| b89cc22896 | |||
| 6a7418e1af | |||
| df3ad2b223 | |||
| 8c870c19a6 |
@@ -0,0 +1,10 @@
|
||||
images:
|
||||
- name: cloudflared
|
||||
dockerfile: Dockerfile
|
||||
context: .
|
||||
versions:
|
||||
- latest
|
||||
registries:
|
||||
- name: docker.io/cloudflare
|
||||
user: env:DOCKER_USER
|
||||
password: env:DOCKER_PASSWORD
|
||||
@@ -9,6 +9,7 @@ guide/public
|
||||
\#*\#
|
||||
cscope.*
|
||||
cloudflared
|
||||
cloudflared.pkg
|
||||
cloudflared.exe
|
||||
!cmd/cloudflared/
|
||||
.DS_Store
|
||||
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# uninstall first in case this is an upgrade
|
||||
/usr/local/bin/cloudflared service uninstall
|
||||
|
||||
# install the new service using launchctl
|
||||
/usr/local/bin/cloudflared service install
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
/usr/local/bin/cloudflared service uninstall
|
||||
rm /usr/local/bin/cloudflared
|
||||
pkgutil --forget com.cloudflare.cloudflared
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "$(uname)" != "Darwin" ]] ; then
|
||||
echo "This should be run on macOS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
go version
|
||||
export GO111MODULE=on
|
||||
|
||||
# build 'cloudflared-darwin-amd64.tgz'
|
||||
mkdir -p artifacts
|
||||
FILENAME="$(pwd)/artifacts/cloudflared-darwin-amd64.tgz"
|
||||
export PATH="$PATH:/usr/local/bin"
|
||||
mkdir -p ../src/github.com/cloudflare/
|
||||
cp -r . ../src/github.com/cloudflare/cloudflared
|
||||
cd ../src/github.com/cloudflare/cloudflared
|
||||
GOCACHE="$PWD/../../../../" GOPATH="$PWD/../../../../" CGO_ENABLED=1 make cloudflared
|
||||
tar czf "$FILENAME" cloudflared
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
FILENAME="${PWD}/artifacts/cloudflared-darwin-amd64.tgz"
|
||||
|
||||
if ! VERSION="$(git describe --tags --exact-match 2>/dev/null)" ; then
|
||||
echo "Skipping public release for an untagged commit."
|
||||
echo "##teamcity[buildStatus status='SUCCESS' text='Skipped due to lack of tag']"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ ! -f "$FILENAME" ]] ; then
|
||||
echo "Missing $FILENAME"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${GITHUB_PRIVATE_KEY:-}" == "" ]] ; then
|
||||
echo "Missing GITHUB_PRIVATE_KEY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# upload to s3 bucket for use by Homebrew formula
|
||||
s3cmd \
|
||||
--acl-public --signature-v2 --access_key="$AWS_ACCESS_KEY_ID" --secret_key="$AWS_SECRET_ACCESS_KEY" --host-bucket="%(bucket)s.s3.cfdata.org" \
|
||||
put "$FILENAME" "s3://cftunnel-docs/dl/cloudflared-$VERSION-darwin-amd64.tgz"
|
||||
s3cmd \
|
||||
--acl-public --signature-v2 --access_key="$AWS_ACCESS_KEY_ID" --secret_key="$AWS_SECRET_ACCESS_KEY" --host-bucket="%(bucket)s.s3.cfdata.org" \
|
||||
cp "s3://cftunnel-docs/dl/cloudflared-$VERSION-darwin-amd64.tgz" "s3://cftunnel-docs/dl/cloudflared-stable-darwin-amd64.tgz"
|
||||
SHA256=$(sha256sum "$FILENAME" | cut -b1-64)
|
||||
|
||||
# set up git (note that UserKnownHostsFile is an absolute path so we can cd wherever)
|
||||
mkdir -p tmp
|
||||
ssh-keyscan -t rsa github.com > tmp/github.txt
|
||||
echo "$GITHUB_PRIVATE_KEY" > tmp/private.key
|
||||
chmod 0400 tmp/private.key
|
||||
export GIT_SSH_COMMAND="ssh -o UserKnownHostsFile=$PWD/tmp/github.txt -i $PWD/tmp/private.key -o IdentitiesOnly=yes"
|
||||
|
||||
# clone Homebrew repo into tmp/homebrew-cloudflare
|
||||
git clone git@github.com:cloudflare/homebrew-cloudflare.git tmp/homebrew-cloudflare
|
||||
cd tmp/homebrew-cloudflare
|
||||
git checkout -f master
|
||||
git reset --hard origin/master
|
||||
|
||||
# modify cloudflared.rb
|
||||
URL="https://developers.cloudflare.com/argo-tunnel/dl/cloudflared-$VERSION-darwin-amd64.tgz"
|
||||
tee cloudflared.rb <<EOF
|
||||
class Cloudflared < Formula
|
||||
desc 'Argo Tunnel'
|
||||
homepage 'https://developers.cloudflare.com/argo-tunnel/'
|
||||
url '$URL'
|
||||
sha256 '$SHA256'
|
||||
version '$VERSION'
|
||||
def install
|
||||
bin.install 'cloudflared'
|
||||
end
|
||||
end
|
||||
EOF
|
||||
|
||||
# push cloudflared.rb
|
||||
git add cloudflared.rb
|
||||
git diff
|
||||
git config user.name "cloudflare-warp-bot"
|
||||
git config user.email "warp-bot@cloudflare.com"
|
||||
git commit -m "Release Argo Tunnel $VERSION"
|
||||
|
||||
git push -v origin master
|
||||
+6
-5
@@ -1,8 +1,11 @@
|
||||
# use a builder image for building cloudflare
|
||||
ARG TARGET_GOOS
|
||||
ARG TARGET_GOARCH
|
||||
FROM golang:1.13.3 as builder
|
||||
ENV GO111MODULE=on
|
||||
ENV CGO_ENABLED=0
|
||||
ENV GOOS=linux
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0 \
|
||||
TARGET_GOOS=${TARGET_GOOS} \
|
||||
TARGET_GOARCH=${TARGET_GOARCH}
|
||||
|
||||
WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
||||
|
||||
@@ -12,8 +15,6 @@ COPY . .
|
||||
# compile cloudflared
|
||||
RUN make cloudflared
|
||||
|
||||
# ---
|
||||
|
||||
# use a distroless base image with glibc
|
||||
FROM gcr.io/distroless/base-debian10:nonroot
|
||||
|
||||
|
||||
@@ -7,17 +7,45 @@ PACKAGE_DIR := $(CURDIR)/packaging
|
||||
INSTALL_BINDIR := usr/local/bin
|
||||
|
||||
EQUINOX_FLAGS = --version="$(VERSION)" \
|
||||
--platforms="$(EQUINOX_BUILD_PLATFORMS)" \
|
||||
--app="$(EQUINOX_APP_ID)" \
|
||||
--token="$(EQUINOX_TOKEN)" \
|
||||
--channel="$(EQUINOX_CHANNEL)"
|
||||
--platforms="$(EQUINOX_BUILD_PLATFORMS)" \
|
||||
--app="$(EQUINOX_APP_ID)" \
|
||||
--token="$(EQUINOX_TOKEN)" \
|
||||
--channel="$(EQUINOX_CHANNEL)"
|
||||
|
||||
ifeq ($(EQUINOX_IS_DRAFT), true)
|
||||
EQUINOX_FLAGS := --draft $(EQUINOX_FLAGS)
|
||||
endif
|
||||
|
||||
ifeq ($(GOARCH),)
|
||||
GOARCH := amd64
|
||||
LOCAL_ARCH ?= $(shell uname -m)
|
||||
ifneq ($(GOARCH),)
|
||||
TARGET_ARCH ?= $(GOARCH)
|
||||
else ifeq ($(LOCAL_ARCH),x86_64)
|
||||
TARGET_ARCH ?= amd64
|
||||
else ifeq ($(shell echo $(LOCAL_ARCH) | head -c 5),armv8)
|
||||
TARGET_ARCH ?= arm64
|
||||
else ifeq ($(LOCAL_ARCH),aarch64)
|
||||
TARGET_ARCH ?= arm64
|
||||
else ifeq ($(shell echo $(LOCAL_ARCH) | head -c 4),armv)
|
||||
TARGET_ARCH ?= arm
|
||||
else
|
||||
$(error This system's architecture $(LOCAL_ARCH) isn't supported)
|
||||
endif
|
||||
|
||||
LOCAL_OS ?= $(shell go env GOOS)
|
||||
ifeq ($(LOCAL_OS),linux)
|
||||
TARGET_OS ?= linux
|
||||
else ifeq ($(LOCAL_OS),darwin)
|
||||
TARGET_OS ?= darwin
|
||||
else ifeq ($(LOCAL_OS),windows)
|
||||
TARGET_OS ?= windows
|
||||
else
|
||||
$(error This system's OS $(LOCAL_OS) isn't supported)
|
||||
endif
|
||||
|
||||
ifeq ($(TARGET_OS), windows)
|
||||
EXECUTABLE_PATH=./cloudflared.exe
|
||||
else
|
||||
EXECUTABLE_PATH=./cloudflared
|
||||
endif
|
||||
|
||||
.PHONY: all
|
||||
@@ -29,11 +57,11 @@ clean:
|
||||
|
||||
.PHONY: cloudflared
|
||||
cloudflared: tunnel-deps
|
||||
go build -v -mod=vendor $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/cloudflared
|
||||
GOOS=$(TARGET_OS) GOARCH=$(TARGET_ARCH) go build -v -mod=vendor $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/cloudflared
|
||||
|
||||
.PHONY: container
|
||||
container:
|
||||
docker build -t cloudflare/cloudflared:"$(VERSION)" .
|
||||
docker build --build-arg=TARGET_ARCH=$(TARGET_ARCH) --build-arg=TARGET_OS=$(TARGET_OS) -t cloudflare/cloudflared-$(TARGET_OS)-$(TARGET_ARCH):"$(VERSION)" .
|
||||
|
||||
.PHONY: test
|
||||
test: vet
|
||||
@@ -43,12 +71,34 @@ test: vet
|
||||
test-ssh-server:
|
||||
docker-compose -f ssh_server_tests/docker-compose.yml up
|
||||
|
||||
define publish_package
|
||||
for HOST in $(CF_PKG_HOSTS); do \
|
||||
ssh-keyscan -t rsa $$HOST >> ~/.ssh/known_hosts; \
|
||||
scp -4 cloudflared_$(VERSION)_amd64.deb cfsync@$$HOST:/state/cf-pkg/staging/$(1)/$(FLAVOR)/cloudflared/; \
|
||||
done
|
||||
endef
|
||||
|
||||
.PHONY: publish-deb
|
||||
publish-deb: cloudflared-deb
|
||||
$(call publish_package,apt)
|
||||
|
||||
.PHONY: publish-rpm
|
||||
publish-rpm: cloudflared-rpm
|
||||
$(call publish_package,yum)
|
||||
|
||||
.PHONY: cloudflared-deb
|
||||
cloudflared-deb: cloudflared
|
||||
mkdir -p $(PACKAGE_DIR)
|
||||
cp cloudflared $(PACKAGE_DIR)/cloudflared
|
||||
fakeroot fpm -C $(PACKAGE_DIR) -s dir -t deb --deb-compression bzip2 \
|
||||
-a $(GOARCH) -v $(VERSION) -n cloudflared cloudflared=/usr/local/bin/
|
||||
-a $(TARGET_ARCH) -v $(VERSION) -n cloudflared cloudflared=/usr/local/bin/
|
||||
|
||||
.PHONY: cloudflared-rpm
|
||||
cloudflared-rpm: cloudflared
|
||||
mkdir -p $(PACKAGE_DIR)
|
||||
cp cloudflared $(PACKAGE_DIR)/cloudflared
|
||||
fakeroot fpm -C $(PACKAGE_DIR) -s dir -t rpm --rpm-compression bzip2 \
|
||||
-a $(TARGET_ARCH) -v $(VERSION) -n cloudflared cloudflared=/usr/local/bin/
|
||||
|
||||
.PHONY: cloudflared-darwin-amd64.tgz
|
||||
cloudflared-darwin-amd64.tgz: cloudflared
|
||||
@@ -68,6 +118,10 @@ homebrew-release: homebrew-upload
|
||||
release: bin/equinox
|
||||
bin/equinox release $(EQUINOX_FLAGS) -- $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/cloudflared
|
||||
|
||||
.PHONY: github-release
|
||||
github-release: cloudflared
|
||||
python3 github_release.py --path $(EXECUTABLE_PATH) --release-version $(VERSION)
|
||||
|
||||
bin/equinox:
|
||||
mkdir -p bin
|
||||
curl -s https://bin.equinox.io/c/75JtLRTsJ3n/release-tool-beta-$(EQUINOX_PLATFORM).tgz | tar xz -C bin/
|
||||
|
||||
@@ -1,3 +1,72 @@
|
||||
2020.7.1
|
||||
- 2020-07-02 DEVTOOLS-7321: Push GitHub homebrew updates to master
|
||||
- 2020-07-06 TUN-3161: Upgrade golang.org/x/ deps
|
||||
- 2020-06-30 AUTH-2854: Create cloudflared RPMs
|
||||
- 2020-06-26 AUTH-2850 log config file path
|
||||
|
||||
2020.7.0
|
||||
- 2020-06-30 TUN-3140: Add timestamps to terminal log entries
|
||||
- 2020-06-30 AUTH-2860: Fix builds
|
||||
- 2020-06-25 TUN-3007: Implement named tunnel connection registration and unregistration.
|
||||
|
||||
2020.6.6
|
||||
- 2020-06-23 AUTH-2685: Adds script to create release
|
||||
- 2020-06-25 AUTH-2652: Update cloudflare repo
|
||||
- 2020-06-26 AUTH-2718: Add target for publishing deb to pkg.cloudflare repo
|
||||
- 2020-06-26 AUTH-2849 all log output to stderr
|
||||
- 2020-06-17 TUN-3106: Pass NamedTunnel config to StartServer
|
||||
- 2020-06-18 TUN-3107: UnregisterConnection shouldn't wrap nil error as RPC error
|
||||
- 2020-06-17 AUTH-2652: Adds .docker-images to push images to docker hub
|
||||
- 2020-06-18 AUTH-2712 mac package build script and better config file handling when started as a service
|
||||
|
||||
2020.6.5
|
||||
- 2020-06-16 DEVTOOLS-7321: Don't skip macOS builds based on tag
|
||||
- 2020-06-16 fix for a flaky test
|
||||
- 2020-06-16 AUTH-2815 flag check was wrong. stupid oversight
|
||||
- 2020-06-16 TUN-3101: Tunnel list command should only show non-deleted, by default
|
||||
- 2020-06-16 TUN-3066: Command line action for tunnel run
|
||||
- 2020-06-16 TUN-3100 make updater report the right text
|
||||
|
||||
2020.6.4
|
||||
- 2020-06-11 TUN-3085: Pass connection authentication information using TunnelAuth struct
|
||||
- 2020-06-15 TUN-3084: Generate and store tunnel_secret value during tunnel creation
|
||||
- 2020-06-16 AUTH-2815 add the log file to support the config.yaml file
|
||||
- 2020-06-02 TUN-3015: Add a new cap'n'proto RPC interface for connection registration as well as matching client and server implementations. The old interface extends the new one for backward compatibility.
|
||||
|
||||
2020.6.3
|
||||
- 2020-06-15 DEVTOOLS-7321: Add openssh-client pkg for missing ssh-keyscan
|
||||
- 2020-06-15 AUTH-2813 adds back a single file support a cloudflared log file
|
||||
|
||||
2020.6.2
|
||||
- 2020-06-11 AUTH-2648 updated usage text
|
||||
- 2020-06-11 AUTH-2763 don't redirect from curl command
|
||||
- 2020-06-12 TUN-3090: Upgrade crypto dep
|
||||
- 2020-06-11 TUN-3038: Add connections to tunnel list table
|
||||
- 2020-06-12 AUTH-2810 added warn for backwards compatibility sake
|
||||
|
||||
2020.6.1
|
||||
- 2020-06-09 AUTH-2796 fixed windows build
|
||||
|
||||
2020.6.0
|
||||
- 2020-06-05 AUTH-2645 protect against user mistaken flag input
|
||||
- 2020-06-05 AUTH-2687 don't copy config unnecessarily
|
||||
- 2020-06-05 AUTH-2169 make access login page more generic
|
||||
- 2020-06-05 AUTH-2729 added log file and level to cmd flags to match config file settings
|
||||
- 2020-06-08 AUTH-2785 service token flag fix and logger fix
|
||||
- 2020-05-20 AUTH-2682: Create buster build
|
||||
- 2020-05-21 TUN-2928, TUN-2929, TUN-2930: Add tunnel subcommands to interact with tunnel store service
|
||||
- 2020-05-29 Adding support for multi-architecture images and binaries (#184)
|
||||
- 2020-05-29 TUN-3019: Remove declarative tunnel entry code
|
||||
- 2020-05-29 TUN-3020: Remove declarative tunnel related RPC code
|
||||
- 2020-05-13 AUTH-2505 added aliases
|
||||
- 2020-05-14 AUTH-2529 added deprecation text to db-connect command
|
||||
- 2020-05-18 AUTH-2686: Added error handling to tunnel subcommand
|
||||
- 2020-05-04 AUTH-2369: RDP Bastion prototype
|
||||
- 2020-04-29 AUTH-2596 added new logger package and replaced logrus
|
||||
- 2020-04-25 DEVTOOLS-7321: Use SSH key from env for pushing to GitHub
|
||||
- 2020-04-25 DEVTOOLS-7321: Push to a test branch instead of to master
|
||||
- 2020-03-30 DEVTOOLS-7321: Add scripts for macOS builds and homebrew uploads
|
||||
|
||||
2020.5.1
|
||||
- 2020-05-07 TUN-2860: Enable quick reconnect feature by default
|
||||
- 2020-05-07 AUTH-2564: error handling and minor fixes
|
||||
|
||||
@@ -5,12 +5,12 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
// DirectoryUploadManager is used to manage file uploads on an interval from a directory
|
||||
type DirectoryUploadManager struct {
|
||||
logger *logrus.Logger
|
||||
logger logger.Service
|
||||
uploader Uploader
|
||||
rootDirectory string
|
||||
sweepInterval time.Duration
|
||||
@@ -23,7 +23,7 @@ type DirectoryUploadManager struct {
|
||||
// uploader is an Uploader to use as an actual uploading engine
|
||||
// directory is the directory to sweep for files to upload
|
||||
// sweepInterval is how often to iterate the directory and upload the files within
|
||||
func NewDirectoryUploadManager(logger *logrus.Logger, uploader Uploader, directory string, sweepInterval time.Duration, shutdownC chan struct{}) *DirectoryUploadManager {
|
||||
func NewDirectoryUploadManager(logger logger.Service, uploader Uploader, directory string, sweepInterval time.Duration, shutdownC chan struct{}) *DirectoryUploadManager {
|
||||
workerCount := 10
|
||||
manager := &DirectoryUploadManager{
|
||||
logger: logger,
|
||||
@@ -97,7 +97,7 @@ func (m *DirectoryUploadManager) worker() {
|
||||
return
|
||||
case filepath := <-m.workQueue:
|
||||
if err := m.Upload(filepath); err != nil {
|
||||
m.logger.WithError(err).Error("Cannot upload file to s3 bucket")
|
||||
m.logger.Errorf("Cannot upload file to s3 bucket: %s", err)
|
||||
} else {
|
||||
os.Remove(filepath)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
type MockUploader struct {
|
||||
@@ -49,7 +49,7 @@ func setupTestDirectory(t *testing.T) string {
|
||||
func createUploadManager(t *testing.T, shouldFailUpload bool) *DirectoryUploadManager {
|
||||
rootDirectory := setupTestDirectory(t)
|
||||
uploader := NewMockUploader(shouldFailUpload)
|
||||
logger := logrus.New()
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
shutdownC := make(chan struct{})
|
||||
return NewDirectoryUploadManager(logger, uploader, rootDirectory, 1*time.Second, shutdownC)
|
||||
}
|
||||
|
||||
+7
-10
@@ -11,6 +11,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -64,7 +66,7 @@ func StartForwarder(conn Connection, address string, shutdownC <-chan struct{},
|
||||
// StartClient will copy the data from stdin/stdout over a WebSocket connection
|
||||
// to the edge (originURL)
|
||||
func StartClient(conn Connection, stream io.ReadWriter, options *StartOptions) error {
|
||||
return serveStream(conn, stream, options)
|
||||
return conn.ServeStream(options, stream)
|
||||
}
|
||||
|
||||
// Serve accepts incoming connections on the specified net.Listener.
|
||||
@@ -101,12 +103,7 @@ func Serve(remoteConn Connection, listener net.Listener, shutdownC <-chan struct
|
||||
// serveConnection handles connections for the Serve() call
|
||||
func serveConnection(remoteConn Connection, c net.Conn, options *StartOptions) {
|
||||
defer c.Close()
|
||||
serveStream(remoteConn, c, options)
|
||||
}
|
||||
|
||||
// serveStream will serve the data over the WebSocket stream
|
||||
func serveStream(remoteConn Connection, conn io.ReadWriter, options *StartOptions) error {
|
||||
return remoteConn.ServeStream(options, conn)
|
||||
remoteConn.ServeStream(options, c)
|
||||
}
|
||||
|
||||
// IsAccessResponse checks the http Response to see if the url location
|
||||
@@ -128,13 +125,13 @@ func IsAccessResponse(resp *http.Response) bool {
|
||||
}
|
||||
|
||||
// BuildAccessRequest builds an HTTP request with the Access token set
|
||||
func BuildAccessRequest(options *StartOptions) (*http.Request, error) {
|
||||
func BuildAccessRequest(options *StartOptions, logger logger.Service) (*http.Request, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := token.FetchToken(req.URL)
|
||||
token, err := token.FetchTokenWithRedirect(req.URL, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -145,7 +142,7 @@ func BuildAccessRequest(options *StartOptions) (*http.Request, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
originRequest.Header.Set("cf-access-token", token)
|
||||
originRequest.Header.Set(h2mux.CFAccessTokenHeader, token)
|
||||
|
||||
for k, v := range options.Headers {
|
||||
if len(v) >= 1 {
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
ws "github.com/gorilla/websocket"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -43,7 +43,7 @@ func (s *testStreamer) Write(p []byte) (int, error) {
|
||||
|
||||
func TestStartClient(t *testing.T) {
|
||||
message := "Good morning Austin! Time for another sunny day in the great state of Texas."
|
||||
logger := logrus.New()
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
wsConn := NewWSConnection(logger, false)
|
||||
ts := newTestWebSocketServer()
|
||||
defer ts.Close()
|
||||
@@ -68,7 +68,7 @@ func TestStartServer(t *testing.T) {
|
||||
t.Fatalf("Error starting listener: %v", err)
|
||||
}
|
||||
message := "Good morning Austin! Time for another sunny day in the great state of Texas."
|
||||
logger := logrus.New()
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
shutdownC := make(chan struct{})
|
||||
wsConn := NewWSConnection(logger, false)
|
||||
ts := newTestWebSocketServer()
|
||||
|
||||
+20
-12
@@ -5,18 +5,19 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/socks"
|
||||
cfwebsocket "github.com/cloudflare/cloudflared/websocket"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Websocket is used to carry data via WS binary frames over the tunnel from client to the origin
|
||||
// This implements the functions for glider proxy (sock5) and the carrier interface
|
||||
type Websocket struct {
|
||||
logger *logrus.Logger
|
||||
logger logger.Service
|
||||
isSocks bool
|
||||
}
|
||||
|
||||
@@ -35,7 +36,7 @@ func (d *wsdialer) Dial(address string) (io.ReadWriteCloser, *socks.AddrSpec, er
|
||||
}
|
||||
|
||||
// NewWSConnection returns a new connection object
|
||||
func NewWSConnection(logger *logrus.Logger, isSocks bool) Connection {
|
||||
func NewWSConnection(logger logger.Service, isSocks bool) Connection {
|
||||
return &Websocket{
|
||||
logger: logger,
|
||||
isSocks: isSocks,
|
||||
@@ -45,9 +46,9 @@ func NewWSConnection(logger *logrus.Logger, isSocks bool) Connection {
|
||||
// ServeStream will create a Websocket client stream connection to the edge
|
||||
// it blocks and writes the raw data from conn over the tunnel
|
||||
func (ws *Websocket) ServeStream(options *StartOptions, conn io.ReadWriter) error {
|
||||
wsConn, err := createWebsocketStream(options)
|
||||
wsConn, err := createWebsocketStream(options, ws.logger)
|
||||
if err != nil {
|
||||
ws.logger.WithError(err).Errorf("failed to connect to %s", options.OriginURL)
|
||||
ws.logger.Errorf("failed to connect to %s with error: %s", options.OriginURL, err)
|
||||
return err
|
||||
}
|
||||
defer wsConn.Close()
|
||||
@@ -73,17 +74,20 @@ func (ws *Websocket) StartServer(listener net.Listener, remote string, shutdownC
|
||||
// createWebsocketStream will create a WebSocket connection to stream data over
|
||||
// It also handles redirects from Access and will present that flow if
|
||||
// the token is not present on the request
|
||||
func createWebsocketStream(options *StartOptions) (*cfwebsocket.Conn, error) {
|
||||
func createWebsocketStream(options *StartOptions, logger logger.Service) (*cfwebsocket.Conn, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header = options.Headers
|
||||
|
||||
dump, err := httputil.DumpRequest(req, false)
|
||||
logger.Debugf("Websocket request: %s", string(dump))
|
||||
|
||||
wsConn, resp, err := cfwebsocket.ClientConnect(req, nil)
|
||||
defer closeRespBody(resp)
|
||||
if err != nil && IsAccessResponse(resp) {
|
||||
wsConn, err = createAccessAuthenticatedStream(options)
|
||||
wsConn, err = createAccessAuthenticatedStream(options, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -99,8 +103,8 @@ func createWebsocketStream(options *StartOptions) (*cfwebsocket.Conn, error) {
|
||||
// this probably means the token in storage is invalid (expired/revoked). If that
|
||||
// happens it deletes the token and runs the connection again, so the user can
|
||||
// login again and generate a new one.
|
||||
func createAccessAuthenticatedStream(options *StartOptions) (*websocket.Conn, error) {
|
||||
wsConn, resp, err := createAccessWebSocketStream(options)
|
||||
func createAccessAuthenticatedStream(options *StartOptions, logger logger.Service) (*websocket.Conn, error) {
|
||||
wsConn, resp, err := createAccessWebSocketStream(options, logger)
|
||||
defer closeRespBody(resp)
|
||||
if err == nil {
|
||||
return wsConn, nil
|
||||
@@ -118,7 +122,7 @@ func createAccessAuthenticatedStream(options *StartOptions) (*websocket.Conn, er
|
||||
if err := token.RemoveTokenIfExists(originReq.URL); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wsConn, resp, err = createAccessWebSocketStream(options)
|
||||
wsConn, resp, err = createAccessWebSocketStream(options, logger)
|
||||
defer closeRespBody(resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -128,10 +132,14 @@ func createAccessAuthenticatedStream(options *StartOptions) (*websocket.Conn, er
|
||||
}
|
||||
|
||||
// createAccessWebSocketStream builds an Access request and makes a connection
|
||||
func createAccessWebSocketStream(options *StartOptions) (*websocket.Conn, *http.Response, error) {
|
||||
req, err := BuildAccessRequest(options)
|
||||
func createAccessWebSocketStream(options *StartOptions, logger logger.Service) (*websocket.Conn, *http.Response, error) {
|
||||
req, err := BuildAccessRequest(options, logger)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
dump, err := httputil.DumpRequest(req, false)
|
||||
logger.Debugf("Access Websocket request: %s", string(dump))
|
||||
|
||||
return cfwebsocket.ClientConnect(req, nil)
|
||||
}
|
||||
|
||||
+99
-1
@@ -1,4 +1,4 @@
|
||||
pinned_go: &pinned_go go=1.12.7-1
|
||||
pinned_go: &pinned_go go=1.12.9-1
|
||||
build_dir: &build_dir /cfsetup_build
|
||||
default-flavor: stretch
|
||||
stretch: &stretch
|
||||
@@ -22,6 +22,18 @@ stretch: &stretch
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared-deb
|
||||
publish-deb:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
- openssh-client
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make publish-deb
|
||||
release-linux-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
@@ -31,6 +43,18 @@ stretch: &stretch
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make release
|
||||
github-release-linux-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
post-cache:
|
||||
- pip3 install pygithub
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make github-release
|
||||
release-linux-armv6:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
@@ -42,6 +66,20 @@ stretch: &stretch
|
||||
- export GOARCH=arm
|
||||
- export CC=arm-linux-gnueabihf-gcc
|
||||
- make release
|
||||
github-release-linux-armv6:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- crossbuild-essential-armhf
|
||||
- gcc-arm-linux-gnueabihf
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
post-cache:
|
||||
- pip3 install pygithub
|
||||
- export GOOS=linux
|
||||
- export GOARCH=arm
|
||||
- export CC=arm-linux-gnueabihf-gcc
|
||||
- make github-release
|
||||
release-linux-386:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
@@ -51,6 +89,18 @@ stretch: &stretch
|
||||
- export GOOS=linux
|
||||
- export GOARCH=386
|
||||
- make release
|
||||
github-release-linux-386:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-multilib
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
post-cache:
|
||||
- pip3 install pygithub
|
||||
- export GOOS=linux
|
||||
- export GOARCH=386
|
||||
- make github-release
|
||||
release-windows-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
@@ -61,6 +111,19 @@ stretch: &stretch
|
||||
- export GOARCH=amd64
|
||||
- export CC=x86_64-w64-mingw32-gcc
|
||||
- make release
|
||||
github-release-windows-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-mingw-w64
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
post-cache:
|
||||
- pip3 install pygithub
|
||||
- export GOOS=windows
|
||||
- export GOARCH=amd64
|
||||
- export CC=x86_64-w64-mingw32-gcc
|
||||
- make github-release
|
||||
release-windows-386:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
@@ -71,6 +134,19 @@ stretch: &stretch
|
||||
- export GOARCH=386
|
||||
- export CC=i686-w64-mingw32-gcc-win32
|
||||
- make release
|
||||
github-release-windows-386:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-mingw-w64
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
post-cache:
|
||||
- pip3 install pygithub
|
||||
- export GOOS=windows
|
||||
- export GOARCH=386
|
||||
- export CC=i686-w64-mingw32-gcc-win32
|
||||
- make github-release
|
||||
test:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
@@ -83,9 +159,31 @@ stretch: &stretch
|
||||
- (cd / && go get github.com/BurntSushi/go-sumtype)
|
||||
- export PATH="$HOME/go/bin:$PATH"
|
||||
- make test
|
||||
update-homebrew:
|
||||
builddeps:
|
||||
- openssh-client
|
||||
- s3cmd
|
||||
post-cache:
|
||||
- .teamcity/update-homebrew.sh
|
||||
|
||||
jessie: *stretch
|
||||
|
||||
buster: *stretch
|
||||
|
||||
centos-7:
|
||||
publish-rpm:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
|
||||
post-cache:
|
||||
- sudo yum install -y fakeroot
|
||||
- wget https://golang.org/dl/go1.12.9.linux-amd64.tar.gz -P /tmp/
|
||||
- sudo tar -C /usr/local -xzf /tmp/go1.12.9.linux-amd64.tar.gz
|
||||
- export PATH=$PATH:/usr/local/go/bin
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make publish-rpm
|
||||
|
||||
# cfsetup compose
|
||||
default-stack: test_dbconnect
|
||||
test_dbconnect:
|
||||
|
||||
@@ -6,29 +6,44 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/carrier"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
"github.com/pkg/errors"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
// StartForwarder starts a client side websocket forward
|
||||
func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}) error {
|
||||
func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, logger logger.Service) error {
|
||||
validURLString, err := validation.ValidateUrl(forwarder.Listener)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error validating origin URL")
|
||||
return errors.Wrap(err, "error validating origin URL")
|
||||
}
|
||||
|
||||
validURL, err := url.Parse(validURLString)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error parsing origin URL")
|
||||
return errors.Wrap(err, "error parsing origin URL")
|
||||
}
|
||||
|
||||
// get the headers from the config file and add to the request
|
||||
headers := make(http.Header)
|
||||
if forwarder.TokenClientID != "" {
|
||||
headers.Set(h2mux.CFAccessClientIDHeader, forwarder.TokenClientID)
|
||||
}
|
||||
|
||||
if forwarder.TokenSecret != "" {
|
||||
headers.Set(h2mux.CFAccessClientSecretHeader, forwarder.TokenSecret)
|
||||
}
|
||||
|
||||
if forwarder.Destination != "" {
|
||||
headers.Add(h2mux.CFJumpDestinationHeader, forwarder.Destination)
|
||||
}
|
||||
|
||||
options := &carrier.StartOptions{
|
||||
OriginURL: forwarder.URL,
|
||||
Headers: make(http.Header), //TODO: TUN-2688 support custom headers from config file
|
||||
Headers: headers, //TODO: TUN-2688 support custom headers from config file
|
||||
}
|
||||
|
||||
// we could add a cmd line variable for this bool if we want the SOCK5 server to be on the client side
|
||||
@@ -43,6 +58,23 @@ func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}) error
|
||||
// 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))
|
||||
if err != nil {
|
||||
return cliutil.PrintLoggerSetupError("error setting up logger", err)
|
||||
}
|
||||
|
||||
// get the hostname from the cmdline and error out if its not provided
|
||||
rawHostName := c.String(sshHostnameFlag)
|
||||
hostname, err := validation.ValidateHostname(rawHostName)
|
||||
@@ -54,15 +86,15 @@ func ssh(c *cli.Context) error {
|
||||
// get the headers from the cmdline and add them
|
||||
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
|
||||
if c.IsSet(sshTokenIDFlag) {
|
||||
headers.Add("CF-Access-Client-Id", c.String(sshTokenIDFlag))
|
||||
headers.Set(h2mux.CFAccessClientIDHeader, c.String(sshTokenIDFlag))
|
||||
}
|
||||
if c.IsSet(sshTokenSecretFlag) {
|
||||
headers.Add("CF-Access-Client-Secret", c.String(sshTokenSecretFlag))
|
||||
headers.Set(h2mux.CFAccessClientSecretHeader, c.String(sshTokenSecretFlag))
|
||||
}
|
||||
|
||||
destination := c.String(sshDestinationFlag)
|
||||
if destination != "" {
|
||||
headers.Add("CF-Access-SSH-Destination", destination)
|
||||
headers.Add(h2mux.CFJumpDestinationHeader, destination)
|
||||
}
|
||||
|
||||
options := &carrier.StartOptions{
|
||||
@@ -74,19 +106,23 @@ func ssh(c *cli.Context) error {
|
||||
wsConn := carrier.NewWSConnection(logger, false)
|
||||
|
||||
if c.NArg() > 0 || c.IsSet(sshURLFlag) {
|
||||
localForwarder, err := config.ValidateUrl(c)
|
||||
localForwarder, err := config.ValidateUrl(c, true)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error validating origin URL")
|
||||
logger.Errorf("Error validating origin URL: %s", err)
|
||||
return errors.Wrap(err, "error validating origin URL")
|
||||
}
|
||||
forwarder, err := url.Parse(localForwarder)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error validating origin URL")
|
||||
logger.Errorf("Error validating origin URL: %s", err)
|
||||
return errors.Wrap(err, "error validating origin URL")
|
||||
}
|
||||
|
||||
logger.Infof("Start Websocket listener on: %s", forwarder.Host)
|
||||
return carrier.StartForwarder(wsConn, forwarder.Host, shutdownC, options)
|
||||
err = carrier.StartForwarder(wsConn, forwarder.Host, shutdownC, options)
|
||||
if err != nil {
|
||||
logger.Errorf("Error on Websocket listener: %s", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return carrier.StartClient(wsConn, &carrier.StdinoutStream{}, options)
|
||||
|
||||
@@ -10,27 +10,31 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/carrier"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/sshgen"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/idna"
|
||||
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
"github.com/getsentry/raven-go"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
sshHostnameFlag = "hostname"
|
||||
sshDestinationFlag = "destination"
|
||||
sshURLFlag = "url"
|
||||
sshHeaderFlag = "header"
|
||||
sshTokenIDFlag = "service-token-id"
|
||||
sshTokenSecretFlag = "service-token-secret"
|
||||
sshGenCertFlag = "short-lived-cert"
|
||||
sshConfigTemplate = `
|
||||
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 = `
|
||||
Add to your {{.Home}}/.ssh/config:
|
||||
|
||||
Host {{.Hostname}}
|
||||
@@ -51,7 +55,6 @@ Host cfpipe-{{.Hostname}}
|
||||
const sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b@sentry.io/189878"
|
||||
|
||||
var (
|
||||
logger = log.CreateLogger()
|
||||
shutdownC chan struct{}
|
||||
graceShutdownC chan struct{}
|
||||
)
|
||||
@@ -66,36 +69,22 @@ func Flags() []cli.Flag {
|
||||
return []cli.Flag{} // no flags yet.
|
||||
}
|
||||
|
||||
// Ensures exit with error code if actionFunc returns an error
|
||||
func errorHandler(actionFunc cli.ActionFunc) cli.ActionFunc {
|
||||
return func(ctx *cli.Context) error {
|
||||
err := actionFunc(ctx)
|
||||
|
||||
if err != nil {
|
||||
// os.Exits with error code if err is cli.ExitCoder or cli.MultiError
|
||||
cli.HandleExitCoder(err)
|
||||
err = cli.Exit(err.Error(), 1)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Commands returns all the Access related subcommands
|
||||
func Commands() []*cli.Command {
|
||||
return []*cli.Command{
|
||||
{
|
||||
Name: "access",
|
||||
Category: "Access (BETA)",
|
||||
Aliases: []string{"forward"},
|
||||
Category: "Access",
|
||||
Usage: "access <subcommand>",
|
||||
Description: `(BETA) Cloudflare Access protects internal resources by securing, authenticating and monitoring access
|
||||
Description: `Cloudflare Access protects internal resources by securing, authenticating and monitoring access
|
||||
per-user and by application. With Cloudflare Access, only authenticated users with the required permissions are
|
||||
able to reach sensitive resources. The commands provided here allow you to interact with Access protected
|
||||
applications from the command line. This feature is considered beta. Your feedback is greatly appreciated!
|
||||
https://cfl.re/CLIAuthBeta`,
|
||||
applications from the command line.`,
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "login",
|
||||
Action: errorHandler(login),
|
||||
Action: cliutil.ErrorHandler(login),
|
||||
Usage: "login <url of access application>",
|
||||
Description: `The login subcommand initiates an authentication flow with your identity provider.
|
||||
The subcommand will launch a browser. For headless systems, a url is provided.
|
||||
@@ -111,7 +100,7 @@ func Commands() []*cli.Command {
|
||||
},
|
||||
{
|
||||
Name: "curl",
|
||||
Action: errorHandler(curl),
|
||||
Action: cliutil.ErrorHandler(curl),
|
||||
Usage: "curl [--allow-request, -ar] <url> [<curl args>...]",
|
||||
Description: `The curl subcommand wraps curl and automatically injects the JWT into a cf-access-token
|
||||
header when using curl to reach an application behind Access.`,
|
||||
@@ -120,7 +109,7 @@ func Commands() []*cli.Command {
|
||||
},
|
||||
{
|
||||
Name: "token",
|
||||
Action: errorHandler(generateToken),
|
||||
Action: cliutil.ErrorHandler(generateToken),
|
||||
Usage: "token -app=<url of access application>",
|
||||
ArgsUsage: "url of Access application",
|
||||
Description: `The token subcommand produces a JWT which can be used to authenticate requests.`,
|
||||
@@ -131,45 +120,57 @@ func Commands() []*cli.Command {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ssh",
|
||||
Action: errorHandler(ssh),
|
||||
Aliases: []string{"rdp", "tcp"},
|
||||
Name: "tcp",
|
||||
Action: cliutil.ErrorHandler(ssh),
|
||||
Aliases: []string{"rdp", "ssh", "smb"},
|
||||
Usage: "",
|
||||
ArgsUsage: "",
|
||||
Description: `The ssh subcommand sends data over a proxy to the Cloudflare edge.`,
|
||||
Description: `The tcp subcommand sends data over a proxy to the Cloudflare edge.`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: sshHostnameFlag,
|
||||
Usage: "specify the hostname of your application.",
|
||||
Name: sshHostnameFlag,
|
||||
Aliases: []string{"tunnel-host", "T"},
|
||||
Usage: "specify the hostname of your application.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshDestinationFlag,
|
||||
Usage: "specify the destination address of your SSH server.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshURLFlag,
|
||||
Usage: "specify the host:port to forward data to Cloudflare edge.",
|
||||
Name: sshURLFlag,
|
||||
Aliases: []string{"listener", "L"},
|
||||
Usage: "specify the host:port to forward data to Cloudflare edge.",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: sshHeaderFlag,
|
||||
Aliases: []string{"H"},
|
||||
Usage: "specify additional headers you wish to send.",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
&cli.StringFlag{
|
||||
Name: sshTokenIDFlag,
|
||||
Aliases: []string{"id"},
|
||||
Usage: "specify an Access service token ID you wish to use.",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
&cli.StringFlag{
|
||||
Name: sshTokenSecretFlag,
|
||||
Aliases: []string{"secret"},
|
||||
Usage: "specify an Access service token secret you wish to use.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshLogDirectoryFlag,
|
||||
Aliases: []string{"logfile"}, //added to match the tunnel side
|
||||
Usage: "Save application log to this directory for reporting issues.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshLogLevelFlag,
|
||||
Aliases: []string{"loglevel"}, //added to match the tunnel side
|
||||
Usage: "Application logging level {fatal, error, info, debug}. ",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ssh-config",
|
||||
Action: errorHandler(sshConfig),
|
||||
Action: cliutil.ErrorHandler(sshConfig),
|
||||
Usage: "",
|
||||
Description: `Prints an example configuration ~/.ssh/config`,
|
||||
Flags: []cli.Flag{
|
||||
@@ -185,7 +186,7 @@ func Commands() []*cli.Command {
|
||||
},
|
||||
{
|
||||
Name: "ssh-gen",
|
||||
Action: errorHandler(sshGen),
|
||||
Action: cliutil.ErrorHandler(sshGen),
|
||||
Usage: "",
|
||||
Description: `Generates a short lived certificate for given hostname`,
|
||||
Flags: []cli.Flag{
|
||||
@@ -205,7 +206,12 @@ func login(c *cli.Context) error {
|
||||
if err := raven.SetDSN(sentryDSN); err != nil {
|
||||
return err
|
||||
}
|
||||
logger := log.CreateLogger()
|
||||
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
args := c.Args()
|
||||
rawURL := ensureURLScheme(args.First())
|
||||
appURL, err := url.Parse(rawURL)
|
||||
@@ -213,8 +219,8 @@ func login(c *cli.Context) error {
|
||||
logger.Errorf("Please provide the url of the Access application\n")
|
||||
return err
|
||||
}
|
||||
if err := verifyTokenAtEdge(appURL, c); err != nil {
|
||||
logger.WithError(err).Error("Could not verify token")
|
||||
if err := verifyTokenAtEdge(appURL, c, logger); err != nil {
|
||||
logger.Errorf("Could not verify token: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -246,7 +252,11 @@ func curl(c *cli.Context) error {
|
||||
if err := raven.SetDSN(sentryDSN); err != nil {
|
||||
return err
|
||||
}
|
||||
logger := log.CreateLogger()
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
args := c.Args()
|
||||
if args.Len() < 1 {
|
||||
logger.Error("Please provide the access app and command you wish to run.")
|
||||
@@ -254,7 +264,7 @@ func curl(c *cli.Context) error {
|
||||
}
|
||||
|
||||
cmdArgs, allowRequest := parseAllowRequest(args.Slice())
|
||||
appURL, err := getAppURL(cmdArgs)
|
||||
appURL, err := getAppURL(cmdArgs, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -262,18 +272,18 @@ func curl(c *cli.Context) error {
|
||||
tok, err := token.GetTokenIfExists(appURL)
|
||||
if err != nil || tok == "" {
|
||||
if allowRequest {
|
||||
logger.Warn("You don't have an Access token set. Please run access token <access application> to fetch one.")
|
||||
logger.Info("You don't have an Access token set. Please run access token <access application> to fetch one.")
|
||||
return shell.Run("curl", cmdArgs...)
|
||||
}
|
||||
tok, err = token.FetchToken(appURL)
|
||||
tok, err = token.FetchToken(appURL, logger)
|
||||
if err != nil {
|
||||
logger.Error("Failed to refresh token: ", err)
|
||||
logger.Errorf("Failed to refresh token: %s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cmdArgs = append(cmdArgs, "-H")
|
||||
cmdArgs = append(cmdArgs, fmt.Sprintf("cf-access-token: %s", tok))
|
||||
cmdArgs = append(cmdArgs, fmt.Sprintf("%s: %s", h2mux.CFAccessTokenHeader, tok))
|
||||
return shell.Run("curl", cmdArgs...)
|
||||
}
|
||||
|
||||
@@ -321,6 +331,11 @@ 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()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
// get the hostname from the cmdline and error out if its not provided
|
||||
rawHostName := c.String(sshHostnameFlag)
|
||||
hostname, err := validation.ValidateHostname(rawHostName)
|
||||
@@ -336,7 +351,7 @@ func sshGen(c *cli.Context) error {
|
||||
// this fetchToken function mutates the appURL param. We should refactor that
|
||||
fetchTokenURL := &url.URL{}
|
||||
*fetchTokenURL = *originURL
|
||||
cfdToken, err := token.FetchToken(fetchTokenURL)
|
||||
cfdToken, err := token.FetchTokenWithRedirect(fetchTokenURL, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -349,7 +364,7 @@ func sshGen(c *cli.Context) error {
|
||||
}
|
||||
|
||||
// getAppURL will pull the appURL needed for fetching a user's Access token
|
||||
func getAppURL(cmdArgs []string) (*url.URL, error) {
|
||||
func getAppURL(cmdArgs []string, logger logger.Service) (*url.URL, error) {
|
||||
if len(cmdArgs) < 1 {
|
||||
logger.Error("Please provide a valid URL as the first argument to curl.")
|
||||
return nil, errors.New("not a valid url")
|
||||
@@ -423,17 +438,17 @@ func isFileThere(candidate string) bool {
|
||||
// verifyTokenAtEdge checks for a token on disk, or generates a new one.
|
||||
// Then makes a request to to the origin with the token to ensure it is valid.
|
||||
// Returns nil if token is valid.
|
||||
func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context) error {
|
||||
func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context, logger logger.Service) error {
|
||||
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
|
||||
if c.IsSet(sshTokenIDFlag) {
|
||||
headers.Add("CF-Access-Client-Id", c.String(sshTokenIDFlag))
|
||||
headers.Add(h2mux.CFAccessClientIDHeader, c.String(sshTokenIDFlag))
|
||||
}
|
||||
if c.IsSet(sshTokenSecretFlag) {
|
||||
headers.Add("CF-Access-Client-Secret", c.String(sshTokenSecretFlag))
|
||||
headers.Add(h2mux.CFAccessClientSecretHeader, c.String(sshTokenSecretFlag))
|
||||
}
|
||||
options := &carrier.StartOptions{OriginURL: appUrl.String(), Headers: headers}
|
||||
|
||||
if valid, err := isTokenValid(options); err != nil {
|
||||
if valid, err := isTokenValid(options, logger); err != nil {
|
||||
return err
|
||||
} else if valid {
|
||||
return nil
|
||||
@@ -443,7 +458,7 @@ func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if valid, err := isTokenValid(options); err != nil {
|
||||
if valid, err := isTokenValid(options, logger); err != nil {
|
||||
return err
|
||||
} else if !valid {
|
||||
return errors.New("failed to verify token")
|
||||
@@ -453,8 +468,8 @@ func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context) error {
|
||||
}
|
||||
|
||||
// isTokenValid makes a request to the origin and returns true if the response was not a 302.
|
||||
func isTokenValid(options *carrier.StartOptions) (bool, error) {
|
||||
req, err := carrier.BuildAccessRequest(options)
|
||||
func isTokenValid(options *carrier.StartOptions, logger logger.Service) (bool, error) {
|
||||
req, err := carrier.BuildAccessRequest(options, logger)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "Could not create access request")
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
// ForwardServiceType is used to identify what kind of overwatch service this is
|
||||
@@ -14,11 +15,12 @@ const ForwardServiceType = "forward"
|
||||
type ForwarderService struct {
|
||||
forwarder config.Forwarder
|
||||
shutdown chan struct{}
|
||||
logger logger.Service
|
||||
}
|
||||
|
||||
// NewForwardService creates a new forwarder service
|
||||
func NewForwardService(f config.Forwarder) *ForwarderService {
|
||||
return &ForwarderService{forwarder: f, shutdown: make(chan struct{}, 1)}
|
||||
func NewForwardService(f config.Forwarder, logger logger.Service) *ForwarderService {
|
||||
return &ForwarderService{forwarder: f, shutdown: make(chan struct{}, 1), logger: logger}
|
||||
}
|
||||
|
||||
// Name is used to figure out this service is related to the others (normally the addr it binds to)
|
||||
@@ -44,5 +46,5 @@ func (s *ForwarderService) Shutdown() {
|
||||
|
||||
// Run is the run loop that is started by the overwatch service
|
||||
func (s *ForwarderService) Run() error {
|
||||
return access.StartForwarder(s.forwarder, s.shutdown)
|
||||
return access.StartForwarder(s.forwarder, s.shutdown, s.logger)
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ResolverServiceType is used to identify what kind of overwatch service this is
|
||||
@@ -15,11 +15,11 @@ const ResolverServiceType = "resolver"
|
||||
type ResolverService struct {
|
||||
resolver config.DNSResolver
|
||||
shutdown chan struct{}
|
||||
logger *logrus.Logger
|
||||
logger logger.Service
|
||||
}
|
||||
|
||||
// NewResolverService creates a new resolver service
|
||||
func NewResolverService(r config.DNSResolver, logger *logrus.Logger) *ResolverService {
|
||||
func NewResolverService(r config.DNSResolver, logger logger.Service) *ResolverService {
|
||||
return &ResolverService{resolver: r,
|
||||
shutdown: make(chan struct{}),
|
||||
logger: logger,
|
||||
@@ -51,7 +51,7 @@ func (s *ResolverService) Shutdown() {
|
||||
func (s *ResolverService) Run() error {
|
||||
// create a listener
|
||||
l, err := tunneldns.CreateListener(s.resolver.AddressOrDefault(), s.resolver.PortOrDefault(),
|
||||
s.resolver.UpstreamsOrDefault(), s.resolver.BootstrapsOrDefault())
|
||||
s.resolver.UpstreamsOrDefault(), s.resolver.BootstrapsOrDefault(), s.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/overwatch"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// AppService is the main service that runs when no command lines flags are passed to cloudflared
|
||||
@@ -13,11 +13,11 @@ type AppService struct {
|
||||
serviceManager overwatch.Manager
|
||||
shutdownC chan struct{}
|
||||
configUpdateChan chan config.Root
|
||||
logger *logrus.Logger
|
||||
logger logger.Service
|
||||
}
|
||||
|
||||
// NewAppService creates a new AppService with needed supporting services
|
||||
func NewAppService(configManager config.Manager, serviceManager overwatch.Manager, shutdownC chan struct{}, logger *logrus.Logger) *AppService {
|
||||
func NewAppService(configManager config.Manager, serviceManager overwatch.Manager, shutdownC chan struct{}, logger logger.Service) *AppService {
|
||||
return &AppService{
|
||||
configManager: configManager,
|
||||
serviceManager: serviceManager,
|
||||
@@ -66,7 +66,7 @@ func (s *AppService) handleConfigUpdate(c config.Root) {
|
||||
// handle the client forward listeners
|
||||
activeServices := map[string]struct{}{}
|
||||
for _, f := range c.Forwarders {
|
||||
service := NewForwardService(f)
|
||||
service := NewForwardService(f, s.logger)
|
||||
s.serviceManager.Add(service)
|
||||
activeServices[service.Name()] = struct{}{}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package buildinfo
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
type BuildInfo struct {
|
||||
@@ -22,7 +22,7 @@ func GetBuildInfo(cloudflaredVersion string) *BuildInfo {
|
||||
}
|
||||
}
|
||||
|
||||
func (bi *BuildInfo) Log(logger *logrus.Logger) {
|
||||
func (bi *BuildInfo) Log(logger logger.Service) {
|
||||
logger.Infof("Version %s", bi.CloudflaredVersion)
|
||||
logger.Infof("GOOS: %s, GOVersion: %s, GoArch: %s", bi.GoOS, bi.GoVersion, bi.GoArch)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package cliutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
type usageError string
|
||||
|
||||
func (ue usageError) Error() string {
|
||||
return string(ue)
|
||||
}
|
||||
|
||||
func UsageError(format string, args ...interface{}) error {
|
||||
if len(args) == 0 {
|
||||
return usageError(format)
|
||||
} else {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
return usageError(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensures exit with error code if actionFunc returns an error
|
||||
func ErrorHandler(actionFunc cli.ActionFunc) cli.ActionFunc {
|
||||
return func(ctx *cli.Context) error {
|
||||
err := actionFunc(ctx)
|
||||
if err != nil {
|
||||
if _, ok := err.(usageError); ok {
|
||||
msg := fmt.Sprintf("%s\nSee 'cloudflared %s --help'.", err.Error(), ctx.Command.FullName())
|
||||
return cli.Exit(msg, -1)
|
||||
}
|
||||
// os.Exits with error code if err is cli.ExitCoder or cli.MultiError
|
||||
cli.HandleExitCoder(err)
|
||||
err = cli.Exit(err.Error(), 1)
|
||||
}
|
||||
logger.SharedWriteManager.Shutdown()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// PrintLoggerSetupError returns an error to stdout to notify when a logger can't start
|
||||
func PrintLoggerSetupError(msg string, err error) error {
|
||||
l, le := logger.New()
|
||||
if le != nil {
|
||||
log.Printf("%s: %s", msg, err)
|
||||
} else {
|
||||
l.Errorf("%s: %s", msg, err)
|
||||
}
|
||||
|
||||
return errors.Wrap(err, msg)
|
||||
}
|
||||
@@ -4,24 +4,65 @@ import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
"gopkg.in/urfave/cli.v2/altsrc"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
)
|
||||
|
||||
var (
|
||||
// File names from which we attempt to read configuration.
|
||||
// DefaultConfigFiles is the file names from which we attempt to read configuration.
|
||||
DefaultConfigFiles = []string{"config.yml", "config.yaml"}
|
||||
|
||||
// DefaultUnixConfigLocation is the primary location to find a config file
|
||||
DefaultUnixConfigLocation = "/usr/local/etc/cloudflared"
|
||||
|
||||
// DefaultUnixLogLocation is the primary location to find log files
|
||||
DefaultUnixLogLocation = "/var/log/cloudflared"
|
||||
|
||||
// Launchd doesn't set root env variables, so there is default
|
||||
// Windows default config dir was ~/cloudflare-warp in documentation; let's keep it compatible
|
||||
DefaultConfigDirs = []string{"~/.cloudflared", "~/.cloudflare-warp", "~/cloudflare-warp", "/usr/local/etc/cloudflared", "/etc/cloudflared"}
|
||||
DefaultConfigDirs = []string{"~/.cloudflared", "~/.cloudflare-warp", "~/cloudflare-warp", "/etc/cloudflared", DefaultUnixConfigLocation}
|
||||
)
|
||||
|
||||
const DefaultCredentialFile = "cert.pem"
|
||||
|
||||
// DefaultConfigDirectory returns the default directory of the config file
|
||||
func DefaultConfigDirectory() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
path := os.Getenv("CFDPATH")
|
||||
if path == "" {
|
||||
path = filepath.Join(os.Getenv("ProgramFiles(x86)"), "cloudflared")
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) { //doesn't exist, so return an empty failure string
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
return DefaultUnixConfigLocation
|
||||
}
|
||||
|
||||
// DefaultLogDirectory returns the default directory for log files
|
||||
func DefaultLogDirectory() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return DefaultConfigDirectory()
|
||||
}
|
||||
return DefaultUnixLogLocation
|
||||
}
|
||||
|
||||
// DefaultConfigPath returns the default location of a config file
|
||||
func DefaultConfigPath() string {
|
||||
dir := DefaultConfigDirectory()
|
||||
if dir == "" {
|
||||
return DefaultConfigFiles[0]
|
||||
}
|
||||
return filepath.Join(dir, DefaultConfigFiles[0])
|
||||
}
|
||||
|
||||
// FileExists checks to see if a file exist at the provided path.
|
||||
func FileExists(path string) (bool, error) {
|
||||
f, err := os.Open(path)
|
||||
@@ -63,6 +104,68 @@ func FindDefaultConfigPath() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// FindOrCreateConfigPath returns the first path that contains a config file
|
||||
// or creates one in the primary default path if it doesn't exist
|
||||
func FindOrCreateConfigPath() string {
|
||||
path := FindDefaultConfigPath()
|
||||
|
||||
if path == "" {
|
||||
// create the default directory if it doesn't exist
|
||||
path = DefaultConfigPath()
|
||||
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// write a new config file out
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
logDir := DefaultLogDirectory()
|
||||
os.MkdirAll(logDir, os.ModePerm) //try and create it. Doesn't matter if it succeed or not, only byproduct will be no logs
|
||||
|
||||
c := Root{
|
||||
LogDirectory: logDir,
|
||||
}
|
||||
if err := yaml.NewEncoder(file).Encode(&c); err != nil {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -74,9 +177,9 @@ func ValidateUnixSocket(c *cli.Context) (string, error) {
|
||||
|
||||
// ValidateUrl will validate url flag correctness. It can be either from --url or argument
|
||||
// Notice ValidateUnixSocket, it will enforce --unix-socket is not used with --url or argument
|
||||
func ValidateUrl(c *cli.Context) (string, error) {
|
||||
func ValidateUrl(c *cli.Context, allowFromArgs bool) (string, error) {
|
||||
var url = c.String("url")
|
||||
if c.NArg() > 0 {
|
||||
if allowFromArgs && c.NArg() > 0 {
|
||||
if c.IsSet("url") {
|
||||
return "", errors.New("Specified origin urls using both --url and argument. Decide which one you want, I can only support one.")
|
||||
}
|
||||
|
||||
@@ -4,9 +4,8 @@ import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/watcher"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
@@ -27,12 +26,12 @@ type FileManager struct {
|
||||
watcher watcher.Notifier
|
||||
notifier Notifier
|
||||
configPath string
|
||||
logger *logrus.Logger
|
||||
logger logger.Service
|
||||
ReadConfig func(string) (Root, error)
|
||||
}
|
||||
|
||||
// NewFileManager creates a config manager
|
||||
func NewFileManager(watcher watcher.Notifier, configPath string, logger *logrus.Logger) (*FileManager, error) {
|
||||
func NewFileManager(watcher watcher.Notifier, configPath string, logger logger.Service) (*FileManager, error) {
|
||||
m := &FileManager{
|
||||
watcher: watcher,
|
||||
configPath: configPath,
|
||||
@@ -94,7 +93,7 @@ func readConfigFromPath(configPath string) (Root, error) {
|
||||
func (m *FileManager) WatcherItemDidChange(filepath string) {
|
||||
config, err := m.GetConfig()
|
||||
if err != nil {
|
||||
m.logger.WithError(err).Error("Failed to read new config")
|
||||
m.logger.Errorf("Failed to read new config: %s", err)
|
||||
return
|
||||
}
|
||||
m.logger.Info("Config file has been updated")
|
||||
@@ -103,5 +102,5 @@ func (m *FileManager) WatcherItemDidChange(filepath string) {
|
||||
|
||||
// WatcherDidError notifies of errors with the file watcher
|
||||
func (m *FileManager) WatcherDidError(err error) {
|
||||
m.logger.WithError(err).Error("Config watcher encountered an error")
|
||||
m.logger.Errorf("Config watcher encountered an error: %s", err)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/watcher"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -49,9 +49,6 @@ func TestConfigChanged(t *testing.T) {
|
||||
os.Remove(filePath)
|
||||
}()
|
||||
c := &Root{
|
||||
OrgKey: "abcd",
|
||||
ConfigType: "mytype",
|
||||
CheckinInterval: 1,
|
||||
Forwarders: []Forwarder{
|
||||
{
|
||||
URL: "test.daltoniam.com",
|
||||
@@ -65,7 +62,8 @@ func TestConfigChanged(t *testing.T) {
|
||||
wait := make(chan struct{})
|
||||
w := &mockFileWatcher{path: filePath, ready: wait}
|
||||
|
||||
logger := log.CreateLogger()
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
service, err := NewFileManager(w, filePath, logger)
|
||||
service.ReadConfig = configRead
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -9,8 +9,11 @@ import (
|
||||
|
||||
// Forwarder represents a client side listener to forward traffic to the edge
|
||||
type Forwarder struct {
|
||||
URL string `json:"url"`
|
||||
Listener string `json:"listener"`
|
||||
URL string `json:"url"`
|
||||
Listener string `json:"listener"`
|
||||
TokenClientID string `json:"service_token_id" yaml:"serviceTokenID"`
|
||||
TokenSecret string `json:"secret_token_id" yaml:"serviceTokenSecret"`
|
||||
Destination string `json:"destination"`
|
||||
}
|
||||
|
||||
// Tunnel represents a tunnel that should be started
|
||||
@@ -23,20 +26,19 @@ type Tunnel struct {
|
||||
// DNSResolver represents a client side DNS resolver
|
||||
type DNSResolver struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Address string `json:"address"`
|
||||
Port uint16 `json:"port"`
|
||||
Upstreams []string `json:"upstreams"`
|
||||
Bootstraps []string `json:"bootstraps"`
|
||||
Address string `json:"address,omitempty"`
|
||||
Port uint16 `json:"port,omitempty"`
|
||||
Upstreams []string `json:"upstreams,omitempty"`
|
||||
Bootstraps []string `json:"bootstraps,omitempty"`
|
||||
}
|
||||
|
||||
// Root is the base options to configure the service
|
||||
type Root struct {
|
||||
OrgKey string `json:"org_key"`
|
||||
ConfigType string `json:"type"`
|
||||
CheckinInterval int `json:"checkin_interval"`
|
||||
Forwarders []Forwarder `json:"forwarders,omitempty"`
|
||||
Tunnels []Tunnel `json:"tunnels,omitempty"`
|
||||
Resolver DNSResolver `json:"resolver"`
|
||||
LogDirectory string `json:"log_directory" yaml:"logDirectory,omitempty"`
|
||||
LogLevel string `json:"log_level" yaml:"logLevel,omitempty"`
|
||||
Forwarders []Forwarder `json:"forwarders,omitempty" yaml:"forwarders,omitempty"`
|
||||
Tunnels []Tunnel `json:"tunnels,omitempty" yaml:"tunnels,omitempty"`
|
||||
Resolver DNSResolver `json:"resolver,omitempty" yaml:"resolver,omitempty"`
|
||||
}
|
||||
|
||||
// Hash returns the computed values to see if the forwarder values change
|
||||
@@ -44,6 +46,9 @@ func (f *Forwarder) Hash() string {
|
||||
h := md5.New()
|
||||
io.WriteString(h, f.URL)
|
||||
io.WriteString(h, f.Listener)
|
||||
io.WriteString(h, f.TokenClientID)
|
||||
io.WriteString(h, f.TokenSecret)
|
||||
io.WriteString(h, f.Destination)
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/pkg/errors"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
@@ -178,24 +180,37 @@ func isSystemd() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile string) error {
|
||||
func copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile string, logger logger.Service) error {
|
||||
if err := ensureConfigDirExists(serviceConfigDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srcCredentialPath := filepath.Join(userConfigDir, userCredentialFile)
|
||||
destCredentialPath := filepath.Join(serviceConfigDir, serviceCredentialFile)
|
||||
if err := copyCredential(srcCredentialPath, destCredentialPath); err != nil {
|
||||
return err
|
||||
if srcCredentialPath != destCredentialPath {
|
||||
if err := copyCredential(srcCredentialPath, destCredentialPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
srcConfigPath := filepath.Join(userConfigDir, userConfigFile)
|
||||
destConfigPath := filepath.Join(serviceConfigDir, serviceConfigFile)
|
||||
if err := copyConfig(srcConfigPath, destConfigPath); err != nil {
|
||||
return err
|
||||
if srcConfigPath != destConfigPath {
|
||||
if err := copyConfig(srcConfigPath, destConfigPath); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("Copied %s to %s", srcConfigPath, destConfigPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func installLinuxService(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
etPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error determining executable path: %v", err)
|
||||
@@ -205,8 +220,8 @@ func installLinuxService(c *cli.Context) error {
|
||||
userConfigDir := filepath.Dir(c.String("config"))
|
||||
userConfigFile := filepath.Base(c.String("config"))
|
||||
userCredentialFile := config.DefaultCredentialFile
|
||||
if err = copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile); err != nil {
|
||||
logger.WithError(err).Infof("Failed to copy user configuration. Before running the service, ensure that %s contains two files, %s and %s",
|
||||
if err = copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile, logger); err != nil {
|
||||
logger.Errorf("Failed to copy user configuration: %s. Before running the service, ensure that %s contains two files, %s and %s", err,
|
||||
serviceConfigDir, serviceCredentialFile, serviceConfigFile)
|
||||
return err
|
||||
}
|
||||
@@ -214,41 +229,41 @@ func installLinuxService(c *cli.Context) error {
|
||||
switch {
|
||||
case isSystemd():
|
||||
logger.Infof("Using Systemd")
|
||||
return installSystemd(&templateArgs)
|
||||
return installSystemd(&templateArgs, logger)
|
||||
default:
|
||||
logger.Infof("Using Sysv")
|
||||
return installSysv(&templateArgs)
|
||||
return installSysv(&templateArgs, logger)
|
||||
}
|
||||
}
|
||||
|
||||
func installSystemd(templateArgs *ServiceTemplateArgs) error {
|
||||
func installSystemd(templateArgs *ServiceTemplateArgs, logger logger.Service) error {
|
||||
for _, serviceTemplate := range systemdTemplates {
|
||||
err := serviceTemplate.Generate(templateArgs)
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("error generating service template")
|
||||
logger.Errorf("error generating service template: %s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := runCommand("systemctl", "enable", "cloudflared.service"); err != nil {
|
||||
logger.WithError(err).Infof("systemctl enable cloudflared.service error")
|
||||
logger.Errorf("systemctl enable cloudflared.service error: %s", err)
|
||||
return err
|
||||
}
|
||||
if err := runCommand("systemctl", "start", "cloudflared-update.timer"); err != nil {
|
||||
logger.WithError(err).Infof("systemctl start cloudflared-update.timer error")
|
||||
logger.Errorf("systemctl start cloudflared-update.timer error: %s", err)
|
||||
return err
|
||||
}
|
||||
logger.Infof("systemctl daemon-reload")
|
||||
return runCommand("systemctl", "daemon-reload")
|
||||
}
|
||||
|
||||
func installSysv(templateArgs *ServiceTemplateArgs) error {
|
||||
func installSysv(templateArgs *ServiceTemplateArgs, logger logger.Service) error {
|
||||
confPath, err := sysvTemplate.ResolvePath()
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("error resolving system path")
|
||||
logger.Errorf("error resolving system path: %s", err)
|
||||
return err
|
||||
}
|
||||
if err := sysvTemplate.Generate(templateArgs); err != nil {
|
||||
logger.WithError(err).Infof("error generating system template")
|
||||
logger.Errorf("error generating system template: %s", err)
|
||||
return err
|
||||
}
|
||||
for _, i := range [...]string{"2", "3", "4", "5"} {
|
||||
@@ -265,28 +280,33 @@ func installSysv(templateArgs *ServiceTemplateArgs) error {
|
||||
}
|
||||
|
||||
func uninstallLinuxService(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
switch {
|
||||
case isSystemd():
|
||||
logger.Infof("Using Systemd")
|
||||
return uninstallSystemd()
|
||||
return uninstallSystemd(logger)
|
||||
default:
|
||||
logger.Infof("Using Sysv")
|
||||
return uninstallSysv()
|
||||
return uninstallSysv(logger)
|
||||
}
|
||||
}
|
||||
|
||||
func uninstallSystemd() error {
|
||||
func uninstallSystemd(logger logger.Service) error {
|
||||
if err := runCommand("systemctl", "disable", "cloudflared.service"); err != nil {
|
||||
logger.WithError(err).Infof("systemctl disable cloudflared.service error")
|
||||
logger.Errorf("systemctl disable cloudflared.service error: %s", err)
|
||||
return err
|
||||
}
|
||||
if err := runCommand("systemctl", "stop", "cloudflared-update.timer"); err != nil {
|
||||
logger.WithError(err).Infof("systemctl stop cloudflared-update.timer error")
|
||||
logger.Errorf("systemctl stop cloudflared-update.timer error: %s", err)
|
||||
return err
|
||||
}
|
||||
for _, serviceTemplate := range systemdTemplates {
|
||||
if err := serviceTemplate.Remove(); err != nil {
|
||||
logger.WithError(err).Infof("error removing service template")
|
||||
logger.Errorf("error removing service template: %s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -294,9 +314,9 @@ func uninstallSystemd() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func uninstallSysv() error {
|
||||
func uninstallSysv(logger logger.Service) error {
|
||||
if err := sysvTemplate.Remove(); err != nil {
|
||||
logger.WithError(err).Infof("error removing service template")
|
||||
logger.Errorf("error removing service template: %s", err)
|
||||
return err
|
||||
}
|
||||
for _, i := range [...]string{"2", "3", "4", "5"} {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -105,6 +106,11 @@ func stderrPath() (string, error) {
|
||||
}
|
||||
|
||||
func installLaunchd(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
if isRootUser() {
|
||||
logger.Infof("Installing Argo Tunnel client as a system launch daemon. " +
|
||||
"Argo Tunnel client will run at boot")
|
||||
@@ -116,35 +122,38 @@ func installLaunchd(c *cli.Context) error {
|
||||
}
|
||||
etPath, err := os.Executable()
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Error determining executable path")
|
||||
logger.Errorf("Error determining executable path: %s", err)
|
||||
return fmt.Errorf("Error determining executable path: %v", err)
|
||||
}
|
||||
installPath, err := installPath()
|
||||
if err != nil {
|
||||
logger.Errorf("Error determining install path: %s", err)
|
||||
return errors.Wrap(err, "Error determining install path")
|
||||
}
|
||||
stdoutPath, err := stdoutPath()
|
||||
if err != nil {
|
||||
logger.Errorf("error determining stdout path: %s", err)
|
||||
return errors.Wrap(err, "error determining stdout path")
|
||||
}
|
||||
stderrPath, err := stderrPath()
|
||||
if err != nil {
|
||||
logger.Errorf("error determining stderr path: %s", err)
|
||||
return errors.Wrap(err, "error determining stderr path")
|
||||
}
|
||||
launchdTemplate := newLaunchdTemplate(installPath, stdoutPath, stderrPath)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("error creating launchd template")
|
||||
logger.Errorf("error creating launchd template: %s", err)
|
||||
return errors.Wrap(err, "error creating launchd template")
|
||||
}
|
||||
templateArgs := ServiceTemplateArgs{Path: etPath}
|
||||
err = launchdTemplate.Generate(&templateArgs)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("error generating launchd template")
|
||||
logger.Errorf("error generating launchd template: %s", err)
|
||||
return err
|
||||
}
|
||||
plistPath, err := launchdTemplate.ResolvePath()
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("error resolving launchd template path")
|
||||
logger.Errorf("error resolving launchd template path: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -153,6 +162,11 @@ func installLaunchd(c *cli.Context) error {
|
||||
}
|
||||
|
||||
func uninstallLaunchd(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
if isRootUser() {
|
||||
logger.Infof("Uninstalling Argo Tunnel as a system launch daemon")
|
||||
} else {
|
||||
@@ -176,12 +190,12 @@ func uninstallLaunchd(c *cli.Context) error {
|
||||
}
|
||||
plistPath, err := launchdTemplate.ResolvePath()
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("error resolving launchd template path")
|
||||
logger.Errorf("error resolving launchd template path: %s", err)
|
||||
return err
|
||||
}
|
||||
err = runCommand("launchctl", "unload", plistPath)
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("error unloading")
|
||||
logger.Errorf("error unloading: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
+24
-10
@@ -6,10 +6,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
|
||||
"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/cmd/cloudflared/updater"
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
log "github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/metrics"
|
||||
"github.com/cloudflare/cloudflared/overwatch"
|
||||
"github.com/cloudflare/cloudflared/watcher"
|
||||
@@ -28,7 +29,6 @@ const (
|
||||
var (
|
||||
Version = "DEV"
|
||||
BuildTime = "unknown"
|
||||
logger = log.CreateLogger()
|
||||
// Mostly network errors that we don't want reported back to Sentry, this is done by substring match.
|
||||
ignoredErrors = []string{
|
||||
"connection reset by peer",
|
||||
@@ -62,7 +62,7 @@ func main() {
|
||||
app := &cli.App{}
|
||||
app.Name = "cloudflared"
|
||||
app.Usage = "Cloudflare's command-line tool and agent"
|
||||
app.ArgsUsage = "origin-url"
|
||||
app.UsageText = "cloudflared [global options] [command] [command options]"
|
||||
app.Copyright = fmt.Sprintf(
|
||||
`(c) %d Cloudflare Inc.
|
||||
Your installation of cloudflared software constitutes a symbol of your signature indicating that you accept
|
||||
@@ -129,7 +129,7 @@ func action(version string, shutdownC, graceShutdownC chan struct{}) cli.ActionF
|
||||
tags := make(map[string]string)
|
||||
tags["hostname"] = c.String("hostname")
|
||||
raven.SetTagsContext(tags)
|
||||
raven.CapturePanic(func() { err = tunnel.StartServer(c, version, shutdownC, graceShutdownC) }, nil)
|
||||
raven.CapturePanic(func() { err = tunnel.StartServer(c, version, shutdownC, graceShutdownC, nil) }, nil)
|
||||
exitCode := 0
|
||||
if err != nil {
|
||||
handleError(err)
|
||||
@@ -148,7 +148,6 @@ func userHomeDir() (string, error) {
|
||||
// use with sudo.
|
||||
homeDir, err := homedir.Dir()
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Cannot determine home directory for the user")
|
||||
return "", errors.Wrap(err, "Cannot determine home directory for the user")
|
||||
}
|
||||
return homeDir, nil
|
||||
@@ -167,25 +166,40 @@ func handleError(err error) {
|
||||
|
||||
// cloudflared was started without any flags
|
||||
func handleServiceMode(shutdownC chan struct{}) error {
|
||||
defer log.SharedWriteManager.Shutdown()
|
||||
logDirectory, logLevel := config.FindLogSettings()
|
||||
|
||||
logger, err := log.New(log.DefaultFile(logDirectory), log.LogLevelString(logLevel))
|
||||
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()
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Cannot load config file")
|
||||
logger.Errorf("Cannot load config file: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
configPath := config.FindDefaultConfigPath()
|
||||
configPath := config.FindOrCreateConfigPath()
|
||||
configManager, err := config.NewFileManager(f, configPath, logger)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Cannot setup config file for monitoring")
|
||||
logger.Errorf("Cannot setup config file for monitoring: %s", err)
|
||||
return err
|
||||
}
|
||||
logger.Infof("monitoring config file at: %s", configPath)
|
||||
|
||||
serviceManager := overwatch.NewAppManager(nil)
|
||||
serviceCallback := func(t string, name string, err error) {
|
||||
if err != nil {
|
||||
logger.Errorf("%s service: %s encountered an error: %s", t, name, err)
|
||||
}
|
||||
}
|
||||
serviceManager := overwatch.NewAppManager(serviceCallback)
|
||||
|
||||
appService := NewAppService(configManager, serviceManager, shutdownC, logger)
|
||||
if err := appService.Run(); err != nil {
|
||||
logger.WithError(err).Error("Failed to start app service")
|
||||
logger.Errorf("Failed to start app service: %s", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -73,21 +73,16 @@ func runCommand(command string, args ...string) error {
|
||||
cmd := exec.Command(command, args...)
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("error getting stderr pipe")
|
||||
return fmt.Errorf("error getting stderr pipe: %v", err)
|
||||
}
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("error starting %s", command)
|
||||
return fmt.Errorf("error starting %s: %v", command, err)
|
||||
}
|
||||
commandErr, _ := ioutil.ReadAll(stderr)
|
||||
if len(commandErr) > 0 {
|
||||
logger.Errorf("%s: %s", command, commandErr)
|
||||
}
|
||||
|
||||
ioutil.ReadAll(stderr)
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("%s returned error", command)
|
||||
return fmt.Errorf("%s returned with error: %v", command, err)
|
||||
}
|
||||
return nil
|
||||
@@ -148,8 +143,7 @@ func copyConfig(srcConfigPath, destConfigPath string) error {
|
||||
// Copy or create config
|
||||
destFile, exists, err := openFile(destConfigPath, true)
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("cannot open %s", destConfigPath)
|
||||
return err
|
||||
return fmt.Errorf("cannot open %s with error: %s", destConfigPath, err)
|
||||
} else if exists {
|
||||
// config already exists, do nothing
|
||||
return nil
|
||||
@@ -173,7 +167,6 @@ func copyConfig(srcConfigPath, destConfigPath string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to copy %s to %s: %v", srcConfigPath, destConfigPath, err)
|
||||
}
|
||||
logger.Infof("Copied %s to %s", srcConfigPath, destConfigPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/path"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/transfer"
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
)
|
||||
@@ -23,8 +23,6 @@ const (
|
||||
keyName = "token"
|
||||
)
|
||||
|
||||
var logger = log.CreateLogger()
|
||||
|
||||
type lock struct {
|
||||
lockFilePath string
|
||||
backoff *origin.BackoffHandler
|
||||
@@ -129,8 +127,20 @@ func isTokenLocked(lockFilePath string) bool {
|
||||
return exists && err == nil
|
||||
}
|
||||
|
||||
// FetchTokenWithRedirect will either load a stored token or generate a new one
|
||||
// it appends a redirect URL to the access cli request if opening the browser
|
||||
func FetchTokenWithRedirect(appURL *url.URL, logger logger.Service) (string, error) {
|
||||
return getToken(appURL, true, logger)
|
||||
}
|
||||
|
||||
// FetchToken will either load a stored token or generate a new one
|
||||
func FetchToken(appURL *url.URL) (string, error) {
|
||||
// it doesn't append a redirect URL to the access cli request if opening the browser
|
||||
func FetchToken(appURL *url.URL, logger logger.Service) (string, error) {
|
||||
return getToken(appURL, false, logger)
|
||||
}
|
||||
|
||||
// getToken will either load a stored token or generate a new one
|
||||
func getToken(appURL *url.URL, shouldRedirect bool, logger logger.Service) (string, error) {
|
||||
if token, err := GetTokenIfExists(appURL); token != "" && err == nil {
|
||||
return token, nil
|
||||
}
|
||||
@@ -156,7 +166,7 @@ func FetchToken(appURL *url.URL) (string, error) {
|
||||
// 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)
|
||||
token, err := transfer.Run(appURL, keyName, keyName, "", path, true, shouldRedirect, logger)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/encrypter"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -22,20 +22,18 @@ const (
|
||||
clientTimeout = time.Second * 60
|
||||
)
|
||||
|
||||
var logger = log.CreateLogger()
|
||||
|
||||
// Run does the transfer "dance" with the end result downloading the supported resource.
|
||||
// The expanded description is run is encapsulation of shared business logic needed
|
||||
// to request a resource (token/cert/etc) from the transfer service (loginhelper).
|
||||
// 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) ([]byte, error) {
|
||||
func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncrypt bool, shouldRedirect bool, logger logger.Service) ([]byte, error) {
|
||||
encrypterClient, err := encrypter.New("cloudflared_priv.pem", "cloudflared_pub.pem")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
requestURL, err := buildRequestURL(transferURL, key, value+encrypterClient.PublicKey(), shouldEncrypt)
|
||||
requestURL, err := buildRequestURL(transferURL, key, value+encrypterClient.PublicKey(), shouldEncrypt, shouldRedirect)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -51,7 +49,7 @@ func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncr
|
||||
var resourceData []byte
|
||||
|
||||
if shouldEncrypt {
|
||||
buf, key, err := transferRequest(baseStoreURL + "transfer/" + encrypterClient.PublicKey())
|
||||
buf, key, err := transferRequest(baseStoreURL+"transfer/"+encrypterClient.PublicKey(), logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -67,7 +65,7 @@ func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncr
|
||||
|
||||
resourceData = decrypted
|
||||
} else {
|
||||
buf, _, err := transferRequest(baseStoreURL + encrypterClient.PublicKey())
|
||||
buf, _, err := transferRequest(baseStoreURL+encrypterClient.PublicKey(), logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -84,7 +82,7 @@ func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncr
|
||||
// BuildRequestURL creates a request suitable for a resource transfer.
|
||||
// it will return a constructed url based off the base url and query key/value provided.
|
||||
// cli will build a url for cli transfer request.
|
||||
func buildRequestURL(baseURL *url.URL, key, value string, cli bool) (string, error) {
|
||||
func buildRequestURL(baseURL *url.URL, key, value string, cli, shouldRedirect bool) (string, error) {
|
||||
q := baseURL.Query()
|
||||
q.Set(key, value)
|
||||
baseURL.RawQuery = q.Encode()
|
||||
@@ -92,24 +90,26 @@ func buildRequestURL(baseURL *url.URL, key, value string, cli bool) (string, err
|
||||
return baseURL.String(), nil
|
||||
}
|
||||
|
||||
q.Set("redirect_url", baseURL.String()) // we add the token as a query param on both the redirect_url
|
||||
baseURL.RawQuery = q.Encode() // and this actual baseURL.
|
||||
if shouldRedirect {
|
||||
q.Set("redirect_url", baseURL.String()) // we add the token as a query param on both the redirect_url and the main url
|
||||
}
|
||||
baseURL.RawQuery = q.Encode() // and this actual baseURL.
|
||||
baseURL.Path = "cdn-cgi/access/cli"
|
||||
return baseURL.String(), nil
|
||||
}
|
||||
|
||||
// transferRequest downloads the requested resource from the request URL
|
||||
func transferRequest(requestURL string) ([]byte, string, error) {
|
||||
func transferRequest(requestURL string, logger logger.Service) ([]byte, string, error) {
|
||||
client := &http.Client{Timeout: clientTimeout}
|
||||
const pollAttempts = 10
|
||||
// we do "long polling" on the endpoint to get the resource.
|
||||
for i := 0; i < pollAttempts; i++ {
|
||||
buf, key, err := poll(client, requestURL)
|
||||
buf, key, err := poll(client, requestURL, logger)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
} else if len(buf) > 0 {
|
||||
if err := putSuccess(client, requestURL); err != nil {
|
||||
logger.WithError(err).Error("Failed to update resource success")
|
||||
logger.Errorf("Failed to update resource success: %s", err)
|
||||
}
|
||||
return buf, key, nil
|
||||
}
|
||||
@@ -118,7 +118,7 @@ func transferRequest(requestURL string) ([]byte, string, error) {
|
||||
}
|
||||
|
||||
// poll the endpoint for the request resource, waiting for the user interaction
|
||||
func poll(client *http.Client, requestURL string) ([]byte, string, error) {
|
||||
func poll(client *http.Client, requestURL string, logger logger.Service) ([]byte, string, error) {
|
||||
resp, err := client.Get(requestURL)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
|
||||
+168
-218
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
@@ -17,21 +18,21 @@ import (
|
||||
|
||||
"github.com/cloudflare/cloudflared/awsuploader"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/dbconnect"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/hello"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/metrics"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"github.com/cloudflare/cloudflared/signal"
|
||||
"github.com/cloudflare/cloudflared/socks"
|
||||
"github.com/cloudflare/cloudflared/sshlog"
|
||||
"github.com/cloudflare/cloudflared/sshserver"
|
||||
"github.com/cloudflare/cloudflared/supervisor"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
|
||||
"github.com/coreos/go-systemd/daemon"
|
||||
@@ -80,10 +81,19 @@ const (
|
||||
// hostKeyPath is the path of the dir to save SSH host keys too
|
||||
hostKeyPath = "host-key-path"
|
||||
|
||||
//sshServerFlag enables cloudflared ssh proxy server
|
||||
sshServerFlag = "ssh-server"
|
||||
|
||||
// socks5Flag is to enable the socks server to deframe
|
||||
socks5Flag = "socks5"
|
||||
|
||||
noIntentMsg = "The --intent argument is required. Cloudflared looks up an Intent to determine what configuration to use (i.e. which tunnels to start). If you don't have any Intents yet, you can use a placeholder Intent Label for now. Then, when you make an Intent with that label, cloudflared will get notified and open the tunnels you specified in that Intent."
|
||||
// bastionFlag is to enable bastion, or jump host, operation
|
||||
bastionFlag = "bastion"
|
||||
|
||||
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."
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -100,7 +110,7 @@ func Commands() []*cli.Command {
|
||||
cmds := []*cli.Command{
|
||||
{
|
||||
Name: "login",
|
||||
Action: login,
|
||||
Action: cliutil.ErrorHandler(login),
|
||||
Usage: "Generate a configuration file with your login details",
|
||||
ArgsUsage: " ",
|
||||
Flags: []cli.Flag{
|
||||
@@ -113,7 +123,7 @@ func Commands() []*cli.Command {
|
||||
},
|
||||
{
|
||||
Name: "proxy-dns",
|
||||
Action: tunneldns.Run,
|
||||
Action: cliutil.ErrorHandler(tunneldns.Run),
|
||||
Usage: "Run a DNS over HTTPS proxy server.",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
@@ -159,14 +169,18 @@ func Commands() []*cli.Command {
|
||||
c.Hidden = false
|
||||
subcommands = append(subcommands, &c)
|
||||
}
|
||||
subcommands = append(subcommands, buildCreateCommand())
|
||||
subcommands = append(subcommands, buildListCommand())
|
||||
subcommands = append(subcommands, buildDeleteCommand())
|
||||
subcommands = append(subcommands, buildRunCommand())
|
||||
|
||||
cmds = append(cmds, &cli.Command{
|
||||
Name: "tunnel",
|
||||
Action: tunnel,
|
||||
Action: cliutil.ErrorHandler(tunnel),
|
||||
Before: Before,
|
||||
Category: "Tunnel",
|
||||
Usage: "Make a locally-running web service accessible over the internet using Argo Tunnel.",
|
||||
ArgsUsage: "[origin-url]",
|
||||
ArgsUsage: " ",
|
||||
Description: `Argo Tunnel asks you to specify a hostname on a Cloudflare-powered
|
||||
domain you control and a local address. Traffic from that hostname is routed
|
||||
(optionally via a Cloudflare Load Balancer) to this machine and appears on the
|
||||
@@ -193,14 +207,43 @@ func Commands() []*cli.Command {
|
||||
}
|
||||
|
||||
func tunnel(c *cli.Context) error {
|
||||
return StartServer(c, version, shutdownC, graceShutdownC)
|
||||
return StartServer(c, version, shutdownC, graceShutdownC, nil)
|
||||
}
|
||||
|
||||
func Init(v string, s, g chan struct{}) {
|
||||
version, shutdownC, graceShutdownC = v, s, g
|
||||
}
|
||||
|
||||
func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan struct{}) error {
|
||||
func createLogger(c *cli.Context, isTransport bool) (logger.Service, error) {
|
||||
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))
|
||||
|
||||
return logger.New(loggerOpts...)
|
||||
}
|
||||
|
||||
func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan struct{}, namedTunnel *origin.NamedTunnelConfig) error {
|
||||
logger, err := createLogger(c, false)
|
||||
if err != nil {
|
||||
return cliutil.PrintLoggerSetupError("error setting up logger", err)
|
||||
}
|
||||
|
||||
_ = raven.SetDSN(sentryDSN)
|
||||
var wg sync.WaitGroup
|
||||
listeners := gracenet.Net{}
|
||||
@@ -209,64 +252,45 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||
dnsReadySignal := make(chan struct{})
|
||||
|
||||
if c.String("config") == "" {
|
||||
logger.Warnf("Cannot determine default configuration path. No file %v in %v", config.DefaultConfigFiles, config.DefaultConfigDirs)
|
||||
}
|
||||
|
||||
if err := configMainLogger(c); err != nil {
|
||||
return errors.Wrap(err, "Error configuring logger")
|
||||
}
|
||||
|
||||
transportLogger, err := configTransportLogger(c)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error configuring transport logger")
|
||||
logger.Infof("Cannot determine default configuration path. No file %v in %v", config.DefaultConfigFiles, config.DefaultConfigDirs)
|
||||
}
|
||||
|
||||
if c.IsSet("trace-output") {
|
||||
tmpTraceFile, err := ioutil.TempFile("", "trace")
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to create new temporary file to save trace output")
|
||||
logger.Errorf("Failed to create new temporary file to save trace output: %s", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := tmpTraceFile.Close(); err != nil {
|
||||
logger.WithError(err).Errorf("Failed to close trace output file %s", tmpTraceFile.Name())
|
||||
logger.Errorf("Failed to close trace output file %s with error: %s", tmpTraceFile.Name(), err)
|
||||
}
|
||||
if err := os.Rename(tmpTraceFile.Name(), c.String("trace-output")); err != nil {
|
||||
logger.WithError(err).Errorf("Failed to rename temporary trace output file %s to %s", tmpTraceFile.Name(), c.String("trace-output"))
|
||||
logger.Errorf("Failed to rename temporary trace output file %s to %s with error: %s", tmpTraceFile.Name(), c.String("trace-output"), err)
|
||||
} else {
|
||||
err := os.Remove(tmpTraceFile.Name())
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Failed to remove the temporary trace file %s", tmpTraceFile.Name())
|
||||
logger.Errorf("Failed to remove the temporary trace file %s with error: %s", tmpTraceFile.Name(), err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err := trace.Start(tmpTraceFile); err != nil {
|
||||
logger.WithError(err).Error("Failed to start trace")
|
||||
logger.Errorf("Failed to start trace: %s", err)
|
||||
return errors.Wrap(err, "Error starting tracing")
|
||||
}
|
||||
defer trace.Stop()
|
||||
}
|
||||
|
||||
if c.String("logfile") != "" {
|
||||
if err := initLogFile(c, logger, transportLogger); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := handleDeprecatedOptions(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buildInfo := buildinfo.GetBuildInfo(version)
|
||||
buildInfo.Log(logger)
|
||||
logClientOptions(c)
|
||||
logClientOptions(c, logger)
|
||||
|
||||
if c.IsSet("proxy-dns") {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errC <- runDNSProxyServer(c, dnsReadySignal, shutdownC)
|
||||
errC <- runDNSProxyServer(c, dnsReadySignal, shutdownC, logger)
|
||||
}()
|
||||
} else {
|
||||
close(dnsReadySignal)
|
||||
@@ -277,7 +301,7 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||
|
||||
metricsListener, err := listeners.Listen("tcp", c.String("metrics"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error opening metrics server listener")
|
||||
logger.Errorf("Error opening metrics server listener: %s", err)
|
||||
return errors.Wrap(err, "Error opening metrics server listener")
|
||||
}
|
||||
defer metricsListener.Close()
|
||||
@@ -289,12 +313,12 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||
|
||||
go notifySystemd(connectedSignal)
|
||||
if c.IsSet("pidfile") {
|
||||
go writePidFile(connectedSignal, c.String("pidfile"))
|
||||
go writePidFile(connectedSignal, c.String("pidfile"), logger)
|
||||
}
|
||||
|
||||
cloudflaredID, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Cannot generate cloudflared ID")
|
||||
logger.Errorf("Cannot generate cloudflared ID: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -304,17 +328,13 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||
cancel()
|
||||
}()
|
||||
|
||||
if c.IsSet("use-declarative-tunnels") {
|
||||
return startDeclarativeTunnel(ctx, c, cloudflaredID, buildInfo, &listeners)
|
||||
}
|
||||
|
||||
// update needs to be after DNS proxy is up to resolve equinox server address
|
||||
if updater.IsAutoupdateEnabled(c) {
|
||||
if updater.IsAutoupdateEnabled(c, logger) {
|
||||
logger.Infof("Autoupdate frequency is set to %v", c.Duration("autoupdate-freq"))
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
autoupdater := updater.NewAutoUpdater(c.Duration("autoupdate-freq"), &listeners)
|
||||
autoupdater := updater.NewAutoUpdater(c.Duration("autoupdate-freq"), &listeners, logger)
|
||||
errC <- autoupdater.Run(ctx)
|
||||
}()
|
||||
}
|
||||
@@ -323,14 +343,14 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||
if dnsProxyStandAlone(c) {
|
||||
connectedSignal.Notify()
|
||||
// no grace period, handle SIGINT/SIGTERM immediately
|
||||
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, 0)
|
||||
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, 0, logger)
|
||||
}
|
||||
|
||||
if c.IsSet("hello-world") {
|
||||
logger.Infof("hello-world set")
|
||||
helloListener, err := hello.CreateTLSListener("127.0.0.1:")
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Cannot start Hello World Server")
|
||||
logger.Errorf("Cannot start Hello World Server: %s", err)
|
||||
return errors.Wrap(err, "Cannot start Hello World Server")
|
||||
}
|
||||
defer helloListener.Close()
|
||||
@@ -339,15 +359,14 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||
defer wg.Done()
|
||||
hello.StartHelloWorldServer(logger, helloListener, shutdownC)
|
||||
}()
|
||||
c.Set("url", "https://"+helloListener.Addr().String())
|
||||
forceSetFlag(c, "url", "https://"+helloListener.Addr().String())
|
||||
}
|
||||
|
||||
if c.IsSet("ssh-server") {
|
||||
if c.IsSet(sshServerFlag) {
|
||||
if runtime.GOOS != "darwin" && runtime.GOOS != "linux" {
|
||||
msg := fmt.Sprintf("--ssh-server is not supported on %s", runtime.GOOS)
|
||||
logger.Error(msg)
|
||||
return errors.New(msg)
|
||||
|
||||
}
|
||||
|
||||
logger.Infof("ssh-server set")
|
||||
@@ -358,13 +377,13 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||
c.String(accessKeyIDFlag), c.String(secretIDFlag), c.String(sessionTokenIDFlag), c.String(s3URLFlag))
|
||||
if err != nil {
|
||||
msg := "Cannot create uploader for SSH Server"
|
||||
logger.WithError(err).Error(msg)
|
||||
logger.Errorf("%s: %s", msg, err)
|
||||
return errors.Wrap(err, msg)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(sshLogFileDirectory, 0700); err != nil {
|
||||
msg := fmt.Sprintf("Cannot create SSH log file directory %s", sshLogFileDirectory)
|
||||
logger.WithError(err).Errorf(msg)
|
||||
logger.Errorf("%s: %s", msg, err)
|
||||
return errors.Wrap(err, msg)
|
||||
}
|
||||
|
||||
@@ -378,25 +397,33 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||
server, err := sshserver.New(logManager, logger, version, localServerAddress, c.String("hostname"), c.Path(hostKeyPath), shutdownC, c.Duration(sshIdleTimeoutFlag), c.Duration(sshMaxTimeoutFlag))
|
||||
if err != nil {
|
||||
msg := "Cannot create new SSH Server"
|
||||
logger.WithError(err).Error(msg)
|
||||
logger.Errorf("%s: %s", msg, err)
|
||||
return errors.Wrap(err, msg)
|
||||
}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err = server.Start(); err != nil && err != ssh.ErrServerClosed {
|
||||
logger.WithError(err).Error("SSH server error")
|
||||
logger.Errorf("SSH server error: %s", err)
|
||||
// TODO: remove when declarative tunnels are implemented.
|
||||
close(shutdownC)
|
||||
}
|
||||
}()
|
||||
c.Set("url", "ssh://"+localServerAddress)
|
||||
forceSetFlag(c, "url", "ssh://"+localServerAddress)
|
||||
}
|
||||
|
||||
if host := hostnameFromURI(c.String("url")); host != "" {
|
||||
url := c.String("url")
|
||||
hostname := c.String("hostname")
|
||||
if url == hostname && url != "" && hostname != "" {
|
||||
errText := "hostname and url shouldn't match. See --help for more information"
|
||||
logger.Error(errText)
|
||||
return fmt.Errorf(errText)
|
||||
}
|
||||
|
||||
if staticHost := hostnameFromURI(c.String("url")); isProxyDestinationConfigured(staticHost, c) {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:")
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Cannot start Websocket Proxy Server")
|
||||
logger.Errorf("Cannot start Websocket Proxy Server: %s", err)
|
||||
return errors.Wrap(err, "Cannot start Websocket Proxy Server")
|
||||
}
|
||||
wg.Add(1)
|
||||
@@ -405,28 +432,44 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||
streamHandler := websocket.DefaultStreamHandler
|
||||
if c.IsSet(socks5Flag) {
|
||||
logger.Info("SOCKS5 server started")
|
||||
streamHandler = func(wsConn *websocket.Conn, remoteConn net.Conn) {
|
||||
streamHandler = func(wsConn *websocket.Conn, remoteConn net.Conn, _ http.Header) {
|
||||
dialer := socks.NewConnDialer(remoteConn)
|
||||
requestHandler := socks.NewRequestHandler(dialer)
|
||||
socksServer := socks.NewConnectionHandler(requestHandler)
|
||||
|
||||
socksServer.Serve(wsConn)
|
||||
}
|
||||
} else if c.IsSet(sshServerFlag) {
|
||||
streamHandler = func(wsConn *websocket.Conn, remoteConn net.Conn, requestHeaders http.Header) {
|
||||
if finalDestination := requestHeaders.Get(h2mux.CFJumpDestinationHeader); finalDestination != "" {
|
||||
token := requestHeaders.Get(h2mux.CFAccessTokenHeader)
|
||||
if err := websocket.SendSSHPreamble(remoteConn, finalDestination, token); err != nil {
|
||||
logger.Errorf("Failed to send SSH preamble: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
websocket.DefaultStreamHandler(wsConn, remoteConn, requestHeaders)
|
||||
}
|
||||
}
|
||||
errC <- websocket.StartProxyServer(logger, listener, host, shutdownC, streamHandler)
|
||||
errC <- websocket.StartProxyServer(logger, listener, staticHost, shutdownC, streamHandler)
|
||||
}()
|
||||
c.Set("url", "http://"+listener.Addr().String())
|
||||
forceSetFlag(c, "url", "http://"+listener.Addr().String())
|
||||
}
|
||||
|
||||
tunnelConfig, err := prepareTunnelConfig(c, buildInfo, version, logger, transportLogger)
|
||||
transportLogger, err := createLogger(c, true)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up transport logger")
|
||||
}
|
||||
|
||||
tunnelConfig, err := prepareTunnelConfig(c, buildInfo, version, logger, transportLogger, namedTunnel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reconnectCh := make(chan origin.ReconnectSignal, 1)
|
||||
if c.IsSet("stdin-control") {
|
||||
logger.Warn("Enabling control through stdin")
|
||||
go stdinControl(reconnectCh)
|
||||
logger.Info("Enabling control through stdin")
|
||||
go stdinControl(reconnectCh, logger)
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
@@ -434,21 +477,36 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||
defer wg.Done()
|
||||
errC <- origin.StartTunnelDaemon(ctx, tunnelConfig, connectedSignal, cloudflaredID, reconnectCh)
|
||||
}()
|
||||
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, c.Duration("grace-period"))
|
||||
|
||||
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, c.Duration("grace-period"), logger)
|
||||
}
|
||||
|
||||
// forceSetFlag attempts to set the given flag value in the closest context that has it defined
|
||||
func forceSetFlag(c *cli.Context, name, value string) {
|
||||
for _, ctx := range c.Lineage() {
|
||||
if err := ctx.Set(name, value); err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Before(c *cli.Context) error {
|
||||
logger, err := createLogger(c, false)
|
||||
if err != nil {
|
||||
return cliutil.PrintLoggerSetupError("error setting up logger", err)
|
||||
}
|
||||
|
||||
if c.String("config") == "" {
|
||||
logger.Debugf("Cannot determine default configuration path. No file %v in %v", config.DefaultConfigFiles, config.DefaultConfigDirs)
|
||||
}
|
||||
inputSource, err := config.FindInputSourceContext(c)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Cannot load configuration from %s", c.String("config"))
|
||||
logger.Errorf("Cannot load configuration from %s: %s", c.String("config"), err)
|
||||
return err
|
||||
} else if inputSource != nil {
|
||||
err := altsrc.ApplyInputSourceValues(c, inputSource, c.App.Flags)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Cannot apply configuration from %s", c.String("config"))
|
||||
logger.Errorf("Cannot apply configuration from %s: %s", c.String("config"), err)
|
||||
return err
|
||||
}
|
||||
logger.Debugf("Applied configuration from %s", c.String("config"))
|
||||
@@ -456,138 +514,27 @@ func Before(c *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func startDeclarativeTunnel(ctx context.Context,
|
||||
c *cli.Context,
|
||||
cloudflaredID uuid.UUID,
|
||||
buildInfo *buildinfo.BuildInfo,
|
||||
listeners *gracenet.Net,
|
||||
) error {
|
||||
reverseProxyOrigin, err := defaultOriginConfig(c)
|
||||
if err != nil {
|
||||
logger.WithError(err)
|
||||
return err
|
||||
}
|
||||
reverseProxyConfig, err := pogs.NewReverseProxyConfig(
|
||||
c.String("hostname"),
|
||||
reverseProxyOrigin,
|
||||
c.Uint64("retries"),
|
||||
c.Duration("proxy-connection-timeout"),
|
||||
c.Uint64("compression-quality"),
|
||||
)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Cannot initialize default client config because reverse proxy config is invalid")
|
||||
return err
|
||||
}
|
||||
defaultClientConfig := &pogs.ClientConfig{
|
||||
Version: pogs.InitVersion(),
|
||||
SupervisorConfig: &pogs.SupervisorConfig{
|
||||
AutoUpdateFrequency: c.Duration("autoupdate-freq"),
|
||||
MetricsUpdateFrequency: c.Duration("metrics-update-freq"),
|
||||
GracePeriod: c.Duration("grace-period"),
|
||||
},
|
||||
EdgeConnectionConfig: &pogs.EdgeConnectionConfig{
|
||||
NumHAConnections: uint8(c.Int("ha-connections")),
|
||||
HeartbeatInterval: c.Duration("heartbeat-interval"),
|
||||
Timeout: c.Duration("dial-edge-timeout"),
|
||||
MaxFailedHeartbeats: c.Uint64("heartbeat-count"),
|
||||
},
|
||||
DoHProxyConfigs: []*pogs.DoHProxyConfig{},
|
||||
ReverseProxyConfigs: []*pogs.ReverseProxyConfig{reverseProxyConfig},
|
||||
}
|
||||
|
||||
autoupdater := updater.NewAutoUpdater(defaultClientConfig.SupervisorConfig.AutoUpdateFrequency, listeners)
|
||||
|
||||
originCert, err := getOriginCert(c)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("error getting origin cert")
|
||||
return err
|
||||
}
|
||||
toEdgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("unable to create TLS config to connect with edge")
|
||||
return err
|
||||
}
|
||||
|
||||
tags, err := NewTagSliceFromCLI(c.StringSlice("tag"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("unable to parse tag")
|
||||
return err
|
||||
}
|
||||
|
||||
intentLabel := c.String("intent")
|
||||
if intentLabel == "" {
|
||||
logger.Error("--intent was empty")
|
||||
return fmt.Errorf(noIntentMsg)
|
||||
}
|
||||
|
||||
cloudflaredConfig := &connection.CloudflaredConfig{
|
||||
BuildInfo: buildInfo,
|
||||
CloudflaredID: cloudflaredID,
|
||||
IntentLabel: intentLabel,
|
||||
Tags: tags,
|
||||
}
|
||||
|
||||
serviceDiscoverer, err := serviceDiscoverer(c, logger)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("unable to create service discoverer")
|
||||
return err
|
||||
}
|
||||
supervisor, err := supervisor.NewSupervisor(defaultClientConfig, originCert, toEdgeTLSConfig,
|
||||
serviceDiscoverer, cloudflaredConfig, autoupdater, updater.SupportAutoUpdate(), logger)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("unable to create Supervisor")
|
||||
return err
|
||||
}
|
||||
return supervisor.Run(ctx)
|
||||
}
|
||||
|
||||
func defaultOriginConfig(c *cli.Context) (pogs.OriginConfig, error) {
|
||||
if c.IsSet("hello-world") {
|
||||
return &pogs.HelloWorldOriginConfig{}, nil
|
||||
}
|
||||
originConfig := &pogs.HTTPOriginConfig{
|
||||
TCPKeepAlive: c.Duration("proxy-tcp-keepalive"),
|
||||
DialDualStack: !c.Bool("proxy-no-happy-eyeballs"),
|
||||
TLSHandshakeTimeout: c.Duration("proxy-tls-timeout"),
|
||||
TLSVerify: !c.Bool("no-tls-verify"),
|
||||
OriginCAPool: c.String("origin-ca-pool"),
|
||||
OriginServerName: c.String("origin-server-name"),
|
||||
MaxIdleConnections: c.Uint64("proxy-keepalive-connections"),
|
||||
IdleConnectionTimeout: c.Duration("proxy-keepalive-timeout"),
|
||||
ProxyConnectionTimeout: c.Duration("proxy-connection-timeout"),
|
||||
ExpectContinueTimeout: c.Duration("proxy-expect-continue-timeout"),
|
||||
ChunkedEncoding: c.Bool("no-chunked-encoding"),
|
||||
}
|
||||
if c.IsSet("unix-socket") {
|
||||
unixSocket, err := config.ValidateUnixSocket(c)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error validating --unix-socket")
|
||||
}
|
||||
originConfig.URLString = unixSocket
|
||||
}
|
||||
originAddr, err := config.ValidateUrl(c)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error validating origin URL")
|
||||
}
|
||||
originConfig.URLString = originAddr
|
||||
return originConfig, nil
|
||||
// isProxyDestinationConfigured returns true if there is a static host set or if bastion mode is set.
|
||||
func isProxyDestinationConfigured(staticHost string, c *cli.Context) bool {
|
||||
return staticHost != "" || c.IsSet(bastionFlag)
|
||||
}
|
||||
|
||||
func waitToShutdown(wg *sync.WaitGroup,
|
||||
errC chan error,
|
||||
shutdownC, graceShutdownC chan struct{},
|
||||
gracePeriod time.Duration,
|
||||
logger logger.Service,
|
||||
) error {
|
||||
var err error
|
||||
if gracePeriod > 0 {
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceShutdownC, gracePeriod)
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceShutdownC, gracePeriod, logger)
|
||||
} else {
|
||||
err = waitForSignal(errC, shutdownC)
|
||||
err = waitForSignal(errC, shutdownC, logger)
|
||||
close(graceShutdownC)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Quitting due to error")
|
||||
logger.Errorf("Quitting due to error: %s", err)
|
||||
} else {
|
||||
logger.Info("Quitting...")
|
||||
}
|
||||
@@ -605,16 +552,16 @@ func notifySystemd(waitForSignal *signal.Signal) {
|
||||
daemon.SdNotify(false, "READY=1")
|
||||
}
|
||||
|
||||
func writePidFile(waitForSignal *signal.Signal, pidFile string) {
|
||||
func writePidFile(waitForSignal *signal.Signal, pidFile string, logger logger.Service) {
|
||||
<-waitForSignal.Wait()
|
||||
expandedPath, err := homedir.Expand(pidFile)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Unable to expand %s, try to use absolute path in --pidfile", pidFile)
|
||||
logger.Errorf("Unable to expand %s, try to use absolute path in --pidfile: %s", pidFile, err)
|
||||
return
|
||||
}
|
||||
file, err := os.Create(expandedPath)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Unable to write pid to %s", expandedPath)
|
||||
logger.Errorf("Unable to write pid to %s: %s", expandedPath, err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
@@ -631,6 +578,8 @@ func hostnameFromURI(uri string) string {
|
||||
return addPortIfMissing(u, 22)
|
||||
case "rdp":
|
||||
return addPortIfMissing(u, 3389)
|
||||
case "smb":
|
||||
return addPortIfMissing(u, 445)
|
||||
case "tcp":
|
||||
return addPortIfMissing(u, 7864) // just a random port since there isn't a default in this case
|
||||
}
|
||||
@@ -660,13 +609,13 @@ func dbConnectCmd() *cli.Command {
|
||||
}
|
||||
|
||||
// Override action to setup the Proxy, then if successful, start the tunnel daemon.
|
||||
cmd.Action = func(c *cli.Context) error {
|
||||
cmd.Action = cliutil.ErrorHandler(func(c *cli.Context) error {
|
||||
err := dbconnect.CmdAction(c)
|
||||
if err == nil {
|
||||
err = tunnel(c)
|
||||
}
|
||||
return err
|
||||
}
|
||||
})
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -817,6 +766,13 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
EnvVars: []string{"TUNNEL_API_CA_KEY"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "api-url",
|
||||
Usage: "Base URL for Cloudflare API v4",
|
||||
EnvVars: []string{"TUNNEL_API_URL"},
|
||||
Value: "https://api.cloudflare.com/client/v4",
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "metrics",
|
||||
Value: "localhost:",
|
||||
@@ -852,15 +808,15 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "loglevel",
|
||||
Value: "info",
|
||||
Usage: "Application logging level {panic, fatal, error, warn, info, debug}. " + debugLevelWarning,
|
||||
Usage: "Application logging level {fatal, error, info, debug}. " + debugLevelWarning,
|
||||
EnvVars: []string{"TUNNEL_LOGLEVEL"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "transport-loglevel",
|
||||
Aliases: []string{"proto-loglevel"}, // This flag used to be called proto-loglevel
|
||||
Value: "warn",
|
||||
Usage: "Transport logging level(previously called protocol logging level) {panic, fatal, error, warn, info, debug}",
|
||||
Value: "fatal",
|
||||
Usage: "Transport logging level(previously called protocol logging level) {fatal, error, info, debug}",
|
||||
EnvVars: []string{"TUNNEL_PROTO_LOGLEVEL", "TUNNEL_TRANSPORT_LOGLEVEL"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
@@ -879,7 +835,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "ssh-server",
|
||||
Name: sshServerFlag,
|
||||
Value: false,
|
||||
Usage: "Run an SSH Server",
|
||||
EnvVars: []string{"TUNNEL_SSH_SERVER"},
|
||||
@@ -897,6 +853,12 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
EnvVars: []string{"TUNNEL_LOGFILE"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: logDirectoryFlag,
|
||||
Usage: "Save application log to this directory for reporting issues.",
|
||||
EnvVars: []string{"TUNNEL_LOGDIRECTORY"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{
|
||||
Name: "ha-connections",
|
||||
Value: 4,
|
||||
@@ -1009,18 +971,6 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
EnvVars: []string{"TUNNEL_TRACE_OUTPUT"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "use-declarative-tunnels",
|
||||
Usage: "Test establishing connections with declarative tunnel methods.",
|
||||
EnvVars: []string{"TUNNEL_USE_DECLARATIVE"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "intent",
|
||||
Usage: "The label of an Intent from which `cloudflared` should gets its tunnels from. Intents can be created in the Origin Registry UI.",
|
||||
EnvVars: []string{"TUNNEL_INTENT"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "use-reconnect-token",
|
||||
Usage: "Test reestablishing connections with the new 'reconnect token' flow.",
|
||||
@@ -1028,13 +978,6 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
EnvVars: []string{"TUNNEL_USE_RECONNECT_TOKEN"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "use-quick-reconnects",
|
||||
Usage: "Test reestablishing connections with the new 'connection digest' flow.",
|
||||
Value: true,
|
||||
EnvVars: []string{"TUNNEL_USE_QUICK_RECONNECTS"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "dial-edge-timeout",
|
||||
Usage: "Maximum wait time to set up a connection with the edge",
|
||||
@@ -1115,12 +1058,19 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
Usage: "specify if this tunnel is running as a SOCK5 Server",
|
||||
EnvVars: []string{"TUNNEL_SOCKS"},
|
||||
Value: false,
|
||||
Hidden: false,
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: bastionFlag,
|
||||
Value: false,
|
||||
Usage: "Runs as jump host",
|
||||
EnvVars: []string{"TUNNEL_BASTION"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func stdinControl(reconnectCh chan origin.ReconnectSignal) {
|
||||
func stdinControl(reconnectCh chan origin.ReconnectSignal, logger logger.Service) {
|
||||
for {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
@@ -1142,11 +1092,11 @@ func stdinControl(reconnectCh chan origin.ReconnectSignal) {
|
||||
logger.Infof("Sending reconnect signal %+v", reconnect)
|
||||
reconnectCh <- reconnect
|
||||
default:
|
||||
logger.Warn("Unknown command: ", command)
|
||||
logger.Infof("Unknown command: %s", command)
|
||||
fallthrough
|
||||
case "help":
|
||||
logger.Info(`Supported command:
|
||||
reconnect [delay]
|
||||
logger.Info(`Supported command:
|
||||
reconnect [delay]
|
||||
- restarts one randomly chosen connection with optional delay before reconnect`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
@@ -47,25 +46,16 @@ func findDefaultOriginCertPath() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func generateRandomClientID(logger *logrus.Logger) (string, error) {
|
||||
func generateRandomClientID(logger logger.Service) (string, error) {
|
||||
u, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("couldn't create UUID for client ID")
|
||||
logger.Errorf("couldn't create UUID for client ID %s", err)
|
||||
return "", err
|
||||
}
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func handleDeprecatedOptions(c *cli.Context) error {
|
||||
// Fail if the user provided an old authentication method
|
||||
if c.IsSet("api-key") || c.IsSet("api-email") || c.IsSet("api-ca-key") {
|
||||
logger.Error("You don't need to give us your api-key anymore. Please use the new login method. Just run cloudflared login")
|
||||
return fmt.Errorf("Client provided deprecated options")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func logClientOptions(c *cli.Context) {
|
||||
func logClientOptions(c *cli.Context, logger logger.Service) {
|
||||
flags := make(map[string]interface{})
|
||||
for _, flag := range c.LocalFlagNames() {
|
||||
flags[flag] = c.Generic(flag)
|
||||
@@ -79,7 +69,7 @@ func logClientOptions(c *cli.Context) {
|
||||
}
|
||||
|
||||
if len(flags) > 0 {
|
||||
logger.WithFields(flags).Info("Flags")
|
||||
logger.Infof("Environment variables %v", flags)
|
||||
}
|
||||
|
||||
envs := make(map[string]string)
|
||||
@@ -102,27 +92,29 @@ func dnsProxyStandAlone(c *cli.Context) bool {
|
||||
return c.IsSet("proxy-dns") && (!c.IsSet("hostname") && !c.IsSet("tag") && !c.IsSet("hello-world"))
|
||||
}
|
||||
|
||||
func getOriginCert(c *cli.Context) ([]byte, error) {
|
||||
if c.String("origincert") == "" {
|
||||
logger.Warnf("Cannot determine default origin certificate path. No file %s in %v", config.DefaultCredentialFile, config.DefaultConfigDirs)
|
||||
func findOriginCert(c *cli.Context, logger logger.Service) (string, error) {
|
||||
originCertPath := c.String("origincert")
|
||||
if originCertPath == "" {
|
||||
logger.Infof("Cannot determine default origin certificate path. No file %s in %v", config.DefaultCredentialFile, config.DefaultConfigDirs)
|
||||
if isRunningFromTerminal() {
|
||||
logger.Errorf("You need to specify the origin certificate path with --origincert option, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", argumentsUrl)
|
||||
return nil, fmt.Errorf("Client didn't specify origincert path when running from terminal")
|
||||
return "", fmt.Errorf("Client didn't specify origincert path when running from terminal")
|
||||
} else {
|
||||
logger.Errorf("You need to specify the origin certificate path by specifying the origincert option in the configuration file, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", serviceUrl)
|
||||
return nil, fmt.Errorf("Client didn't specify origincert path")
|
||||
return "", fmt.Errorf("Client didn't specify origincert path")
|
||||
}
|
||||
}
|
||||
// Check that the user has acquired a certificate using the login command
|
||||
originCertPath, err := homedir.Expand(c.String("origincert"))
|
||||
var err error
|
||||
originCertPath, err = homedir.Expand(originCertPath)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Cannot resolve path %s", c.String("origincert"))
|
||||
return nil, fmt.Errorf("Cannot resolve path %s", c.String("origincert"))
|
||||
logger.Errorf("Cannot resolve path %s: %s", originCertPath, err)
|
||||
return "", fmt.Errorf("Cannot resolve path %s", originCertPath)
|
||||
}
|
||||
// Check that the user has acquired a certificate using the login command
|
||||
ok, err := config.FileExists(originCertPath)
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot check if origin cert exists at path %s", c.String("origincert"))
|
||||
return nil, fmt.Errorf("Cannot check if origin cert exists at path %s", c.String("origincert"))
|
||||
logger.Errorf("Cannot check if origin cert exists at path %s", originCertPath)
|
||||
return "", fmt.Errorf("Cannot check if origin cert exists at path %s", originCertPath)
|
||||
}
|
||||
if !ok {
|
||||
logger.Errorf(`Cannot find a valid certificate for your origin at the path:
|
||||
@@ -134,26 +126,45 @@ If you don't have a certificate signed by Cloudflare, run the command:
|
||||
|
||||
%s login
|
||||
`, originCertPath, os.Args[0])
|
||||
return nil, fmt.Errorf("Cannot find a valid certificate at the path %s", originCertPath)
|
||||
return "", fmt.Errorf("Cannot find a valid certificate at the path %s", originCertPath)
|
||||
}
|
||||
|
||||
return originCertPath, nil
|
||||
}
|
||||
|
||||
func readOriginCert(originCertPath string, logger logger.Service) ([]byte, error) {
|
||||
logger.Debugf("Reading origin cert from %s", originCertPath)
|
||||
|
||||
// Easier to send the certificate as []byte via RPC than decoding it at this point
|
||||
originCert, err := ioutil.ReadFile(originCertPath)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Cannot read %s to load origin certificate", originCertPath)
|
||||
logger.Errorf("Cannot read %s to load origin certificate: %s", originCertPath, err)
|
||||
return nil, fmt.Errorf("Cannot read %s to load origin certificate", originCertPath)
|
||||
}
|
||||
return originCert, nil
|
||||
}
|
||||
|
||||
func getOriginCert(c *cli.Context, logger logger.Service) ([]byte, error) {
|
||||
if originCertPath, err := findOriginCert(c, logger); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return readOriginCert(originCertPath, logger)
|
||||
}
|
||||
}
|
||||
|
||||
func prepareTunnelConfig(
|
||||
c *cli.Context,
|
||||
buildInfo *buildinfo.BuildInfo,
|
||||
version string, logger,
|
||||
transportLogger *logrus.Logger,
|
||||
version string,
|
||||
logger logger.Service,
|
||||
transportLogger logger.Service,
|
||||
namedTunnel *origin.NamedTunnelConfig,
|
||||
) (*origin.TunnelConfig, error) {
|
||||
compatibilityMode := namedTunnel == nil
|
||||
|
||||
hostname, err := validation.ValidateHostname(c.String("hostname"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Invalid hostname")
|
||||
logger.Errorf("Invalid hostname: %s", err)
|
||||
return nil, errors.Wrap(err, "Invalid hostname")
|
||||
}
|
||||
isFreeTunnel := hostname == ""
|
||||
@@ -167,21 +178,21 @@ func prepareTunnelConfig(
|
||||
|
||||
tags, err := NewTagSliceFromCLI(c.StringSlice("tag"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Tag parse failure")
|
||||
logger.Errorf("Tag parse failure: %s", err)
|
||||
return nil, errors.Wrap(err, "Tag parse failure")
|
||||
}
|
||||
|
||||
tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID})
|
||||
|
||||
originURL, err := config.ValidateUrl(c)
|
||||
originURL, err := config.ValidateUrl(c, compatibilityMode)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error validating origin URL")
|
||||
logger.Errorf("Error validating origin URL: %s", err)
|
||||
return nil, errors.Wrap(err, "Error validating origin URL")
|
||||
}
|
||||
|
||||
var originCert []byte
|
||||
if !isFreeTunnel {
|
||||
originCert, err = getOriginCert(c)
|
||||
originCert, err = getOriginCert(c, logger)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error getting origin cert")
|
||||
}
|
||||
@@ -189,7 +200,7 @@ func prepareTunnelConfig(
|
||||
|
||||
originCertPool, err := tlsconfig.LoadOriginCA(c, logger)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error loading cert pool")
|
||||
logger.Errorf("Error loading cert pool: %s", err)
|
||||
return nil, errors.Wrap(err, "Error loading cert pool")
|
||||
}
|
||||
|
||||
@@ -216,7 +227,7 @@ func prepareTunnelConfig(
|
||||
if c.IsSet("unix-socket") {
|
||||
unixSocket, err := config.ValidateUnixSocket(c)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error validating --unix-socket")
|
||||
logger.Errorf("Error validating --unix-socket: %s", err)
|
||||
return nil, errors.Wrap(err, "Error validating --unix-socket")
|
||||
}
|
||||
|
||||
@@ -233,60 +244,66 @@ func prepareTunnelConfig(
|
||||
if !c.IsSet("hello-world") && c.IsSet("origin-server-name") {
|
||||
httpTransport.TLSClientConfig.ServerName = c.String("origin-server-name")
|
||||
}
|
||||
|
||||
err = validation.ValidateHTTPService(originURL, hostname, httpTransport)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("unable to connect to the origin")
|
||||
// If tunnel running in bastion mode, a connection to origin will not exist until initiated by the client.
|
||||
if !c.IsSet(bastionFlag) {
|
||||
if err = validation.ValidateHTTPService(originURL, hostname, httpTransport); err != nil {
|
||||
logger.Errorf("unable to connect to the origin: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
toEdgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("unable to create TLS config to connect with edge")
|
||||
logger.Errorf("unable to create TLS config to connect with edge: %s", err)
|
||||
return nil, errors.Wrap(err, "unable to create TLS config to connect with edge")
|
||||
}
|
||||
|
||||
return &origin.TunnelConfig{
|
||||
BuildInfo: buildInfo,
|
||||
ClientID: clientID,
|
||||
ClientTlsConfig: httpTransport.TLSClientConfig,
|
||||
CompressionQuality: c.Uint64("compression-quality"),
|
||||
EdgeAddrs: c.StringSlice("edge"),
|
||||
GracePeriod: c.Duration("grace-period"),
|
||||
HAConnections: c.Int("ha-connections"),
|
||||
HTTPTransport: httpTransport,
|
||||
HeartbeatInterval: c.Duration("heartbeat-interval"),
|
||||
Hostname: hostname,
|
||||
HTTPHostHeader: c.String("http-host-header"),
|
||||
IncidentLookup: origin.NewIncidentLookup(),
|
||||
IsAutoupdated: c.Bool("is-autoupdated"),
|
||||
IsFreeTunnel: isFreeTunnel,
|
||||
LBPool: c.String("lb-pool"),
|
||||
Logger: logger,
|
||||
MaxHeartbeats: c.Uint64("heartbeat-count"),
|
||||
Metrics: tunnelMetrics,
|
||||
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
|
||||
NoChunkedEncoding: c.Bool("no-chunked-encoding"),
|
||||
OriginCert: originCert,
|
||||
OriginUrl: originURL,
|
||||
ReportedVersion: version,
|
||||
Retries: c.Uint("retries"),
|
||||
RunFromTerminal: isRunningFromTerminal(),
|
||||
Tags: tags,
|
||||
TlsConfig: toEdgeTLSConfig,
|
||||
TransportLogger: transportLogger,
|
||||
UseDeclarativeTunnel: c.Bool("use-declarative-tunnels"),
|
||||
UseReconnectToken: c.Bool("use-reconnect-token"),
|
||||
UseQuickReconnects: c.Bool("use-quick-reconnects"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func serviceDiscoverer(c *cli.Context, logger *logrus.Logger) (*edgediscovery.Edge, error) {
|
||||
// If --edge is specfied, resolve edge server addresses
|
||||
if len(c.StringSlice("edge")) > 0 {
|
||||
return edgediscovery.StaticEdge(logger, c.StringSlice("edge"))
|
||||
if namedTunnel != nil {
|
||||
clientUUID, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can't generate clientUUID")
|
||||
}
|
||||
namedTunnel.Client = tunnelpogs.ClientInfo{
|
||||
ClientID: clientUUID[:],
|
||||
Features: []string{origin.FeatureSerializedHeaders},
|
||||
Version: version,
|
||||
Arch: fmt.Sprintf("%s_%s", buildInfo.GoOS, buildInfo.GoArch),
|
||||
}
|
||||
}
|
||||
// Otherwise lookup edge server addresses through service discovery
|
||||
return edgediscovery.ResolveEdge(logger)
|
||||
|
||||
return &origin.TunnelConfig{
|
||||
BuildInfo: buildInfo,
|
||||
ClientID: clientID,
|
||||
ClientTlsConfig: httpTransport.TLSClientConfig,
|
||||
CompressionQuality: c.Uint64("compression-quality"),
|
||||
EdgeAddrs: c.StringSlice("edge"),
|
||||
GracePeriod: c.Duration("grace-period"),
|
||||
HAConnections: c.Int("ha-connections"),
|
||||
HTTPTransport: httpTransport,
|
||||
HeartbeatInterval: c.Duration("heartbeat-interval"),
|
||||
Hostname: hostname,
|
||||
HTTPHostHeader: c.String("http-host-header"),
|
||||
IncidentLookup: origin.NewIncidentLookup(),
|
||||
IsAutoupdated: c.Bool("is-autoupdated"),
|
||||
IsFreeTunnel: isFreeTunnel,
|
||||
LBPool: c.String("lb-pool"),
|
||||
Logger: logger,
|
||||
TransportLogger: transportLogger,
|
||||
MaxHeartbeats: c.Uint64("heartbeat-count"),
|
||||
Metrics: tunnelMetrics,
|
||||
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
|
||||
NoChunkedEncoding: c.Bool("no-chunked-encoding"),
|
||||
OriginCert: originCert,
|
||||
OriginUrl: originURL,
|
||||
ReportedVersion: version,
|
||||
Retries: c.Uint("retries"),
|
||||
RunFromTerminal: isRunningFromTerminal(),
|
||||
Tags: tags,
|
||||
TlsConfig: toEdgeTLSConfig,
|
||||
NamedTunnel: namedTunnel,
|
||||
ReplaceExisting: c.Bool("force"),
|
||||
// turn off use of reconnect token and auth refresh when using named tunnels
|
||||
UseReconnectToken: compatibilityMode && c.Bool("use-reconnect-token"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func isRunningFromTerminal() bool {
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
"github.com/rifflock/lfshook"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const 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."
|
||||
|
||||
var logger = log.CreateLogger()
|
||||
|
||||
func configMainLogger(c *cli.Context) error {
|
||||
logLevel, err := logrus.ParseLevel(c.String("loglevel"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Unknown logging level specified")
|
||||
return errors.Wrap(err, "Unknown logging level specified")
|
||||
}
|
||||
logger.SetLevel(logLevel)
|
||||
if logLevel == logrus.DebugLevel {
|
||||
logger.Warn(debugLevelWarning)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func configTransportLogger(c *cli.Context) (*logrus.Logger, error) {
|
||||
transportLogLevel, err := logrus.ParseLevel(c.String("transport-loglevel"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Fatal("Unknown transport logging level specified")
|
||||
return nil, errors.Wrap(err, "Unknown transport logging level specified")
|
||||
}
|
||||
transportLogger := logrus.New()
|
||||
transportLogger.Level = transportLogLevel
|
||||
return transportLogger, nil
|
||||
}
|
||||
|
||||
func initLogFile(c *cli.Context, loggers ...*logrus.Logger) error {
|
||||
filePath, err := homedir.Expand(c.String("logfile"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Cannot resolve logfile path")
|
||||
}
|
||||
|
||||
fileMode := os.O_WRONLY | os.O_APPEND | os.O_CREATE | os.O_TRUNC
|
||||
// do not truncate log file if the client has been autoupdated
|
||||
if c.Bool("is-autoupdated") {
|
||||
fileMode = os.O_WRONLY | os.O_APPEND | os.O_CREATE
|
||||
}
|
||||
f, err := os.OpenFile(filePath, fileMode, 0664)
|
||||
if err != nil {
|
||||
errors.Wrap(err, fmt.Sprintf("Cannot open file %s", filePath))
|
||||
}
|
||||
defer f.Close()
|
||||
pathMap := lfshook.PathMap{
|
||||
logrus.DebugLevel: filePath,
|
||||
logrus.WarnLevel: filePath,
|
||||
logrus.InfoLevel: filePath,
|
||||
logrus.ErrorLevel: filePath,
|
||||
logrus.FatalLevel: filePath,
|
||||
logrus.PanicLevel: filePath,
|
||||
}
|
||||
|
||||
for _, l := range loggers {
|
||||
l.Hooks.Add(lfshook.NewHook(pathMap, &logrus.JSONFormatter{}))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -9,7 +9,9 @@ import (
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/transfer"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
@@ -19,6 +21,11 @@ const (
|
||||
)
|
||||
|
||||
func login(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
path, ok, err := checkForExistingCert()
|
||||
if ok {
|
||||
fmt.Fprintf(os.Stdout, "You have an existing certificate at %s which login would overwrite.\nIf this is intentional, please move or delete that file then run this command again.\n", path)
|
||||
@@ -33,7 +40,7 @@ func login(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = transfer.Run(loginURL, "cert", "callback", callbackStoreURL, path, false)
|
||||
_, err = transfer.Run(loginURL, "cert", "callback", callbackStoreURL, path, false, true, 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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
@@ -8,23 +9,20 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func runDNSProxyServer(c *cli.Context, dnsReadySignal, shutdownC chan struct{}) error {
|
||||
func runDNSProxyServer(c *cli.Context, dnsReadySignal, shutdownC chan struct{}, logger logger.Service) error {
|
||||
port := c.Int("proxy-dns-port")
|
||||
if port <= 0 || port > 65535 {
|
||||
logger.Errorf("The 'proxy-dns-port' must be a valid port number in <1, 65535> range.")
|
||||
return errors.New("The 'proxy-dns-port' must be a valid port number in <1, 65535> range.")
|
||||
}
|
||||
listener, err := tunneldns.CreateListener(c.String("proxy-dns-address"), uint16(port), c.StringSlice("proxy-dns-upstream"), c.StringSlice("proxy-dns-bootstrap"))
|
||||
listener, err := tunneldns.CreateListener(c.String("proxy-dns-address"), uint16(port), c.StringSlice("proxy-dns-upstream"), c.StringSlice("proxy-dns-bootstrap"), logger)
|
||||
if err != nil {
|
||||
close(dnsReadySignal)
|
||||
listener.Stop()
|
||||
logger.WithError(err).Error("Cannot create the DNS over HTTPS proxy server")
|
||||
return errors.Wrap(err, "Cannot create the DNS over HTTPS proxy server")
|
||||
}
|
||||
|
||||
err = listener.Start(dnsReadySignal)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Cannot start the DNS over HTTPS proxy server")
|
||||
return errors.Wrap(err, "Cannot start the DNS over HTTPS proxy server")
|
||||
}
|
||||
<-shutdownC
|
||||
|
||||
@@ -5,12 +5,14 @@ import (
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
// waitForSignal notifies all routines to shutdownC immediately by closing the
|
||||
// shutdownC when one of the routines in main exits, or when this process receives
|
||||
// SIGTERM/SIGINT
|
||||
func waitForSignal(errC chan error, shutdownC chan struct{}) error {
|
||||
func waitForSignal(errC chan error, shutdownC chan struct{}, logger logger.Service) error {
|
||||
signals := make(chan os.Signal, 10)
|
||||
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
|
||||
defer signal.Stop(signals)
|
||||
@@ -39,6 +41,7 @@ func waitForSignal(errC chan error, shutdownC chan struct{}) error {
|
||||
func waitForSignalWithGraceShutdown(errC chan error,
|
||||
shutdownC, graceShutdownC chan struct{},
|
||||
gracePeriod time.Duration,
|
||||
logger logger.Service,
|
||||
) error {
|
||||
signals := make(chan os.Signal, 10)
|
||||
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
|
||||
@@ -53,9 +56,9 @@ func waitForSignalWithGraceShutdown(errC chan error,
|
||||
case s := <-signals:
|
||||
logger.Infof("Initiating graceful shutdown due to signal %s ...", s)
|
||||
close(graceShutdownC)
|
||||
waitForGracePeriod(signals, errC, shutdownC, gracePeriod)
|
||||
waitForGracePeriod(signals, errC, shutdownC, gracePeriod, logger)
|
||||
case <-graceShutdownC:
|
||||
waitForGracePeriod(signals, errC, shutdownC, gracePeriod)
|
||||
waitForGracePeriod(signals, errC, shutdownC, gracePeriod, logger)
|
||||
case <-shutdownC:
|
||||
close(graceShutdownC)
|
||||
}
|
||||
@@ -67,6 +70,7 @@ func waitForGracePeriod(signals chan os.Signal,
|
||||
errC chan error,
|
||||
shutdownC chan struct{},
|
||||
gracePeriod time.Duration,
|
||||
logger logger.Service,
|
||||
) {
|
||||
// Unregister signal handler early, so the client can send a second SIGTERM/SIGINT
|
||||
// to force shutdown cloudflared
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -27,6 +28,8 @@ func testChannelClosed(t *testing.T, c chan struct{}) {
|
||||
}
|
||||
|
||||
func TestWaitForSignal(t *testing.T) {
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
// Test handling server error
|
||||
errC := make(chan error)
|
||||
shutdownC := make(chan struct{})
|
||||
@@ -36,7 +39,7 @@ func TestWaitForSignal(t *testing.T) {
|
||||
}()
|
||||
|
||||
// received error, shutdownC should be closed
|
||||
err := waitForSignal(errC, shutdownC)
|
||||
err := waitForSignal(errC, shutdownC, logger)
|
||||
assert.Equal(t, serverErr, err)
|
||||
testChannelClosed(t, shutdownC)
|
||||
|
||||
@@ -56,7 +59,7 @@ func TestWaitForSignal(t *testing.T) {
|
||||
syscall.Kill(syscall.Getpid(), sig)
|
||||
}(sig)
|
||||
|
||||
err = waitForSignal(errC, shutdownC)
|
||||
err = waitForSignal(errC, shutdownC, logger)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, shutdownErr, <-errC)
|
||||
testChannelClosed(t, shutdownC)
|
||||
@@ -73,8 +76,10 @@ func TestWaitForSignalWithGraceShutdown(t *testing.T) {
|
||||
errC <- serverErr
|
||||
}()
|
||||
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
// received error, both shutdownC and graceshutdownC should be closed
|
||||
err := waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick)
|
||||
err := waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
|
||||
assert.Equal(t, serverErr, err)
|
||||
testChannelClosed(t, shutdownC)
|
||||
testChannelClosed(t, graceshutdownC)
|
||||
@@ -84,7 +89,7 @@ func TestWaitForSignalWithGraceShutdown(t *testing.T) {
|
||||
shutdownC = make(chan struct{})
|
||||
graceshutdownC = make(chan struct{})
|
||||
close(shutdownC)
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick)
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
|
||||
assert.NoError(t, err)
|
||||
testChannelClosed(t, shutdownC)
|
||||
testChannelClosed(t, graceshutdownC)
|
||||
@@ -94,7 +99,7 @@ func TestWaitForSignalWithGraceShutdown(t *testing.T) {
|
||||
shutdownC = make(chan struct{})
|
||||
graceshutdownC = make(chan struct{})
|
||||
close(graceshutdownC)
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick)
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
|
||||
assert.NoError(t, err)
|
||||
testChannelClosed(t, shutdownC)
|
||||
testChannelClosed(t, graceshutdownC)
|
||||
@@ -117,7 +122,7 @@ func TestWaitForSignalWithGraceShutdown(t *testing.T) {
|
||||
syscall.Kill(syscall.Getpid(), sig)
|
||||
}(sig)
|
||||
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick)
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, graceShutdownErr, <-errC)
|
||||
testChannelClosed(t, shutdownC)
|
||||
@@ -143,7 +148,7 @@ func TestWaitForSignalWithGraceShutdown(t *testing.T) {
|
||||
syscall.Kill(syscall.Getpid(), sig)
|
||||
}(sig)
|
||||
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick)
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, shutdownErr, <-errC)
|
||||
testChannelClosed(t, shutdownC)
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/certutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
)
|
||||
|
||||
var (
|
||||
showDeletedFlag = &cli.BoolFlag{
|
||||
Name: "show-deleted",
|
||||
Aliases: []string{"d"},
|
||||
Usage: "Include deleted tunnels in the list",
|
||||
}
|
||||
outputFormatFlag = &cli.StringFlag{
|
||||
Name: "output",
|
||||
Aliases: []string{"o"},
|
||||
Usage: "Render output using given `FORMAT`. Valid options are 'json' or 'yaml'",
|
||||
}
|
||||
forceFlag = &cli.BoolFlag{
|
||||
Name: "force",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "By default, if a tunnel is currently being run from a cloudflared, you can't " +
|
||||
"simultaneously rerun it again from a second cloudflared. The --force flag lets you " +
|
||||
"overwrite the previous tunnel. If you want to use a single hostname with multiple " +
|
||||
"tunnels, you can do so with Cloudflare's Load Balancer product.",
|
||||
}
|
||||
)
|
||||
|
||||
const hideSubcommands = true
|
||||
|
||||
func buildCreateCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "create",
|
||||
Action: cliutil.ErrorHandler(createTunnel),
|
||||
Usage: "Create a new tunnel with given name",
|
||||
ArgsUsage: "TUNNEL-NAME",
|
||||
Hidden: hideSubcommands,
|
||||
Flags: []cli.Flag{outputFormatFlag},
|
||||
}
|
||||
}
|
||||
|
||||
// generateTunnelSecret as an array of 32 bytes using secure random number generator
|
||||
func generateTunnelSecret() ([]byte, error) {
|
||||
randomBytes := make([]byte, 32)
|
||||
_, err := rand.Read(randomBytes)
|
||||
return randomBytes, err
|
||||
}
|
||||
|
||||
func createTunnel(c *cli.Context) error {
|
||||
if c.NArg() != 1 {
|
||||
return cliutil.UsageError(`"cloudflared tunnel create" requires exactly 1 argument, the name of tunnel to create.`)
|
||||
}
|
||||
name := c.Args().First()
|
||||
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
tunnelSecret, err := generateTunnelSecret()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
originCertPath, err := findOriginCert(c, logger)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error locating origin cert")
|
||||
}
|
||||
cert, err := getOriginCertFromContext(originCertPath, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := newTunnelstoreClient(c, cert, logger)
|
||||
|
||||
tunnel, err := client.CreateTunnel(name, tunnelSecret)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error creating a new tunnel")
|
||||
}
|
||||
|
||||
if writeFileErr := writeTunnelCredentials(tunnel.ID, cert.AccountID, originCertPath, tunnelSecret, logger); err != nil {
|
||||
var errorLines []string
|
||||
errorLines = append(errorLines, fmt.Sprintf("Your tunnel '%v' was created with ID %v. However, cloudflared couldn't write to the tunnel credentials file at %v.json.", tunnel.Name, tunnel.ID, tunnel.ID))
|
||||
errorLines = append(errorLines, fmt.Sprintf("The file-writing error is: %v", writeFileErr))
|
||||
if deleteErr := client.DeleteTunnel(tunnel.ID); deleteErr != nil {
|
||||
errorLines = append(errorLines, fmt.Sprintf("Cloudflared tried to delete the tunnel for you, but encountered an error. You should use `cloudflared tunnel delete %v` to delete the tunnel yourself, because the tunnel can't be run without the tunnelfile.", tunnel.ID))
|
||||
errorLines = append(errorLines, fmt.Sprintf("The delete tunnel error is: %v", deleteErr))
|
||||
} else {
|
||||
errorLines = append(errorLines, fmt.Sprintf("The tunnel was deleted, because the tunnel can't be run without the tunnelfile"))
|
||||
}
|
||||
errorMsg := strings.Join(errorLines, "\n")
|
||||
return errors.New(errorMsg)
|
||||
}
|
||||
|
||||
if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" {
|
||||
return renderOutput(outputFormat, &tunnel)
|
||||
}
|
||||
|
||||
logger.Infof("Created tunnel %s with id %s", tunnel.Name, tunnel.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func tunnelFilePath(tunnelID, originCertPath string) (string, error) {
|
||||
fileName := fmt.Sprintf("%v.json", tunnelID)
|
||||
return filepath.Clean(fmt.Sprintf("%v/../%v", originCertPath, fileName)), nil
|
||||
}
|
||||
|
||||
func writeTunnelCredentials(tunnelID, accountID, originCertPath string, tunnelSecret []byte, logger logger.Service) error {
|
||||
filePath, err := tunnelFilePath(tunnelID, originCertPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body, err := json.Marshal(pogs.TunnelAuth{
|
||||
AccountTag: accountID,
|
||||
TunnelSecret: tunnelSecret,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to marshal tunnel credentials to JSON")
|
||||
}
|
||||
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 readTunnelCredentials(tunnelID, originCertPath string) (*pogs.TunnelAuth, error) {
|
||||
filePath, err := tunnelFilePath(tunnelID, originCertPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "couldn't read tunnel credentials from %v", filePath)
|
||||
}
|
||||
|
||||
var auth pogs.TunnelAuth
|
||||
if err = json.Unmarshal(body, &auth); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &auth, nil
|
||||
}
|
||||
|
||||
func buildListCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "list",
|
||||
Action: cliutil.ErrorHandler(listTunnels),
|
||||
Usage: "List existing tunnels",
|
||||
ArgsUsage: " ",
|
||||
Hidden: hideSubcommands,
|
||||
Flags: []cli.Flag{outputFormatFlag, showDeletedFlag},
|
||||
}
|
||||
}
|
||||
|
||||
func listTunnels(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
originCertPath, err := findOriginCert(c, logger)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error locating origin cert")
|
||||
}
|
||||
cert, err := getOriginCertFromContext(originCertPath, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := newTunnelstoreClient(c, cert, logger)
|
||||
|
||||
allTunnels, err := client.ListTunnels()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error listing tunnels")
|
||||
}
|
||||
|
||||
var tunnels []tunnelstore.Tunnel
|
||||
if c.Bool("show-deleted") {
|
||||
tunnels = allTunnels
|
||||
} else {
|
||||
for _, tunnel := range allTunnels {
|
||||
if tunnel.DeletedAt.IsZero() {
|
||||
tunnels = append(tunnels, tunnel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" {
|
||||
return renderOutput(outputFormat, tunnels)
|
||||
}
|
||||
if len(tunnels) > 0 {
|
||||
const listFormat = "%-40s%-30s%-30s%s\n"
|
||||
fmt.Printf(listFormat, "ID", "NAME", "CREATED", "CONNECTIONS")
|
||||
for _, t := range tunnels {
|
||||
fmt.Printf(listFormat, t.ID, t.Name, t.CreatedAt.Format(time.RFC3339), fmtConnections(t.Connections))
|
||||
}
|
||||
} else {
|
||||
fmt.Println("You have no tunnels, use 'cloudflared tunnel create' to define a new tunnel")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fmtConnections(connections []tunnelstore.Connection) string {
|
||||
|
||||
// Count connections per colo
|
||||
numConnsPerColo := make(map[string]uint, len(connections))
|
||||
for _, connection := range connections {
|
||||
numConnsPerColo[connection.ColoName]++
|
||||
}
|
||||
|
||||
// Get sorted list of colos
|
||||
sortedColos := []string{}
|
||||
for coloName := range numConnsPerColo {
|
||||
sortedColos = append(sortedColos, coloName)
|
||||
}
|
||||
sort.Strings(sortedColos)
|
||||
|
||||
// Map each colo to its frequency, combine into output string.
|
||||
var output []string
|
||||
for _, coloName := range sortedColos {
|
||||
output = append(output, fmt.Sprintf("%dx%s", numConnsPerColo[coloName], coloName))
|
||||
}
|
||||
return strings.Join(output, ", ")
|
||||
}
|
||||
|
||||
func buildDeleteCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "delete",
|
||||
Action: cliutil.ErrorHandler(deleteTunnel),
|
||||
Usage: "Delete existing tunnel with given ID",
|
||||
ArgsUsage: "TUNNEL-ID",
|
||||
Hidden: hideSubcommands,
|
||||
}
|
||||
}
|
||||
|
||||
func deleteTunnel(c *cli.Context) error {
|
||||
if c.NArg() != 1 {
|
||||
return cliutil.UsageError(`"cloudflared tunnel delete" requires exactly 1 argument, the ID of the tunnel to delete.`)
|
||||
}
|
||||
id := c.Args().First()
|
||||
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
originCertPath, err := findOriginCert(c, logger)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error locating origin cert")
|
||||
}
|
||||
cert, err := getOriginCertFromContext(originCertPath, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := newTunnelstoreClient(c, cert, logger)
|
||||
|
||||
if err := client.DeleteTunnel(id); err != nil {
|
||||
return errors.Wrapf(err, "Error deleting tunnel %s", id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func renderOutput(format string, v interface{}) error {
|
||||
switch format {
|
||||
case "json":
|
||||
encoder := json.NewEncoder(os.Stdout)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(v)
|
||||
case "yaml":
|
||||
return yaml.NewEncoder(os.Stdout).Encode(v)
|
||||
default:
|
||||
return errors.Errorf("Unknown output format '%s'", format)
|
||||
}
|
||||
}
|
||||
|
||||
func newTunnelstoreClient(c *cli.Context, cert *certutil.OriginCert, logger logger.Service) tunnelstore.Client {
|
||||
client := tunnelstore.NewRESTClient(c.String("api-url"), cert.AccountID, cert.ServiceKey, logger)
|
||||
return client
|
||||
}
|
||||
|
||||
func getOriginCertFromContext(originCertPath string, logger logger.Service) (*certutil.OriginCert, error) {
|
||||
|
||||
blocks, err := readOriginCert(originCertPath, logger)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Can't read origin cert from %s", originCertPath)
|
||||
}
|
||||
|
||||
cert, err := certutil.DecodeOriginCert(blocks)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error decoding origin cert")
|
||||
}
|
||||
|
||||
if cert.AccountID == "" {
|
||||
return nil, errors.Errorf(`Origin certificate needs to be refreshed before creating new tunnels.\nDelete %s and run "cloudflared login" to obtain a new cert.`, originCertPath)
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func buildRunCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "run",
|
||||
Action: cliutil.ErrorHandler(runTunnel),
|
||||
Usage: "Proxy a local web server by running the given tunnel",
|
||||
ArgsUsage: "TUNNEL-ID",
|
||||
Hidden: hideSubcommands,
|
||||
Flags: []cli.Flag{forceFlag},
|
||||
}
|
||||
}
|
||||
|
||||
func runTunnel(c *cli.Context) error {
|
||||
if c.NArg() != 1 {
|
||||
return cliutil.UsageError(`"cloudflared tunnel run" requires exactly 1 argument, the ID of the tunnel to run.`)
|
||||
}
|
||||
id := c.Args().First()
|
||||
tunnelID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error parsing tunnel ID")
|
||||
}
|
||||
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
originCertPath, err := findOriginCert(c, logger)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error locating origin cert")
|
||||
}
|
||||
credentials, err := readTunnelCredentials(id, originCertPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Debugf("Read credentials for %v", credentials.AccountTag)
|
||||
return StartServer(c, version, shutdownC, graceShutdownC, &origin.NamedTunnelConfig{Auth: *credentials, ID: tunnelID})
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_fmtConnections(t *testing.T) {
|
||||
type args struct {
|
||||
connections []tunnelstore.Connection
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
args: args{
|
||||
connections: []tunnelstore.Connection{},
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "trivial",
|
||||
args: args{
|
||||
connections: []tunnelstore.Connection{
|
||||
{
|
||||
ColoName: "DFW",
|
||||
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "1xDFW",
|
||||
},
|
||||
{
|
||||
name: "many colos",
|
||||
args: args{
|
||||
connections: []tunnelstore.Connection{
|
||||
{
|
||||
ColoName: "YRV",
|
||||
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
|
||||
},
|
||||
{
|
||||
ColoName: "DFW",
|
||||
ID: uuid.MustParse("c13c0b3b-0fbf-453c-8169-a1990fced6d0"),
|
||||
},
|
||||
{
|
||||
ColoName: "ATL",
|
||||
ID: uuid.MustParse("70c90639-e386-4e8d-9a4e-7f046d70e63f"),
|
||||
},
|
||||
{
|
||||
ColoName: "DFW",
|
||||
ID: uuid.MustParse("30ad6251-0305-4635-a670-d3994f474981"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "1xATL, 2xDFW, 1xYRV",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := fmtConnections(tt.args.connections); got != tt.want {
|
||||
t.Errorf("fmtConnections() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTunnelfilePath(t *testing.T) {
|
||||
actual, err := tunnelFilePath("tunnel", "~/.cloudflared/cert.pem")
|
||||
assert.NoError(t, err)
|
||||
expected := "~/.cloudflared/tunnel.json"
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
@@ -10,9 +10,10 @@ import (
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/equinox-io/equinox"
|
||||
"github.com/facebookgo/grace/gracenet"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -30,7 +31,6 @@ dsCmJ/QZ6aw0w9qkkwEpne1Lmo6+0pGexZzFZOH6w5amShn+RXt7qkSid9iWlzGq
|
||||
EKx0BZogHSor9Wy5VztdFaAaVbsJiCbO
|
||||
-----END ECDSA PUBLIC KEY-----
|
||||
`)
|
||||
logger = log.CreateLogger()
|
||||
)
|
||||
|
||||
// BinaryUpdated implements ExitCoder interface, the app will exit with status code 11
|
||||
@@ -67,7 +67,7 @@ type UpdateOutcome struct {
|
||||
}
|
||||
|
||||
func (uo *UpdateOutcome) noUpdate() bool {
|
||||
return uo.Error != nil && uo.Updated == false
|
||||
return uo.Error == nil && uo.Updated == false
|
||||
}
|
||||
|
||||
func checkForUpdateAndApply() UpdateOutcome {
|
||||
@@ -93,7 +93,12 @@ func checkForUpdateAndApply() UpdateOutcome {
|
||||
}
|
||||
|
||||
func Update(_ *cli.Context) error {
|
||||
updateOutcome := loggedUpdate()
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
updateOutcome := loggedUpdate(logger)
|
||||
if updateOutcome.Error != nil {
|
||||
return &statusErr{updateOutcome.Error}
|
||||
}
|
||||
@@ -107,13 +112,13 @@ func Update(_ *cli.Context) error {
|
||||
}
|
||||
|
||||
// Checks for an update and applies it if one is available
|
||||
func loggedUpdate() UpdateOutcome {
|
||||
func loggedUpdate(logger logger.Service) UpdateOutcome {
|
||||
updateOutcome := checkForUpdateAndApply()
|
||||
if updateOutcome.Updated {
|
||||
logger.Infof("cloudflared has been updated to version %s", updateOutcome.Version)
|
||||
}
|
||||
if updateOutcome.Error != nil {
|
||||
logger.WithError(updateOutcome.Error).Error("update check failed")
|
||||
logger.Errorf("update check failed: %s", updateOutcome.Error)
|
||||
}
|
||||
|
||||
return updateOutcome
|
||||
@@ -124,6 +129,7 @@ type AutoUpdater struct {
|
||||
configurable *configurable
|
||||
listeners *gracenet.Net
|
||||
updateConfigChan chan *configurable
|
||||
logger logger.Service
|
||||
}
|
||||
|
||||
// AutoUpdaterConfigurable is the attributes of AutoUpdater that can be reconfigured during runtime
|
||||
@@ -132,7 +138,7 @@ type configurable struct {
|
||||
freq time.Duration
|
||||
}
|
||||
|
||||
func NewAutoUpdater(freq time.Duration, listeners *gracenet.Net) *AutoUpdater {
|
||||
func NewAutoUpdater(freq time.Duration, listeners *gracenet.Net, logger logger.Service) *AutoUpdater {
|
||||
updaterConfigurable := &configurable{
|
||||
enabled: true,
|
||||
freq: freq,
|
||||
@@ -145,6 +151,7 @@ func NewAutoUpdater(freq time.Duration, listeners *gracenet.Net) *AutoUpdater {
|
||||
configurable: updaterConfigurable,
|
||||
listeners: listeners,
|
||||
updateConfigChan: make(chan *configurable),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,20 +159,20 @@ func (a *AutoUpdater) Run(ctx context.Context) error {
|
||||
ticker := time.NewTicker(a.configurable.freq)
|
||||
for {
|
||||
if a.configurable.enabled {
|
||||
updateOutcome := loggedUpdate()
|
||||
updateOutcome := loggedUpdate(a.logger)
|
||||
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
|
||||
logger.Infof("Restarting service managed by SysV...")
|
||||
a.logger.Info("Restarting service managed by SysV...")
|
||||
pid, err := a.listeners.StartProcess()
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Unable to restart server automatically")
|
||||
a.logger.Errorf("Unable to restart server automatically: %s", err)
|
||||
return &statusErr{err: err}
|
||||
}
|
||||
// stop old process after autoupdate. Otherwise we create a new process
|
||||
// after each update
|
||||
logger.Infof("PID of the new process is %d", pid)
|
||||
a.logger.Infof("PID of the new process is %d", pid)
|
||||
}
|
||||
return &statusSuccess{newVersion: updateOutcome.Version}
|
||||
}
|
||||
@@ -197,14 +204,14 @@ func (a *AutoUpdater) Update(newFreq time.Duration) {
|
||||
a.updateConfigChan <- newConfigurable
|
||||
}
|
||||
|
||||
func IsAutoupdateEnabled(c *cli.Context) bool {
|
||||
if !SupportAutoUpdate() {
|
||||
func IsAutoupdateEnabled(c *cli.Context, l logger.Service) bool {
|
||||
if !SupportAutoUpdate(l) {
|
||||
return false
|
||||
}
|
||||
return !c.Bool("no-autoupdate") && c.Duration("autoupdate-freq") != 0
|
||||
}
|
||||
|
||||
func SupportAutoUpdate() bool {
|
||||
func SupportAutoUpdate(logger logger.Service) bool {
|
||||
if runtime.GOOS == "windows" {
|
||||
logger.Info(noUpdateOnWindowsMessage)
|
||||
return false
|
||||
|
||||
@@ -4,13 +4,15 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/facebookgo/grace/gracenet"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDisabledAutoUpdater(t *testing.T) {
|
||||
listeners := &gracenet.Net{}
|
||||
autoupdater := NewAutoUpdater(0, listeners)
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
autoupdater := NewAutoUpdater(0, listeners, logger)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
errC := make(chan error)
|
||||
go func() {
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/pkg/errors"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
@@ -65,6 +67,12 @@ func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
// 2. get ERROR_FAILED_SERVICE_CONTROLLER_CONNECT
|
||||
// This involves actually trying to start the service.
|
||||
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
isIntSess, err := svc.IsAnInteractiveSession()
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to determine if we are running in an interactive session: %v", err)
|
||||
@@ -97,9 +105,15 @@ type windowsService struct {
|
||||
|
||||
// called by the package code at the start of the service
|
||||
func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeRequest, statusChan chan<- svc.Status) (ssec bool, errno uint32) {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
elog, err := eventlog.Open(windowsServiceName)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Cannot open event log for %s", windowsServiceName)
|
||||
logger.Errorf("Cannot open event log for %s with error: %s", windowsServiceName, err)
|
||||
return
|
||||
}
|
||||
defer elog.Close()
|
||||
@@ -160,6 +174,11 @@ func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeReques
|
||||
}
|
||||
|
||||
func installWindowsService(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
logger.Infof("Installing Argo Tunnel Windows service")
|
||||
exepath, err := os.Executable()
|
||||
if err != nil {
|
||||
@@ -168,7 +187,7 @@ func installWindowsService(c *cli.Context) error {
|
||||
}
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Cannot establish a connection to the service control manager")
|
||||
logger.Errorf("Cannot establish a connection to the service control manager: %s", err)
|
||||
return err
|
||||
}
|
||||
defer m.Disconnect()
|
||||
@@ -189,18 +208,23 @@ func installWindowsService(c *cli.Context) error {
|
||||
err = eventlog.InstallAsEventCreate(windowsServiceName, eventlog.Error|eventlog.Warning|eventlog.Info)
|
||||
if err != nil {
|
||||
s.Delete()
|
||||
logger.WithError(err).Errorf("Cannot install event logger")
|
||||
logger.Errorf("Cannot install event logger: %s", err)
|
||||
return fmt.Errorf("SetupEventLogSource() failed: %s", err)
|
||||
}
|
||||
err = configRecoveryOption(s.Handle)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Cannot set service recovery actions")
|
||||
logger.Errorf("Cannot set service recovery actions: %s", err)
|
||||
logger.Infof("See %s to manually configure service recovery actions", windowsServiceUrl)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func uninstallWindowsService(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
logger.Infof("Uninstalling Argo Tunnel Windows Service")
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
const (
|
||||
openStreamTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
type Connection struct {
|
||||
id uuid.UUID
|
||||
muxer *h2mux.Muxer
|
||||
addr *net.TCPAddr
|
||||
isLongLived bool
|
||||
longLivedID int
|
||||
}
|
||||
|
||||
func newConnection(muxer *h2mux.Muxer, addr *net.TCPAddr) (*Connection, error) {
|
||||
id, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Connection{
|
||||
id: id,
|
||||
muxer: muxer,
|
||||
addr: addr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Connection) Serve(ctx context.Context) error {
|
||||
// Serve doesn't return until h2mux is shutdown
|
||||
return c.muxer.Serve(ctx)
|
||||
}
|
||||
|
||||
// Connect is used to establish connections with cloudflare's edge network
|
||||
func (c *Connection) Connect(ctx context.Context, parameters *tunnelpogs.ConnectParameters, logger *logrus.Entry) (tunnelpogs.ConnectResult, error) {
|
||||
tsClient, err := NewRPCClient(ctx, c.muxer, logger.WithField("rpc", "connect"), openStreamTimeout)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot create new RPC connection")
|
||||
}
|
||||
defer tsClient.Close()
|
||||
return tsClient.Connect(ctx, parameters)
|
||||
}
|
||||
|
||||
func (c *Connection) Shutdown() {
|
||||
c.muxer.Shutdown()
|
||||
}
|
||||
@@ -1,302 +0,0 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/streamhandler"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
const (
|
||||
quickStartLink = "https://developers.cloudflare.com/argo-tunnel/quickstart/"
|
||||
faqLink = "https://developers.cloudflare.com/argo-tunnel/faq/"
|
||||
defaultRetryAfter = time.Second * 5
|
||||
packageNamespace = "connection"
|
||||
edgeManagerSubsystem = "edgemanager"
|
||||
)
|
||||
|
||||
// EdgeManager manages connections with the edge
|
||||
type EdgeManager struct {
|
||||
// streamHandler handles stream opened by the edge
|
||||
streamHandler *streamhandler.StreamHandler
|
||||
// TLSConfig is the TLS configuration to connect with edge
|
||||
tlsConfig *tls.Config
|
||||
// cloudflaredConfig is the cloudflared configuration that is determined when the process first starts
|
||||
cloudflaredConfig *CloudflaredConfig
|
||||
// serviceDiscoverer returns the next edge addr to connect to
|
||||
serviceDiscoverer *edgediscovery.Edge
|
||||
// state is attributes of ConnectionManager that can change during runtime.
|
||||
state *edgeManagerState
|
||||
|
||||
logger *logrus.Entry
|
||||
|
||||
metrics *metrics
|
||||
}
|
||||
|
||||
type metrics struct {
|
||||
// activeStreams is a gauge shared by all muxers of this process to expose the total number of active streams
|
||||
activeStreams prometheus.Gauge
|
||||
}
|
||||
|
||||
func newMetrics(namespace, subsystem string) *metrics {
|
||||
return &metrics{
|
||||
activeStreams: h2mux.NewActiveStreamsMetrics(namespace, subsystem),
|
||||
}
|
||||
}
|
||||
|
||||
// EdgeManagerConfigurable is the configurable attributes of a EdgeConnectionManager
|
||||
type EdgeManagerConfigurable struct {
|
||||
TunnelHostnames []h2mux.TunnelHostname
|
||||
*tunnelpogs.EdgeConnectionConfig
|
||||
}
|
||||
|
||||
type CloudflaredConfig struct {
|
||||
CloudflaredID uuid.UUID
|
||||
Tags []tunnelpogs.Tag
|
||||
BuildInfo *buildinfo.BuildInfo
|
||||
IntentLabel string
|
||||
}
|
||||
|
||||
func NewEdgeManager(
|
||||
streamHandler *streamhandler.StreamHandler,
|
||||
edgeConnMgrConfigurable *EdgeManagerConfigurable,
|
||||
userCredential []byte,
|
||||
tlsConfig *tls.Config,
|
||||
serviceDiscoverer *edgediscovery.Edge,
|
||||
cloudflaredConfig *CloudflaredConfig,
|
||||
logger *logrus.Logger,
|
||||
) *EdgeManager {
|
||||
return &EdgeManager{
|
||||
streamHandler: streamHandler,
|
||||
tlsConfig: tlsConfig,
|
||||
cloudflaredConfig: cloudflaredConfig,
|
||||
serviceDiscoverer: serviceDiscoverer,
|
||||
state: newEdgeConnectionManagerState(edgeConnMgrConfigurable, userCredential),
|
||||
logger: logger.WithField("subsystem", "connectionManager"),
|
||||
metrics: newMetrics(packageNamespace, edgeManagerSubsystem),
|
||||
}
|
||||
}
|
||||
|
||||
func (em *EdgeManager) Run(ctx context.Context) error {
|
||||
defer em.shutdown()
|
||||
|
||||
// Currently, declarative tunnels don't have any concept of a stable connection
|
||||
// Each edge connection is transient and when it dies, it is replaced by a different one,
|
||||
// not restarted.
|
||||
// So in the future we should really change this so that n connections are stored individually
|
||||
connIndex := 0
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return errors.Wrap(ctx.Err(), "EdgeConnectionManager terminated")
|
||||
default:
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
// Create/delete connection one at a time, so we don't need to adjust for connections that are being created/deleted
|
||||
// in shouldCreateConnection or shouldReduceConnection calculation
|
||||
if em.state.shouldCreateConnection(em.serviceDiscoverer.AvailableAddrs()) {
|
||||
if connErr := em.newConnection(ctx, connIndex); connErr != nil {
|
||||
if !connErr.ShouldRetry {
|
||||
em.logger.WithError(connErr).Error(em.noRetryMessage())
|
||||
return connErr
|
||||
}
|
||||
em.logger.WithError(connErr).Error("cannot create new connection")
|
||||
} else {
|
||||
connIndex++
|
||||
}
|
||||
} else if em.state.shouldReduceConnection() {
|
||||
if err := em.closeConnection(ctx); err != nil {
|
||||
em.logger.WithError(err).Error("cannot close connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (em *EdgeManager) UpdateConfigurable(newConfigurable *EdgeManagerConfigurable) {
|
||||
em.logger.Infof("New edge connection manager configuration %+v", newConfigurable)
|
||||
em.state.updateConfigurable(newConfigurable)
|
||||
}
|
||||
|
||||
func (em *EdgeManager) newConnection(ctx context.Context, index int) *tunnelpogs.ConnectError {
|
||||
edgeTCPAddr, err := em.serviceDiscoverer.GetAddr(index)
|
||||
if err != nil {
|
||||
return retryConnection(fmt.Sprintf("edge address discovery error: %v", err))
|
||||
}
|
||||
configurable := em.state.getConfigurable()
|
||||
edgeConn, err := DialEdge(ctx, configurable.Timeout, em.tlsConfig, edgeTCPAddr)
|
||||
if err != nil {
|
||||
return retryConnection(fmt.Sprintf("dial edge error: %v", err))
|
||||
}
|
||||
// Establish a muxed connection with the edge
|
||||
// Client mux handshake with agent server
|
||||
muxer, err := h2mux.Handshake(edgeConn, edgeConn, h2mux.MuxerConfig{
|
||||
Timeout: configurable.Timeout,
|
||||
Handler: em.streamHandler,
|
||||
IsClient: true,
|
||||
HeartbeatInterval: configurable.HeartbeatInterval,
|
||||
MaxHeartbeats: configurable.MaxFailedHeartbeats,
|
||||
Logger: em.logger.WithField("subsystem", "muxer"),
|
||||
}, em.metrics.activeStreams)
|
||||
if err != nil {
|
||||
retryConnection(fmt.Sprintf("couldn't perform handshake with edge: %v", err))
|
||||
}
|
||||
|
||||
h2muxConn, err := newConnection(muxer, edgeTCPAddr)
|
||||
if err != nil {
|
||||
return retryConnection(fmt.Sprintf("couldn't create h2mux connection: %v", err))
|
||||
}
|
||||
|
||||
go em.serveConn(ctx, h2muxConn)
|
||||
|
||||
connResult, err := h2muxConn.Connect(ctx, &tunnelpogs.ConnectParameters{
|
||||
CloudflaredID: em.cloudflaredConfig.CloudflaredID,
|
||||
CloudflaredVersion: em.cloudflaredConfig.BuildInfo.CloudflaredVersion,
|
||||
NumPreviousAttempts: 0,
|
||||
OriginCert: em.state.getUserCredential(),
|
||||
IntentLabel: em.cloudflaredConfig.IntentLabel,
|
||||
Tags: em.cloudflaredConfig.Tags,
|
||||
}, em.logger)
|
||||
if err != nil {
|
||||
h2muxConn.Shutdown()
|
||||
return retryConnection(fmt.Sprintf("couldn't connect to edge: %v", err))
|
||||
}
|
||||
|
||||
if connErr := connResult.ConnectError(); connErr != nil {
|
||||
return connErr
|
||||
}
|
||||
|
||||
em.state.newConnection(h2muxConn)
|
||||
em.logger.Infof("connected to %s", connResult.ConnectedTo())
|
||||
|
||||
if connResult.ClientConfig() != nil {
|
||||
em.streamHandler.UseConfiguration(ctx, connResult.ClientConfig())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (em *EdgeManager) closeConnection(ctx context.Context) error {
|
||||
conn := em.state.getFirstConnection()
|
||||
if conn == nil {
|
||||
return fmt.Errorf("no connection to close")
|
||||
}
|
||||
conn.Shutdown()
|
||||
// teardown will be handled by EdgeManager.serveConn in another goroutine
|
||||
return nil
|
||||
}
|
||||
|
||||
func (em *EdgeManager) serveConn(ctx context.Context, conn *Connection) {
|
||||
err := conn.Serve(ctx)
|
||||
em.logger.WithError(err).Warn("Connection closed")
|
||||
em.state.closeConnection(conn)
|
||||
em.serviceDiscoverer.GiveBack(conn.addr)
|
||||
}
|
||||
|
||||
func (em *EdgeManager) noRetryMessage() string {
|
||||
messageTemplate := "cloudflared could not register an Argo Tunnel on your account. Please confirm the following before trying again:" +
|
||||
"1. You have Argo Smart Routing enabled in your account, See Enable Argo section of %s." +
|
||||
"2. Your credential at %s is still valid. See %s."
|
||||
return fmt.Sprintf(messageTemplate, quickStartLink, em.state.getConfigurable().UserCredentialPath, faqLink)
|
||||
}
|
||||
|
||||
func (em *EdgeManager) shutdown() {
|
||||
em.state.shutdown()
|
||||
}
|
||||
|
||||
type edgeManagerState struct {
|
||||
sync.RWMutex
|
||||
configurable *EdgeManagerConfigurable
|
||||
userCredential []byte
|
||||
conns map[uuid.UUID]*Connection
|
||||
}
|
||||
|
||||
func newEdgeConnectionManagerState(configurable *EdgeManagerConfigurable, userCredential []byte) *edgeManagerState {
|
||||
return &edgeManagerState{
|
||||
configurable: configurable,
|
||||
userCredential: userCredential,
|
||||
conns: make(map[uuid.UUID]*Connection),
|
||||
}
|
||||
}
|
||||
|
||||
func (ems *edgeManagerState) shouldCreateConnection(availableEdgeAddrs int) bool {
|
||||
ems.RLock()
|
||||
defer ems.RUnlock()
|
||||
expectedHAConns := int(ems.configurable.NumHAConnections)
|
||||
if availableEdgeAddrs < expectedHAConns {
|
||||
expectedHAConns = availableEdgeAddrs
|
||||
}
|
||||
return len(ems.conns) < expectedHAConns
|
||||
}
|
||||
|
||||
func (ems *edgeManagerState) shouldReduceConnection() bool {
|
||||
ems.RLock()
|
||||
defer ems.RUnlock()
|
||||
return uint8(len(ems.conns)) > ems.configurable.NumHAConnections
|
||||
}
|
||||
|
||||
func (ems *edgeManagerState) newConnection(conn *Connection) {
|
||||
ems.Lock()
|
||||
defer ems.Unlock()
|
||||
ems.conns[conn.id] = conn
|
||||
}
|
||||
|
||||
func (ems *edgeManagerState) closeConnection(conn *Connection) {
|
||||
ems.Lock()
|
||||
defer ems.Unlock()
|
||||
delete(ems.conns, conn.id)
|
||||
}
|
||||
|
||||
func (ems *edgeManagerState) getFirstConnection() *Connection {
|
||||
ems.RLock()
|
||||
defer ems.RUnlock()
|
||||
|
||||
for _, conn := range ems.conns {
|
||||
return conn
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ems *edgeManagerState) shutdown() {
|
||||
ems.Lock()
|
||||
defer ems.Unlock()
|
||||
for _, conn := range ems.conns {
|
||||
conn.Shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
func (ems *edgeManagerState) getConfigurable() *EdgeManagerConfigurable {
|
||||
ems.Lock()
|
||||
defer ems.Unlock()
|
||||
return ems.configurable
|
||||
}
|
||||
|
||||
func (ems *edgeManagerState) updateConfigurable(newConfigurable *EdgeManagerConfigurable) {
|
||||
ems.Lock()
|
||||
defer ems.Unlock()
|
||||
ems.configurable = newConfigurable
|
||||
}
|
||||
|
||||
func (ems *edgeManagerState) getUserCredential() []byte {
|
||||
ems.RLock()
|
||||
defer ems.RUnlock()
|
||||
return ems.userCredential
|
||||
}
|
||||
|
||||
func retryConnection(cause string) *tunnelpogs.ConnectError {
|
||||
return &tunnelpogs.ConnectError{
|
||||
Cause: cause,
|
||||
RetryAfter: defaultRetryAfter,
|
||||
ShouldRetry: true,
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/streamhandler"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
var (
|
||||
configurable = &EdgeManagerConfigurable{
|
||||
[]h2mux.TunnelHostname{
|
||||
"http.example.com",
|
||||
"ws.example.com",
|
||||
"hello.example.com",
|
||||
},
|
||||
&pogs.EdgeConnectionConfig{
|
||||
NumHAConnections: 1,
|
||||
HeartbeatInterval: 1 * time.Second,
|
||||
Timeout: 5 * time.Second,
|
||||
MaxFailedHeartbeats: 3,
|
||||
UserCredentialPath: "/etc/cloudflared/cert.pem",
|
||||
},
|
||||
}
|
||||
cloudflaredConfig = &CloudflaredConfig{
|
||||
CloudflaredID: uuid.New(),
|
||||
Tags: []pogs.Tag{
|
||||
{Name: "pool", Value: "east-6"},
|
||||
},
|
||||
BuildInfo: &buildinfo.BuildInfo{
|
||||
GoOS: "linux",
|
||||
GoVersion: "1.12",
|
||||
GoArch: "amd64",
|
||||
CloudflaredVersion: "2019.6.0",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func mockEdgeManager() *EdgeManager {
|
||||
newConfigChan := make(chan<- *pogs.ClientConfig)
|
||||
useConfigResultChan := make(<-chan *pogs.UseConfigurationResult)
|
||||
logger := logrus.New()
|
||||
edge := edgediscovery.MockEdge(logger, []*net.TCPAddr{})
|
||||
return NewEdgeManager(
|
||||
streamhandler.NewStreamHandler(newConfigChan, useConfigResultChan, logger),
|
||||
configurable,
|
||||
[]byte{},
|
||||
nil,
|
||||
edge,
|
||||
cloudflaredConfig,
|
||||
logger,
|
||||
)
|
||||
}
|
||||
|
||||
func TestUpdateConfigurable(t *testing.T) {
|
||||
m := mockEdgeManager()
|
||||
newConfigurable := &EdgeManagerConfigurable{
|
||||
[]h2mux.TunnelHostname{
|
||||
"second.example.com",
|
||||
},
|
||||
&pogs.EdgeConnectionConfig{
|
||||
NumHAConnections: 2,
|
||||
},
|
||||
}
|
||||
m.UpdateConfigurable(newConfigurable)
|
||||
|
||||
assert.Equal(t, newConfigurable, m.state.getConfigurable())
|
||||
}
|
||||
+2
-2
@@ -5,10 +5,10 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
rpc "zombiezen.com/go/capnproto2/rpc"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
func NewRPCClient(
|
||||
ctx context.Context,
|
||||
muxer *h2mux.Muxer,
|
||||
logger *logrus.Entry,
|
||||
logger logger.Service,
|
||||
openStreamTimeout time.Duration,
|
||||
) (client tunnelpogs.TunnelServer_PogsClient, err error) {
|
||||
openStreamCtx, openStreamCancel := context.WithTimeout(ctx, openStreamTimeout)
|
||||
|
||||
+10
-2
@@ -15,11 +15,19 @@ import (
|
||||
// The tunnel package is responsible for appending this to tunnel.Commands().
|
||||
func Cmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Category: "Database Connect (ALPHA)",
|
||||
Category: "Database Connect (ALPHA) - Deprecated",
|
||||
Name: "db-connect",
|
||||
Usage: "Access your SQL database from Cloudflare Workers or the browser",
|
||||
Usage: "deprecated: Access your SQL database from Cloudflare Workers or the browser",
|
||||
ArgsUsage: " ",
|
||||
Description: `
|
||||
This feature has been deprecated.
|
||||
Please see:
|
||||
|
||||
cloudflared access tcp --help
|
||||
|
||||
for setting up database connections to the cloudflare edge.
|
||||
|
||||
|
||||
Creates a connection between your database and the Cloudflare edge.
|
||||
Now you can execute SQL commands anywhere you can send HTTPS requests.
|
||||
|
||||
|
||||
+15
-8
@@ -11,17 +11,17 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/hello"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Proxy is an HTTP server that proxies requests to a Client.
|
||||
type Proxy struct {
|
||||
client Client
|
||||
accessValidator *validation.Access
|
||||
logger *logrus.Logger
|
||||
logger logger.Service
|
||||
}
|
||||
|
||||
// NewInsecureProxy creates a Proxy that talks to a Client at an origin.
|
||||
@@ -43,7 +43,12 @@ func NewInsecureProxy(ctx context.Context, origin string) (*Proxy, error) {
|
||||
return nil, errors.Wrap(err, "could not connect to the database")
|
||||
}
|
||||
|
||||
return &Proxy{client, nil, logrus.New()}, nil
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
return &Proxy{client, nil, logger}, nil
|
||||
}
|
||||
|
||||
// NewSecureProxy creates a Proxy that talks to a Client at an origin.
|
||||
@@ -90,7 +95,8 @@ func (proxy *Proxy) IsAllowed(r *http.Request, verbose ...bool) bool {
|
||||
// Warn administrators that invalid JWTs are being rejected. This is indicative
|
||||
// of either a misconfiguration of the CLI or a massive failure of upstream systems.
|
||||
if len(verbose) > 0 {
|
||||
proxy.httpLog(r, err).Error("Failed JWT authentication")
|
||||
cfRay := proxy.getRayHeader(r)
|
||||
proxy.logger.Infof("dbproxy: Failed JWT authentication: cf-ray: %s %s", cfRay, err)
|
||||
}
|
||||
|
||||
return false
|
||||
@@ -234,13 +240,14 @@ func (proxy *Proxy) httpRespondErr(w http.ResponseWriter, r *http.Request, defau
|
||||
|
||||
proxy.httpRespond(w, r, status, err.Error())
|
||||
if len(err.Error()) > 0 {
|
||||
proxy.httpLog(r, err).Warn("Database proxy error")
|
||||
cfRay := proxy.getRayHeader(r)
|
||||
proxy.logger.Infof("dbproxy: Database proxy error: cf-ray: %s %s", cfRay, err)
|
||||
}
|
||||
}
|
||||
|
||||
// httpLog returns a logrus.Entry that is formatted to output a request Cf-ray.
|
||||
func (proxy *Proxy) httpLog(r *http.Request, err error) *logrus.Entry {
|
||||
return proxy.logger.WithContext(r.Context()).WithField("CF-RAY", r.Header.Get("Cf-ray")).WithError(err)
|
||||
// getRayHeader returns the request's Cf-ray header.
|
||||
func (proxy *Proxy) getRayHeader(r *http.Request) string {
|
||||
return r.Header.Get("Cf-ray")
|
||||
}
|
||||
|
||||
// httpError extracts common errors and returns an status code and friendly error.
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -58,15 +58,15 @@ var friendlyDNSErrorLines = []string{
|
||||
}
|
||||
|
||||
// EdgeDiscovery implements HA service discovery lookup.
|
||||
func edgeDiscovery(logger *logrus.Entry) ([][]*net.TCPAddr, error) {
|
||||
func edgeDiscovery(logger logger.Service) ([][]*net.TCPAddr, error) {
|
||||
_, addrs, err := netLookupSRV(srvService, srvProto, srvName)
|
||||
if err != nil {
|
||||
_, fallbackAddrs, fallbackErr := fallbackLookupSRV(srvService, srvProto, srvName)
|
||||
if fallbackErr != nil || len(fallbackAddrs) == 0 {
|
||||
// use the original DNS error `err` in messages, not `fallbackErr`
|
||||
logger.Errorln("Error looking up Cloudflare edge IPs: the DNS query failed:", err)
|
||||
logger.Errorf("Error looking up Cloudflare edge IPs: the DNS query failed: %s", err)
|
||||
for _, s := range friendlyDNSErrorLines {
|
||||
logger.Errorln(s)
|
||||
logger.Error(s)
|
||||
}
|
||||
return nil, errors.Wrapf(err, "Could not lookup srv records on _%v._%v.%v", srvService, srvProto, srvName)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package allregions
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -19,7 +19,8 @@ func TestEdgeDiscovery(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
addrLists, err := edgeDiscovery(logrus.New().WithFields(logrus.Fields{}))
|
||||
l := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
addrLists, err := edgeDiscovery(l)
|
||||
assert.NoError(t, err)
|
||||
actualAddrSet := map[string]bool{}
|
||||
for _, addrs := range addrLists {
|
||||
|
||||
@@ -2,8 +2,6 @@ package allregions
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Region contains cloudflared edge addresses. The edge is partitioned into several regions for
|
||||
@@ -59,7 +57,7 @@ func (r Region) GetUnusedIP(excluding *net.TCPAddr) *net.TCPAddr {
|
||||
// Use the address, assigning it to a proxy connection.
|
||||
func (r Region) Use(addr *net.TCPAddr, connID int) {
|
||||
if addr == nil {
|
||||
logrus.Errorf("Attempted to use nil address for connection %d", connID)
|
||||
//logrus.Errorf("Attempted to use nil address for connection %d", connID)
|
||||
return
|
||||
}
|
||||
r.connFor[addr] = InUse(connID)
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
// Regions stores Cloudflare edge network IPs, partitioned into two regions.
|
||||
@@ -19,7 +19,7 @@ type Regions struct {
|
||||
// ------------------------------------
|
||||
|
||||
// ResolveEdge resolves the Cloudflare edge, returning all regions discovered.
|
||||
func ResolveEdge(logger *logrus.Entry) (*Regions, error) {
|
||||
func ResolveEdge(logger logger.Service) (*Regions, error) {
|
||||
addrLists, err := edgeDiscovery(logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -6,8 +6,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -20,7 +19,7 @@ var errNoAddressesLeft = fmt.Errorf("There are no free edge addresses left")
|
||||
type Edge struct {
|
||||
regions *allregions.Regions
|
||||
sync.Mutex
|
||||
logger *logrus.Entry
|
||||
logger logger.Service
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
@@ -29,37 +28,34 @@ type Edge struct {
|
||||
|
||||
// ResolveEdge runs the initial discovery of the Cloudflare edge, finding Addrs that can be allocated
|
||||
// to connections.
|
||||
func ResolveEdge(l *logrus.Logger) (*Edge, error) {
|
||||
logger := l.WithField("subsystem", subsystem)
|
||||
regions, err := allregions.ResolveEdge(logger)
|
||||
func ResolveEdge(l logger.Service) (*Edge, error) {
|
||||
regions, err := allregions.ResolveEdge(l)
|
||||
if err != nil {
|
||||
return new(Edge), err
|
||||
}
|
||||
return &Edge{
|
||||
logger: logger,
|
||||
logger: l,
|
||||
regions: regions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// StaticEdge creates a list of edge addresses from the list of hostnames. Mainly used for testing connectivity.
|
||||
func StaticEdge(l *logrus.Logger, hostnames []string) (*Edge, error) {
|
||||
logger := l.WithField("subsystem", subsystem)
|
||||
func StaticEdge(l logger.Service, hostnames []string) (*Edge, error) {
|
||||
regions, err := allregions.StaticEdge(hostnames)
|
||||
if err != nil {
|
||||
return new(Edge), err
|
||||
}
|
||||
return &Edge{
|
||||
logger: logger,
|
||||
logger: l,
|
||||
regions: regions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MockEdge creates a Cloudflare Edge from arbitrary TCP addresses. Used for testing.
|
||||
func MockEdge(l *logrus.Logger, addrs []*net.TCPAddr) *Edge {
|
||||
logger := l.WithField("subsystem", subsystem)
|
||||
func MockEdge(l logger.Service, addrs []*net.TCPAddr) *Edge {
|
||||
regions := allregions.NewNoResolve(addrs)
|
||||
return &Edge{
|
||||
logger: logger,
|
||||
logger: l,
|
||||
regions: regions,
|
||||
}
|
||||
}
|
||||
@@ -83,24 +79,20 @@ func (ed *Edge) GetAddrForRPC() (*net.TCPAddr, error) {
|
||||
func (ed *Edge) GetAddr(connID int) (*net.TCPAddr, error) {
|
||||
ed.Lock()
|
||||
defer ed.Unlock()
|
||||
logger := ed.logger.WithFields(logrus.Fields{
|
||||
"connID": connID,
|
||||
"function": "GetAddr",
|
||||
})
|
||||
|
||||
// If this connection has already used an edge addr, return it.
|
||||
if addr := ed.regions.AddrUsedBy(connID); addr != nil {
|
||||
logger.Debug("Returning same address back to proxy connection")
|
||||
ed.logger.Debugf("edgediscovery - GetAddr: Returning same address back to proxy connection: connID: %d", connID)
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// Otherwise, give it an unused one
|
||||
addr := ed.regions.GetUnusedAddr(nil, connID)
|
||||
if addr == nil {
|
||||
logger.Debug("No addresses left to give proxy connection")
|
||||
ed.logger.Debugf("edgediscovery - GetAddr: No addresses left to give proxy connection: connID: %d", connID)
|
||||
return nil, errNoAddressesLeft
|
||||
}
|
||||
logger.Debugf("Giving connection its new address %s", addr)
|
||||
ed.logger.Debugf("edgediscovery - GetAddr: Giving connection its new address %s: connID: %d", addr, connID)
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
@@ -108,10 +100,6 @@ func (ed *Edge) GetAddr(connID int) (*net.TCPAddr, error) {
|
||||
func (ed *Edge) GetDifferentAddr(connID int) (*net.TCPAddr, error) {
|
||||
ed.Lock()
|
||||
defer ed.Unlock()
|
||||
logger := ed.logger.WithFields(logrus.Fields{
|
||||
"connID": connID,
|
||||
"function": "GetDifferentAddr",
|
||||
})
|
||||
|
||||
oldAddr := ed.regions.AddrUsedBy(connID)
|
||||
if oldAddr != nil {
|
||||
@@ -119,11 +107,11 @@ func (ed *Edge) GetDifferentAddr(connID int) (*net.TCPAddr, error) {
|
||||
}
|
||||
addr := ed.regions.GetUnusedAddr(oldAddr, connID)
|
||||
if addr == nil {
|
||||
logger.Debug("No addresses left to give proxy connection")
|
||||
ed.logger.Debugf("edgediscovery - GetDifferentAddr: No addresses left to give proxy connection: connID: %d", connID)
|
||||
// note: if oldAddr were not nil, it will become available on the next iteration
|
||||
return nil, errNoAddressesLeft
|
||||
}
|
||||
logger.Debugf("Giving connection its new address %s", addr)
|
||||
ed.logger.Debugf("edgediscovery - GetDifferentAddr: Giving connection its new address %s: connID: %d", addr, connID)
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
@@ -139,6 +127,6 @@ func (ed *Edge) AvailableAddrs() int {
|
||||
func (ed *Edge) GiveBack(addr *net.TCPAddr) bool {
|
||||
ed.Lock()
|
||||
defer ed.Unlock()
|
||||
ed.logger.WithField("function", "GiveBack").Debug("Address now unused")
|
||||
ed.logger.Debug("edgediscovery - GiveBack: Address now unused")
|
||||
return ed.regions.GiveBack(addr)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -32,7 +32,7 @@ var (
|
||||
)
|
||||
|
||||
func TestGiveBack(t *testing.T) {
|
||||
l := logrus.New()
|
||||
l := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
edge := MockEdge(l, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
|
||||
// Give this connection an address
|
||||
@@ -49,7 +49,7 @@ func TestGiveBack(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRPCAndProxyShareSingleEdgeIP(t *testing.T) {
|
||||
l := logrus.New()
|
||||
l := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
// Make an edge with a single IP
|
||||
edge := MockEdge(l, []*net.TCPAddr{&addr0})
|
||||
@@ -66,7 +66,7 @@ func TestRPCAndProxyShareSingleEdgeIP(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetAddrForRPC(t *testing.T) {
|
||||
l := logrus.New()
|
||||
l := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
edge := MockEdge(l, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
|
||||
// Get a connection
|
||||
@@ -84,7 +84,7 @@ func TestGetAddrForRPC(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOnePerRegion(t *testing.T) {
|
||||
l := logrus.New()
|
||||
l := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
// Make an edge with only one address
|
||||
edge := MockEdge(l, []*net.TCPAddr{&addr0, &addr1})
|
||||
@@ -108,7 +108,7 @@ func TestOnePerRegion(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOnlyOneAddrLeft(t *testing.T) {
|
||||
l := logrus.New()
|
||||
l := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
// Make an edge with only one address
|
||||
edge := MockEdge(l, []*net.TCPAddr{&addr0})
|
||||
@@ -130,7 +130,7 @@ func TestOnlyOneAddrLeft(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNoAddrsLeft(t *testing.T) {
|
||||
l := logrus.New()
|
||||
l := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
// Make an edge with no addresses
|
||||
edge := MockEdge(l, []*net.TCPAddr{})
|
||||
@@ -142,7 +142,7 @@ func TestNoAddrsLeft(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetAddr(t *testing.T) {
|
||||
l := logrus.New()
|
||||
l := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
edge := MockEdge(l, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
|
||||
// Give this connection an address
|
||||
@@ -158,7 +158,7 @@ func TestGetAddr(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetDifferentAddr(t *testing.T) {
|
||||
l := logrus.New()
|
||||
l := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
edge := MockEdge(l, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
|
||||
// Give this connection an address
|
||||
|
||||
Executable
+128
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/python3
|
||||
"""
|
||||
Creates Github Releases and uploads assets
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
|
||||
from github import Github, GithubException, UnknownObjectException
|
||||
|
||||
FORMAT = "%(levelname)s - %(asctime)s: %(message)s"
|
||||
logging.basicConfig(format=FORMAT)
|
||||
|
||||
CLOUDFLARED_REPO = os.environ.get("GITHUB_REPO", "cloudflare/cloudflared")
|
||||
GITHUB_CONFLICT_CODE = "already_exists"
|
||||
|
||||
|
||||
def assert_tag_exists(repo, version):
|
||||
""" Raise exception if repo does not contain a tag matching version """
|
||||
tags = repo.get_tags()
|
||||
if not tags or tags[0].name != version:
|
||||
raise Exception("Tag {} not found".format(version))
|
||||
|
||||
|
||||
def get_or_create_release(repo, version, dry_run=False):
|
||||
"""
|
||||
Get a Github Release matching the version tag or create a new one.
|
||||
If a conflict occurs on creation, attempt to fetch the Release on last time
|
||||
"""
|
||||
try:
|
||||
release = repo.get_release(version)
|
||||
logging.info("Release %s found", version)
|
||||
return release
|
||||
except UnknownObjectException:
|
||||
logging.info("Release %s not found", version)
|
||||
|
||||
# We dont want to create a new release tag if one doesnt already exist
|
||||
assert_tag_exists(repo, version)
|
||||
|
||||
if dry_run:
|
||||
logging.info("Skipping Release creation because of dry-run")
|
||||
return
|
||||
|
||||
try:
|
||||
logging.info("Creating release %s", version)
|
||||
return repo.create_git_release(version, version, "")
|
||||
except GithubException as e:
|
||||
errors = e.data.get("errors", [])
|
||||
if e.status == 422 and any(
|
||||
[err.get("code") == GITHUB_CONFLICT_CODE for err in errors]
|
||||
):
|
||||
logging.warning(
|
||||
"Conflict: Release was likely just made by a different build: %s",
|
||||
e.data,
|
||||
)
|
||||
return repo.get_release(version)
|
||||
raise e
|
||||
|
||||
|
||||
def parse_args():
|
||||
""" Parse and validate args """
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Creates Github Releases and uploads assets."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--api-key", default=os.environ.get("API_KEY"), help="Github API key"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--release-version",
|
||||
metavar="version",
|
||||
default=os.environ.get("VERSION"),
|
||||
help="Release version",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--path", default=os.environ.get("ASSET_PATH"), help="Asset path"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--name", default=os.environ.get("ASSET_NAME"), help="Asset Name"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run", action="store_true", help="Do not create release or upload asset"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
is_valid = True
|
||||
if not args.release_version:
|
||||
logging.error("Missing release version")
|
||||
is_valid = False
|
||||
|
||||
if not args.path:
|
||||
logging.error("Missing asset path")
|
||||
is_valid = False
|
||||
|
||||
if not args.name:
|
||||
logging.error("Missing asset name")
|
||||
is_valid = False
|
||||
|
||||
if not args.api_key:
|
||||
logging.error("Missing API key")
|
||||
is_valid = False
|
||||
|
||||
if is_valid:
|
||||
return args
|
||||
|
||||
parser.print_usage()
|
||||
exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
""" Attempts to upload Asset to Github Release. Creates Release if it doesnt exist """
|
||||
try:
|
||||
args = parse_args()
|
||||
client = Github(args.api_key)
|
||||
repo = client.get_repo(CLOUDFLARED_REPO)
|
||||
release = get_or_create_release(repo, args.release_version, args.dry_run)
|
||||
|
||||
if args.dry_run:
|
||||
logging.info("Skipping asset upload because of dry-run")
|
||||
return
|
||||
|
||||
release.upload_asset(args.path, name=args.name)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
exit(1)
|
||||
|
||||
|
||||
main()
|
||||
@@ -5,12 +5,14 @@ go 1.12
|
||||
require (
|
||||
github.com/BurntSushi/go-sumtype v0.0.0-20190304192233-fcb4a6205bdc // indirect
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3
|
||||
github.com/acmacalister/skittles v0.0.0-20160609003031-7423546701e1
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect
|
||||
github.com/aws/aws-sdk-go v1.25.8
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect
|
||||
github.com/cloudflare/brotli-go v0.0.0-20191101163834-d34379f7ff93
|
||||
github.com/cloudflare/cfssl v0.0.0-20141119014638-2f7f44e802e2
|
||||
github.com/cloudflare/cfssl v0.0.0-20141119014638-2f7f44e802e2 // indirect
|
||||
github.com/cloudflare/golibs v0.0.0-20170913112048-333127dbecfc
|
||||
github.com/coredns/coredns v1.2.0
|
||||
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73
|
||||
@@ -37,7 +39,7 @@ require (
|
||||
github.com/kshvakov/clickhouse v1.3.11
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/lib/pq v1.2.0
|
||||
github.com/mattn/go-colorable v0.1.4
|
||||
github.com/mattn/go-colorable v0.1.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.10 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.11.0
|
||||
github.com/mholt/caddy v0.0.0-20180807230124-d3b731e9255b // indirect
|
||||
@@ -51,16 +53,15 @@ require (
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 // indirect
|
||||
github.com/prometheus/common v0.7.0 // indirect
|
||||
github.com/prometheus/procfs v0.0.5 // indirect
|
||||
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 // indirect
|
||||
github.com/stretchr/testify v1.3.0
|
||||
github.com/tinylib/msgp v1.1.0 // indirect
|
||||
github.com/xo/dburl v0.0.0-20191005012637-293c3298d6c0
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
|
||||
golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 // indirect
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
||||
golang.org/x/sys v0.0.0-20191020212454-3e7259c5e7c2
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae
|
||||
google.golang.org/appengine v1.5.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20191007204434-a023cd5227bd // indirect
|
||||
google.golang.org/grpc v1.24.0 // indirect
|
||||
@@ -68,7 +69,7 @@ require (
|
||||
gopkg.in/square/go-jose.v2 v2.4.0 // indirect
|
||||
gopkg.in/urfave/cli.v2 v2.0.0-20180128181224-d604b6ffeee8
|
||||
gopkg.in/yaml.v2 v2.2.4
|
||||
zombiezen.com/go/capnproto2 v0.0.0-20180616160808-7cfd211c19c7
|
||||
zombiezen.com/go/capnproto2 v2.18.0+incompatible
|
||||
)
|
||||
|
||||
// ../../go/pkg/mod/github.com/coredns/coredns@v1.2.0/plugin/metrics/metrics.go:40:49: too many arguments in call to prometheus.NewProcessCollector
|
||||
|
||||
@@ -9,8 +9,11 @@ github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFD
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
||||
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
|
||||
github.com/acmacalister/skittles v0.0.0-20160609003031-7423546701e1 h1:RKnVV4C7qoN/sToLX2y1dqH7T6kKLMHcwRJlgwb9Ggk=
|
||||
github.com/acmacalister/skittles v0.0.0-20160609003031-7423546701e1/go.mod h1:gI5CyA/CEnS6eqNV22rqs4dG3aGfaSbXgPORIlwr2r0=
|
||||
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
@@ -201,6 +204,10 @@ golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
@@ -219,6 +226,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582 h1:p9xBe/w/OzkeYVKm234g55gMdD1nSIooTir5kV11kfA=
|
||||
golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
|
||||
@@ -240,6 +249,9 @@ golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9Y
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191020212454-3e7259c5e7c2 h1:nq114VpM8lsSlP+lyUbANecYHYiFcSNFtqcBlxRV+gA=
|
||||
golang.org/x/sys v0.0.0-20191020212454-3e7259c5e7c2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
@@ -282,3 +294,5 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
zombiezen.com/go/capnproto2 v0.0.0-20180616160808-7cfd211c19c7 h1:CZoOFlTPbKfAShKYrMuUfYbnXexFT1rYRUX1SPnrdE4=
|
||||
zombiezen.com/go/capnproto2 v0.0.0-20180616160808-7cfd211c19c7/go.mod h1:TMGa8HWGJkXiq4nHe9Zu/JgRF5oUtg4XizFC+Vexbec=
|
||||
zombiezen.com/go/capnproto2 v2.18.0+incompatible h1:mwfXZniffG5mXokQGHUJWGnqIBggoPfT/CEwon9Yess=
|
||||
zombiezen.com/go/capnproto2 v2.18.0+incompatible/go.mod h1:XO5Pr2SbXgqZwn0m0Ru54QBqpOf4K5AYBO+8LAOBQEQ=
|
||||
|
||||
+6
-6
@@ -7,8 +7,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/hpack"
|
||||
"golang.org/x/sync/errgroup"
|
||||
@@ -48,7 +48,7 @@ type MuxerConfig struct {
|
||||
// The minimum number of heartbeats to send before terminating the connection.
|
||||
MaxHeartbeats uint64
|
||||
// Logger to use
|
||||
Logger *log.Entry
|
||||
Logger logger.Service
|
||||
CompressionQuality CompressionSetting
|
||||
// Initial size for HTTP2 flow control windows
|
||||
DefaultWindowSize uint32
|
||||
@@ -136,10 +136,10 @@ func Handshake(
|
||||
handshakeSetting := http2.Setting{ID: SettingMuxerMagic, Val: MuxerMagicEdge}
|
||||
compressionSetting := http2.Setting{ID: SettingCompression, Val: config.CompressionQuality.toH2Setting()}
|
||||
if CompressionIsSupported() {
|
||||
log.Debug("Compression is supported")
|
||||
config.Logger.Debug("muxer: Compression is supported")
|
||||
m.compressionQuality = config.CompressionQuality.getPreset()
|
||||
} else {
|
||||
log.Debug("Compression is not supported")
|
||||
config.Logger.Debug("muxer: Compression is not supported")
|
||||
compressionSetting = http2.Setting{ID: SettingCompression, Val: 0}
|
||||
}
|
||||
|
||||
@@ -176,12 +176,12 @@ func Handshake(
|
||||
// Sanity check to enusre idelDuration is sane
|
||||
if idleDuration == 0 || idleDuration < defaultTimeout {
|
||||
idleDuration = defaultTimeout
|
||||
config.Logger.Warn("Minimum idle time has been adjusted to ", defaultTimeout)
|
||||
config.Logger.Infof("muxer: Minimum idle time has been adjusted to %d", defaultTimeout)
|
||||
}
|
||||
maxRetries := config.MaxHeartbeats
|
||||
if maxRetries == 0 {
|
||||
maxRetries = defaultRetries
|
||||
config.Logger.Warn("Minimum number of unacked heartbeats to send before closing the connection has been adjusted to ", maxRetries)
|
||||
config.Logger.Infof("muxer: Minimum number of unacked heartbeats to send before closing the connection has been adjusted to %d", maxRetries)
|
||||
}
|
||||
|
||||
compBytesBefore, compBytesAfter := NewAtomicCounter(0), NewAtomicCounter(0)
|
||||
|
||||
+24
-18
@@ -15,8 +15,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
@@ -28,7 +28,7 @@ const (
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if os.Getenv("VERBOSE") == "1" {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
//TODO: set log level
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
@@ -51,7 +51,7 @@ func NewDefaultMuxerPair(t assert.TestingT, testName string, f MuxedStreamFunc)
|
||||
Handler: f,
|
||||
IsClient: true,
|
||||
Name: "origin",
|
||||
Logger: log.NewEntry(log.New()),
|
||||
Logger: logger.NewOutputWriter(logger.NewMockWriteManager()),
|
||||
DefaultWindowSize: (1 << 8) - 1,
|
||||
MaxWindowSize: (1 << 15) - 1,
|
||||
StreamWriteBufferMaxLen: 1024,
|
||||
@@ -63,7 +63,7 @@ func NewDefaultMuxerPair(t assert.TestingT, testName string, f MuxedStreamFunc)
|
||||
Timeout: testHandshakeTimeout,
|
||||
IsClient: false,
|
||||
Name: "edge",
|
||||
Logger: log.NewEntry(log.New()),
|
||||
Logger: logger.NewOutputWriter(logger.NewMockWriteManager()),
|
||||
DefaultWindowSize: (1 << 8) - 1,
|
||||
MaxWindowSize: (1 << 15) - 1,
|
||||
StreamWriteBufferMaxLen: 1024,
|
||||
@@ -86,7 +86,7 @@ func NewCompressedMuxerPair(t assert.TestingT, testName string, quality Compress
|
||||
IsClient: true,
|
||||
Name: "origin",
|
||||
CompressionQuality: quality,
|
||||
Logger: log.NewEntry(log.New()),
|
||||
Logger: logger.NewOutputWriter(logger.NewMockWriteManager()),
|
||||
HeartbeatInterval: defaultTimeout,
|
||||
MaxHeartbeats: defaultRetries,
|
||||
},
|
||||
@@ -96,7 +96,7 @@ func NewCompressedMuxerPair(t assert.TestingT, testName string, quality Compress
|
||||
IsClient: false,
|
||||
Name: "edge",
|
||||
CompressionQuality: quality,
|
||||
Logger: log.NewEntry(log.New()),
|
||||
Logger: logger.NewOutputWriter(logger.NewMockWriteManager()),
|
||||
HeartbeatInterval: defaultTimeout,
|
||||
MaxHeartbeats: defaultRetries,
|
||||
},
|
||||
@@ -301,6 +301,7 @@ func TestSingleStreamLargeResponseBody(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMultipleStreams(t *testing.T) {
|
||||
l := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
f := MuxedStreamFunc(func(stream *MuxedStream) error {
|
||||
if len(stream.Headers) != 1 {
|
||||
t.Fatalf("expected %d headers, got %d", 1, len(stream.Headers))
|
||||
@@ -308,13 +309,13 @@ func TestMultipleStreams(t *testing.T) {
|
||||
if stream.Headers[0].Name != "client-token" {
|
||||
t.Fatalf("expected header name %s, got %s", "client-token", stream.Headers[0].Name)
|
||||
}
|
||||
log.Debugf("Got request for stream %s", stream.Headers[0].Value)
|
||||
l.Debugf("Got request for stream %s", stream.Headers[0].Value)
|
||||
stream.WriteHeaders([]Header{
|
||||
{Name: "response-token", Value: stream.Headers[0].Value},
|
||||
})
|
||||
log.Debugf("Wrote headers for stream %s", stream.Headers[0].Value)
|
||||
l.Debugf("Wrote headers for stream %s", stream.Headers[0].Value)
|
||||
stream.Write([]byte("OK"))
|
||||
log.Debugf("Wrote body for stream %s", stream.Headers[0].Value)
|
||||
l.Debugf("Wrote body for stream %s", stream.Headers[0].Value)
|
||||
return nil
|
||||
})
|
||||
muxPair := NewDefaultMuxerPair(t, t.Name(), f)
|
||||
@@ -332,7 +333,7 @@ func TestMultipleStreams(t *testing.T) {
|
||||
[]Header{{Name: "client-token", Value: tokenString}},
|
||||
nil,
|
||||
)
|
||||
log.Debugf("Got headers for stream %d", tokenId)
|
||||
l.Debugf("Got headers for stream %d", tokenId)
|
||||
if err != nil {
|
||||
errorsC <- err
|
||||
return
|
||||
@@ -370,7 +371,7 @@ func TestMultipleStreams(t *testing.T) {
|
||||
testFail := false
|
||||
for err := range errorsC {
|
||||
testFail = true
|
||||
log.Error(err)
|
||||
l.Errorf("%s", err)
|
||||
}
|
||||
if testFail {
|
||||
t.Fatalf("TestMultipleStreams failed")
|
||||
@@ -448,6 +449,8 @@ func TestMultipleStreamsFlowControl(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGracefulShutdown(t *testing.T) {
|
||||
l := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
sendC := make(chan struct{})
|
||||
responseBuf := bytes.Repeat([]byte("Hello world"), 65536)
|
||||
|
||||
@@ -456,17 +459,17 @@ func TestGracefulShutdown(t *testing.T) {
|
||||
{Name: "response-header", Value: "responseValue"},
|
||||
})
|
||||
<-sendC
|
||||
log.Debugf("Writing %d bytes", len(responseBuf))
|
||||
l.Debugf("Writing %d bytes", len(responseBuf))
|
||||
stream.Write(responseBuf)
|
||||
stream.CloseWrite()
|
||||
log.Debugf("Wrote %d bytes", len(responseBuf))
|
||||
l.Debugf("Wrote %d bytes", len(responseBuf))
|
||||
// Reading from the stream will block until the edge closes its end of the stream.
|
||||
// Otherwise, we'll close the whole connection before receiving the 'stream closed'
|
||||
// message from the edge.
|
||||
// Graceful shutdown works if you omit this, it just gives spurious errors for now -
|
||||
// TODO ignore errors when writing 'stream closed' and we're shutting down.
|
||||
stream.Read([]byte{0})
|
||||
log.Debugf("Handler ends")
|
||||
l.Debugf("Handler ends")
|
||||
return nil
|
||||
})
|
||||
muxPair := NewDefaultMuxerPair(t, t.Name(), f)
|
||||
@@ -483,7 +486,7 @@ func TestGracefulShutdown(t *testing.T) {
|
||||
muxPair.EdgeMux.Shutdown()
|
||||
close(sendC)
|
||||
responseBody := make([]byte, len(responseBuf))
|
||||
log.Debugf("Waiting for %d bytes", len(responseBuf))
|
||||
l.Debugf("Waiting for %d bytes", len(responseBuf))
|
||||
n, err := io.ReadFull(stream, responseBody)
|
||||
if err != nil {
|
||||
t.Fatalf("error from (*MuxedStream).Read with %d bytes read: %s", n, err)
|
||||
@@ -676,6 +679,7 @@ func AssertIfPipeReadable(t *testing.T, pipe io.ReadCloser) {
|
||||
}
|
||||
|
||||
func TestMultipleStreamsWithDictionaries(t *testing.T) {
|
||||
l := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
for q := CompressionNone; q <= CompressionMax; q++ {
|
||||
htmlBody := `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"` +
|
||||
@@ -812,7 +816,7 @@ func TestMultipleStreamsWithDictionaries(t *testing.T) {
|
||||
testFail := false
|
||||
for err := range errorsC {
|
||||
testFail = true
|
||||
log.Error(err)
|
||||
l.Errorf("%s", err)
|
||||
}
|
||||
if testFail {
|
||||
t.Fatalf("TestMultipleStreams failed")
|
||||
@@ -826,6 +830,8 @@ func TestMultipleStreamsWithDictionaries(t *testing.T) {
|
||||
}
|
||||
|
||||
func sampleSiteHandler(files map[string][]byte) MuxedStreamFunc {
|
||||
l := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
return func(stream *MuxedStream) error {
|
||||
var contentType string
|
||||
var pathHeader Header
|
||||
@@ -853,13 +859,13 @@ func sampleSiteHandler(files map[string][]byte) MuxedStreamFunc {
|
||||
stream.WriteHeaders([]Header{
|
||||
Header{Name: "content-type", Value: contentType},
|
||||
})
|
||||
log.Debugf("Wrote headers for stream %s", pathHeader.Value)
|
||||
l.Debugf("Wrote headers for stream %s", pathHeader.Value)
|
||||
file, ok := files[pathHeader.Value]
|
||||
if !ok {
|
||||
return fmt.Errorf("%s content is not preloaded", pathHeader.Value)
|
||||
}
|
||||
stream.Write(file)
|
||||
log.Debugf("Wrote body for stream %s", pathHeader.Value)
|
||||
l.Debugf("Wrote body for stream %s", pathHeader.Value)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,11 @@ const (
|
||||
ResponseMetaHeaderField = "cf-cloudflared-response-meta"
|
||||
ResponseSourceCloudflared = "cloudflared"
|
||||
ResponseSourceOrigin = "origin"
|
||||
|
||||
CFAccessTokenHeader = "cf-access-token"
|
||||
CFJumpDestinationHeader = "CF-Access-Jump-Destination"
|
||||
CFAccessClientIDHeader = "CF-Access-Client-Id"
|
||||
CFAccessClientSecretHeader = "CF-Access-Client-Secret"
|
||||
)
|
||||
|
||||
// H2RequestHeadersToH1Request converts the HTTP/2 headers coming from origintunneld
|
||||
|
||||
+10
-15
@@ -4,10 +4,9 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/golang-collections/collections/queue"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// data points used to compute average receive window and send window size
|
||||
@@ -22,7 +21,7 @@ type muxMetricsUpdater interface {
|
||||
// metrics returns the latest metrics
|
||||
metrics() *MuxerMetrics
|
||||
// run is a blocking call to start the event loop
|
||||
run(logger *log.Entry) error
|
||||
run(logger logger.Service) error
|
||||
// updateRTTChan is called by muxReader to report new RTT measurements
|
||||
updateRTT(rtt *roundTripMeasurement)
|
||||
//updateReceiveWindowChan is called by muxReader and muxWriter when receiveWindow size is updated
|
||||
@@ -139,34 +138,30 @@ func (updater *muxMetricsUpdaterImpl) metrics() *MuxerMetrics {
|
||||
return m
|
||||
}
|
||||
|
||||
func (updater *muxMetricsUpdaterImpl) run(parentLogger *log.Entry) error {
|
||||
logger := parentLogger.WithFields(log.Fields{
|
||||
"subsystem": "mux",
|
||||
"dir": "metrics",
|
||||
})
|
||||
defer logger.Debug("event loop finished")
|
||||
func (updater *muxMetricsUpdaterImpl) run(logger logger.Service) error {
|
||||
defer logger.Debug("mux - metrics: event loop finished")
|
||||
for {
|
||||
select {
|
||||
case <-updater.abortChan:
|
||||
logger.Infof("Stopping mux metrics updater")
|
||||
logger.Infof("mux - metrics: Stopping mux metrics updater")
|
||||
return nil
|
||||
case roundTripMeasurement := <-updater.updateRTTChan:
|
||||
go updater.rttData.update(roundTripMeasurement)
|
||||
logger.Debug("Update rtt")
|
||||
logger.Debug("mux - metrics: Update rtt")
|
||||
case receiveWindow := <-updater.updateReceiveWindowChan:
|
||||
go updater.receiveWindowData.update(receiveWindow)
|
||||
logger.Debug("Update receive window")
|
||||
logger.Debug("mux - metrics: Update receive window")
|
||||
case sendWindow := <-updater.updateSendWindowChan:
|
||||
go updater.sendWindowData.update(sendWindow)
|
||||
logger.Debug("Update send window")
|
||||
logger.Debug("mux - metrics: Update send window")
|
||||
case inBoundBytes := <-updater.updateInBoundBytesChan:
|
||||
// inBoundBytes is bytes/sec because the update interval is 1 sec
|
||||
go updater.inBoundRate.update(inBoundBytes)
|
||||
logger.Debugf("Inbound bytes %d", inBoundBytes)
|
||||
logger.Debugf("mux - metrics: Inbound bytes %d", inBoundBytes)
|
||||
case outBoundBytes := <-updater.updateOutBoundBytesChan:
|
||||
// outBoundBytes is bytes/sec because the update interval is 1 sec
|
||||
go updater.outBoundRate.update(outBoundBytes)
|
||||
logger.Debugf("Outbound bytes %d", outBoundBytes)
|
||||
logger.Debugf("mux - metrics: Outbound bytes %d", outBoundBytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -91,7 +91,7 @@ func TestMuxMetricsUpdater(t *testing.T) {
|
||||
abortChan := make(chan struct{})
|
||||
compBefore, compAfter := NewAtomicCounter(0), NewAtomicCounter(0)
|
||||
m := newMuxMetricsUpdater(abortChan, compBefore, compAfter)
|
||||
logger := log.NewEntry(log.New())
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
go func() {
|
||||
errChan <- m.run(logger)
|
||||
|
||||
+16
-20
@@ -3,11 +3,12 @@ package h2mux
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
@@ -67,12 +68,8 @@ func (r *MuxReader) Shutdown() <-chan struct{} {
|
||||
return done
|
||||
}
|
||||
|
||||
func (r *MuxReader) run(parentLogger *log.Entry) error {
|
||||
logger := parentLogger.WithFields(log.Fields{
|
||||
"subsystem": "mux",
|
||||
"dir": "read",
|
||||
})
|
||||
defer logger.Debug("event loop finished")
|
||||
func (r *MuxReader) run(logger logger.Service) error {
|
||||
defer logger.Debug("mux - read: event loop finished")
|
||||
|
||||
// routine to periodically update bytesRead
|
||||
go func() {
|
||||
@@ -90,13 +87,13 @@ func (r *MuxReader) run(parentLogger *log.Entry) error {
|
||||
for {
|
||||
frame, err := r.f.ReadFrame()
|
||||
if err != nil {
|
||||
errLogger := logger.WithError(err)
|
||||
errorString := fmt.Sprintf("mux - read: %s", err)
|
||||
if errorDetail := r.f.ErrorDetail(); errorDetail != nil {
|
||||
errLogger = errLogger.WithField("errorDetail", errorDetail)
|
||||
errorString = fmt.Sprintf("%s: errorDetail: %s", errorString, errorDetail)
|
||||
}
|
||||
switch e := err.(type) {
|
||||
case http2.StreamError:
|
||||
errLogger.Warn("stream error")
|
||||
logger.Infof("%s: stream error", errorString)
|
||||
// Ideally we wouldn't return here, since that aborts the muxer.
|
||||
// We should communicate the error to the relevant MuxedStream
|
||||
// data structure, so that callers of MuxedStream.Read() and
|
||||
@@ -104,25 +101,25 @@ func (r *MuxReader) run(parentLogger *log.Entry) error {
|
||||
// and keep the muxer going.
|
||||
return r.streamError(e.StreamID, e.Code)
|
||||
case http2.ConnectionError:
|
||||
errLogger.Warn("connection error")
|
||||
logger.Infof("%s: stream error", errorString)
|
||||
return r.connectionError(err)
|
||||
default:
|
||||
if isConnectionClosedError(err) {
|
||||
if r.streams.Len() == 0 {
|
||||
// don't log the error here -- that would just be extra noise
|
||||
logger.Debug("shutting down")
|
||||
logger.Debug("mux - read: shutting down")
|
||||
return nil
|
||||
}
|
||||
errLogger.Warn("connection closed unexpectedly")
|
||||
logger.Infof("%s: connection closed unexpectedly", errorString)
|
||||
return err
|
||||
} else {
|
||||
errLogger.Warn("frame read error")
|
||||
logger.Infof("%s: frame read error", errorString)
|
||||
return r.connectionError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
r.connActive.Signal()
|
||||
logger.WithField("data", frame).Debug("read frame")
|
||||
logger.Debugf("mux - read: read frame: data %v", frame)
|
||||
switch f := frame.(type) {
|
||||
case *http2.DataFrame:
|
||||
err = r.receiveFrameData(f, logger)
|
||||
@@ -158,7 +155,7 @@ func (r *MuxReader) run(parentLogger *log.Entry) error {
|
||||
err = ErrUnexpectedFrameType
|
||||
}
|
||||
if err != nil {
|
||||
logger.WithField("data", frame).WithError(err).Debug("frame error")
|
||||
logger.Debugf("mux - read: read error: data %v", frame)
|
||||
return r.connectionError(err)
|
||||
}
|
||||
}
|
||||
@@ -279,8 +276,7 @@ func (r *MuxReader) handleStream(stream *MuxedStream) {
|
||||
}
|
||||
|
||||
// Receives a data frame from a stream. A non-nil error is a connection error.
|
||||
func (r *MuxReader) receiveFrameData(frame *http2.DataFrame, parentLogger *log.Entry) error {
|
||||
logger := parentLogger.WithField("stream", frame.Header().StreamID)
|
||||
func (r *MuxReader) receiveFrameData(frame *http2.DataFrame, logger logger.Service) error {
|
||||
stream, err := r.getStreamForFrame(frame)
|
||||
if err != nil {
|
||||
return r.defaultStreamErrorHandler(err, frame.Header())
|
||||
@@ -296,9 +292,9 @@ func (r *MuxReader) receiveFrameData(frame *http2.DataFrame, parentLogger *log.E
|
||||
if frame.Header().Flags.Has(http2.FlagDataEndStream) {
|
||||
if stream.receiveEOF() {
|
||||
r.streams.Delete(stream.streamID)
|
||||
logger.Debug("stream closed")
|
||||
logger.Debugf("mux - read: stream closed: streamID: %d", frame.Header().StreamID)
|
||||
} else {
|
||||
logger.Debug("shutdown receive side")
|
||||
logger.Debugf("mux - read: shutdown receive side: streamID: %d", frame.Header().StreamID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+22
-28
@@ -6,7 +6,7 @@ import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/hpack"
|
||||
)
|
||||
@@ -72,12 +72,8 @@ func tsToPingData(ts int64) [8]byte {
|
||||
return pingData
|
||||
}
|
||||
|
||||
func (w *MuxWriter) run(parentLogger *log.Entry) error {
|
||||
logger := parentLogger.WithFields(log.Fields{
|
||||
"subsystem": "mux",
|
||||
"dir": "write",
|
||||
})
|
||||
defer logger.Debug("event loop finished")
|
||||
func (w *MuxWriter) run(logger logger.Service) error {
|
||||
defer logger.Debug("mux - write: event loop finished")
|
||||
|
||||
// routine to periodically communicate bytesWrote
|
||||
go func() {
|
||||
@@ -95,17 +91,17 @@ func (w *MuxWriter) run(parentLogger *log.Entry) error {
|
||||
for {
|
||||
select {
|
||||
case <-w.abortChan:
|
||||
logger.Debug("aborting writer thread")
|
||||
logger.Debug("mux - write: aborting writer thread")
|
||||
return nil
|
||||
case errCode := <-w.goAwayChan:
|
||||
logger.Debug("sending GOAWAY code ", errCode)
|
||||
logger.Debugf("mux - write: sending GOAWAY code %v", errCode)
|
||||
err := w.f.WriteGoAway(w.streams.LastPeerStreamID(), errCode, []byte{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.idleTimer.MarkActive()
|
||||
case <-w.pingTimestamp.GetUpdateChan():
|
||||
logger.Debug("sending PING ACK")
|
||||
logger.Debug("mux - write: sending PING ACK")
|
||||
err := w.f.WritePing(true, tsToPingData(w.pingTimestamp.Get()))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -115,7 +111,7 @@ func (w *MuxWriter) run(parentLogger *log.Entry) error {
|
||||
if !w.idleTimer.Retry() {
|
||||
return ErrConnectionDropped
|
||||
}
|
||||
logger.Debug("sending PING")
|
||||
logger.Debug("mux - write: sending PING")
|
||||
err := w.f.WritePing(false, tsToPingData(time.Now().UnixNano()))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -125,7 +121,7 @@ func (w *MuxWriter) run(parentLogger *log.Entry) error {
|
||||
w.idleTimer.MarkActive()
|
||||
case <-w.streamErrors.GetSignalChan():
|
||||
for streamID, errCode := range w.streamErrors.GetErrors() {
|
||||
logger.WithField("stream", streamID).WithField("code", errCode).Debug("resetting stream")
|
||||
logger.Debugf("mux - write: resetting stream with code: %v streamID: %d", errCode, streamID)
|
||||
err := w.f.WriteRSTStream(streamID, errCode)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -145,19 +141,17 @@ func (w *MuxWriter) run(parentLogger *log.Entry) error {
|
||||
if streamRequest.body != nil {
|
||||
go streamRequest.flushBody()
|
||||
}
|
||||
streamLogger := logger.WithField("stream", streamID)
|
||||
err := w.writeStreamData(streamRequest.stream, streamLogger)
|
||||
err := w.writeStreamData(streamRequest.stream, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.idleTimer.MarkActive()
|
||||
case streamID := <-w.readyStreamChan:
|
||||
streamLogger := logger.WithField("stream", streamID)
|
||||
stream, ok := w.streams.Get(streamID)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
err := w.writeStreamData(stream, streamLogger)
|
||||
err := w.writeStreamData(stream, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -165,7 +159,7 @@ func (w *MuxWriter) run(parentLogger *log.Entry) error {
|
||||
case useDict := <-w.useDictChan:
|
||||
err := w.writeUseDictionary(useDict)
|
||||
if err != nil {
|
||||
logger.WithError(err).Warn("error writing use dictionary")
|
||||
logger.Errorf("mux - write: error writing use dictionary: %s", err)
|
||||
return err
|
||||
}
|
||||
w.idleTimer.MarkActive()
|
||||
@@ -173,18 +167,18 @@ func (w *MuxWriter) run(parentLogger *log.Entry) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (w *MuxWriter) writeStreamData(stream *MuxedStream, logger *log.Entry) error {
|
||||
logger.Debug("writable")
|
||||
func (w *MuxWriter) writeStreamData(stream *MuxedStream, logger logger.Service) error {
|
||||
logger.Debugf("mux - write: writable: streamID: %d", stream.streamID)
|
||||
chunk := stream.getChunk()
|
||||
w.metricsUpdater.updateReceiveWindow(stream.getReceiveWindow())
|
||||
w.metricsUpdater.updateSendWindow(stream.getSendWindow())
|
||||
if chunk.sendHeadersFrame() {
|
||||
err := w.writeHeaders(chunk.streamID, chunk.headers)
|
||||
if err != nil {
|
||||
logger.WithError(err).Warn("error writing headers")
|
||||
logger.Errorf("mux - write: error writing headers: %s: streamID: %d", err, stream.streamID)
|
||||
return err
|
||||
}
|
||||
logger.Debug("output headers")
|
||||
logger.Debugf("mux - write: output headers: streamID: %d", stream.streamID)
|
||||
}
|
||||
|
||||
if chunk.sendWindowUpdateFrame() {
|
||||
@@ -195,22 +189,22 @@ func (w *MuxWriter) writeStreamData(stream *MuxedStream, logger *log.Entry) erro
|
||||
// window, unless the receiver treats this as a connection error"
|
||||
err := w.f.WriteWindowUpdate(chunk.streamID, chunk.windowUpdate)
|
||||
if err != nil {
|
||||
logger.WithError(err).Warn("error writing window update")
|
||||
logger.Errorf("mux - write: error writing window update: %s: streamID: %d", err, stream.streamID)
|
||||
return err
|
||||
}
|
||||
logger.Debugf("increment receive window by %d", chunk.windowUpdate)
|
||||
logger.Debugf("mux - write: increment receive window by %d streamID: %d", chunk.windowUpdate, stream.streamID)
|
||||
}
|
||||
|
||||
for chunk.sendDataFrame() {
|
||||
payload, sentEOF := chunk.nextDataFrame(int(w.maxFrameSize))
|
||||
err := w.f.WriteData(chunk.streamID, sentEOF, payload)
|
||||
if err != nil {
|
||||
logger.WithError(err).Warn("error writing data")
|
||||
logger.Errorf("mux - write: error writing data: %s: streamID: %d", err, stream.streamID)
|
||||
return err
|
||||
}
|
||||
// update the amount of data wrote
|
||||
w.bytesWrote.IncrementBy(uint64(len(payload)))
|
||||
logger.WithField("len", len(payload)).Debug("output data")
|
||||
logger.Debugf("mux - write: output data: %d: streamID: %d", len(payload), stream.streamID)
|
||||
|
||||
if sentEOF {
|
||||
if stream.readBuffer.Closed() {
|
||||
@@ -218,15 +212,15 @@ func (w *MuxWriter) writeStreamData(stream *MuxedStream, logger *log.Entry) erro
|
||||
if !stream.gotReceiveEOF() {
|
||||
// the peer may send data that we no longer want to receive. Force them into the
|
||||
// closed state.
|
||||
logger.Debug("resetting stream")
|
||||
logger.Debugf("mux - write: resetting stream: streamID: %d", stream.streamID)
|
||||
w.f.WriteRSTStream(chunk.streamID, http2.ErrCodeNo)
|
||||
} else {
|
||||
// Half-open stream transitioned into closed
|
||||
logger.Debug("closing stream")
|
||||
logger.Debugf("mux - write: closing stream: streamID: %d", stream.streamID)
|
||||
}
|
||||
w.streams.Delete(chunk.streamID)
|
||||
} else {
|
||||
logger.Debug("closing stream write side")
|
||||
logger.Debugf("mux - write: closing stream write side: streamID: %d", stream.streamID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -12,8 +12,8 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
)
|
||||
@@ -91,7 +91,7 @@ const indexTemplate = `
|
||||
</html>
|
||||
`
|
||||
|
||||
func StartHelloWorldServer(logger *logrus.Logger, listener net.Listener, shutdownC <-chan struct{}) error {
|
||||
func StartHelloWorldServer(logger logger.Service, listener net.Listener, shutdownC <-chan struct{}) error {
|
||||
logger.Infof("Starting Hello World server at %s", listener.Addr())
|
||||
serverName := defaultServerName
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
@@ -148,7 +148,7 @@ func uptimeHandler(startTime time.Time) http.HandlerFunc {
|
||||
}
|
||||
|
||||
// This handler will echo message
|
||||
func websocketHandler(logger *logrus.Logger, upgrader websocket.Upgrader) http.HandlerFunc {
|
||||
func websocketHandler(logger logger.Service, upgrader websocket.Upgrader) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
@@ -158,12 +158,12 @@ func websocketHandler(logger *logrus.Logger, upgrader websocket.Upgrader) http.H
|
||||
for {
|
||||
mt, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("websocket read message error")
|
||||
logger.Errorf("websocket read message error: %s", err)
|
||||
break
|
||||
}
|
||||
|
||||
if err := conn.WriteMessage(mt, message); err != nil {
|
||||
logger.WithError(err).Error("websocket write message error")
|
||||
logger.Errorf("websocket write message error: %s", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
-85
@@ -1,85 +0,0 @@
|
||||
// this forks the logrus json formatter to rename msg -> message as that's the
|
||||
// expected field. Ideally the logger should make it easier for us.
|
||||
package log
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/mattn/go-colorable"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultTimestampFormat = time.RFC3339Nano
|
||||
)
|
||||
|
||||
type JSONFormatter struct {
|
||||
// TimestampFormat sets the format used for marshaling timestamps.
|
||||
TimestampFormat string
|
||||
}
|
||||
|
||||
func CreateLogger() *logrus.Logger {
|
||||
logger := logrus.New()
|
||||
logger.Out = colorable.NewColorableStderr()
|
||||
logger.Formatter = &logrus.TextFormatter{ForceColors: runtime.GOOS == "windows"}
|
||||
return logger
|
||||
}
|
||||
|
||||
func (f *JSONFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||
data := make(logrus.Fields, len(entry.Data)+3)
|
||||
for k, v := range entry.Data {
|
||||
switch v := v.(type) {
|
||||
case error:
|
||||
// Otherwise errors are ignored by `encoding/json`
|
||||
// https://github.com/sirupsen/logrus/issues/137
|
||||
data[k] = v.Error()
|
||||
default:
|
||||
data[k] = v
|
||||
}
|
||||
}
|
||||
prefixFieldClashes(data)
|
||||
|
||||
timestampFormat := f.TimestampFormat
|
||||
if timestampFormat == "" {
|
||||
timestampFormat = DefaultTimestampFormat
|
||||
}
|
||||
|
||||
data["time"] = entry.Time.Format(timestampFormat)
|
||||
data["message"] = entry.Message
|
||||
data["level"] = entry.Level.String()
|
||||
|
||||
serialized, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
|
||||
}
|
||||
return append(serialized, '\n'), nil
|
||||
}
|
||||
|
||||
// This is to not silently overwrite `time`, `msg` and `level` fields when
|
||||
// dumping it. If this code wasn't there doing:
|
||||
//
|
||||
// logrus.WithField("level", 1).Info("hello")
|
||||
//
|
||||
// Would just silently drop the user provided level. Instead with this code
|
||||
// it'll logged as:
|
||||
//
|
||||
// {"level": "info", "fields.level": 1, "msg": "hello", "time": "..."}
|
||||
//
|
||||
// It's not exported because it's still using Data in an opinionated way. It's to
|
||||
// avoid code duplication between the two default formatters.
|
||||
func prefixFieldClashes(data logrus.Fields) {
|
||||
if t, ok := data["time"]; ok {
|
||||
data["fields.time"] = t
|
||||
}
|
||||
|
||||
if m, ok := data["msg"]; ok {
|
||||
data["fields.msg"] = m
|
||||
}
|
||||
|
||||
if l, ok := data["level"]; ok {
|
||||
data["fields.level"] = l
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alecthomas/units"
|
||||
)
|
||||
|
||||
// Option is to encaspulate actions that will be called by Parse and run later to build an Options struct
|
||||
type Option func(*Options) error
|
||||
|
||||
// Options is use to set logging configuration data
|
||||
type Options struct {
|
||||
logFileDirectory string
|
||||
maxFileSize units.Base2Bytes
|
||||
maxFileCount uint
|
||||
terminalOutputDisabled bool
|
||||
supportedFileLevels []Level
|
||||
supportedTerminalLevels []Level
|
||||
}
|
||||
|
||||
// DisableTerminal stops terminal output for the logger
|
||||
func DisableTerminal(disable bool) Option {
|
||||
return func(c *Options) error {
|
||||
c.terminalOutputDisabled = disable
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// File sets a custom file to log events
|
||||
func File(path string, size units.Base2Bytes, count uint) Option {
|
||||
return func(c *Options) error {
|
||||
c.logFileDirectory = path
|
||||
c.maxFileSize = size
|
||||
c.maxFileCount = count
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultFile configures the log options will the defaults
|
||||
func DefaultFile(directoryPath string) Option {
|
||||
return func(c *Options) error {
|
||||
size, err := units.ParseBase2Bytes("1MB")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.logFileDirectory = directoryPath
|
||||
c.maxFileSize = size
|
||||
c.maxFileCount = 5
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// SupportedFileLevels sets the supported logging levels for the log file
|
||||
func SupportedFileLevels(supported []Level) Option {
|
||||
return func(c *Options) error {
|
||||
c.supportedFileLevels = supported
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// SupportedTerminalevels sets the supported logging levels for the terminal output
|
||||
func SupportedTerminalevels(supported []Level) Option {
|
||||
return func(c *Options) error {
|
||||
c.supportedTerminalLevels = supported
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// LogLevelString sets the supported logging levels from a command line flag
|
||||
func LogLevelString(level string) Option {
|
||||
return func(c *Options) error {
|
||||
supported, err := ParseLevelString(level)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.supportedFileLevels = supported
|
||||
c.supportedTerminalLevels = supported
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Parse builds the Options struct so the caller knows what actions should be run
|
||||
func Parse(opts ...Option) (*Options, error) {
|
||||
options := &Options{}
|
||||
for _, opt := range opts {
|
||||
if err := opt(options); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return options, nil
|
||||
}
|
||||
|
||||
// New setups a new logger based on the options.
|
||||
// The default behavior is to write to standard out
|
||||
func New(opts ...Option) (Service, error) {
|
||||
config, err := Parse(opts...)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l := NewOutputWriter(SharedWriteManager)
|
||||
if config.logFileDirectory != "" {
|
||||
l.Add(NewFileRollingWriter(config.logFileDirectory,
|
||||
"cloudflared",
|
||||
int64(config.maxFileSize),
|
||||
config.maxFileCount),
|
||||
NewDefaultFormatter(time.RFC3339Nano), config.supportedFileLevels...)
|
||||
}
|
||||
|
||||
if !config.terminalOutputDisabled {
|
||||
terminalFormatter := NewTerminalFormatter(time.RFC3339)
|
||||
|
||||
if len(config.supportedTerminalLevels) == 0 {
|
||||
l.Add(os.Stderr, terminalFormatter, InfoLevel, ErrorLevel, FatalLevel)
|
||||
} else {
|
||||
l.Add(os.Stderr, terminalFormatter, config.supportedTerminalLevels...)
|
||||
}
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// ParseLevelString returns the expected log levels based on the cmd flag
|
||||
func ParseLevelString(lvl string) ([]Level, error) {
|
||||
switch strings.ToLower(lvl) {
|
||||
case "fatal":
|
||||
return []Level{FatalLevel}, nil
|
||||
case "error":
|
||||
return []Level{FatalLevel, ErrorLevel}, nil
|
||||
case "info", "warn":
|
||||
return []Level{FatalLevel, ErrorLevel, InfoLevel}, nil
|
||||
case "debug":
|
||||
return []Level{FatalLevel, ErrorLevel, InfoLevel, DebugLevel}, nil
|
||||
}
|
||||
return []Level{}, fmt.Errorf("not a valid log level: %q", lvl)
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// FileRollingWriter maintains a set of log files numbered in order
|
||||
// to keep a subset of log data to ensure it doesn't grow pass defined limits
|
||||
type FileRollingWriter struct {
|
||||
baseFileName string
|
||||
directory string
|
||||
maxFileSize int64
|
||||
maxFileCount uint
|
||||
fileHandle *os.File
|
||||
}
|
||||
|
||||
// NewFileRollingWriter creates a new rolling file writer.
|
||||
// directory is the working directory for the files
|
||||
// baseFileName is the log file name. This writer appends .log to the name for the file name
|
||||
// maxFileSize is the size in bytes of how large each file can be. Not a hard limit, general limit based after each write
|
||||
// maxFileCount is the number of rolled files to keep.
|
||||
func NewFileRollingWriter(directory, baseFileName string, maxFileSize int64, maxFileCount uint) *FileRollingWriter {
|
||||
return &FileRollingWriter{
|
||||
directory: directory,
|
||||
baseFileName: baseFileName,
|
||||
maxFileSize: maxFileSize,
|
||||
maxFileCount: maxFileCount,
|
||||
}
|
||||
}
|
||||
|
||||
// Write is an implementation of io.writer the rolls the file once it reaches its max size
|
||||
// It is expected the caller to Write is doing so in a thread safe manner (as WriteManager does).
|
||||
func (w *FileRollingWriter) Write(p []byte) (n int, err error) {
|
||||
logFile, isSingleFile := buildPath(w.directory, w.baseFileName)
|
||||
if w.fileHandle == nil {
|
||||
h, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0664)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
w.fileHandle = h
|
||||
}
|
||||
|
||||
// get size for rolling check
|
||||
info, err := w.fileHandle.Stat()
|
||||
if err != nil {
|
||||
// failed to stat the file. Close the file handle and attempt to open a new handle on the next write
|
||||
w.Close()
|
||||
w.fileHandle = nil
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// write to the file
|
||||
written, err := w.fileHandle.Write(p)
|
||||
|
||||
// check if the file needs to be rolled
|
||||
if err == nil && info.Size()+int64(written) > w.maxFileSize && !isSingleFile {
|
||||
// close the file handle than do the renaming. A new one will be opened on the next write
|
||||
w.Close()
|
||||
w.rename(logFile, 1)
|
||||
}
|
||||
|
||||
return written, err
|
||||
}
|
||||
|
||||
// Close closes the file handle if it is open
|
||||
func (w *FileRollingWriter) Close() {
|
||||
if w.fileHandle != nil {
|
||||
w.fileHandle.Close()
|
||||
w.fileHandle = nil
|
||||
}
|
||||
}
|
||||
|
||||
// rename is how the files are rolled. It works recursively to move the base log file to the rolled ones
|
||||
// e.g. cloudflared.log -> cloudflared-1.log,
|
||||
// but if cloudflared-1.log already exists, it is renamed to cloudflared-2.log,
|
||||
// then the other files move in to their postion
|
||||
func (w *FileRollingWriter) rename(sourcePath string, index uint) {
|
||||
destinationPath, isSingleFile := buildPath(w.directory, fmt.Sprintf("%s-%d", w.baseFileName, index))
|
||||
if isSingleFile {
|
||||
return //don't need to rename anything, it is a single file
|
||||
}
|
||||
|
||||
// rolled to the max amount of files allowed on disk
|
||||
if index >= w.maxFileCount {
|
||||
os.Remove(destinationPath)
|
||||
}
|
||||
|
||||
// if the rolled path already exist, rename it to cloudflared-2.log, then do this one.
|
||||
// recursive call since the oldest one needs to be renamed, before the newer ones can be moved
|
||||
if exists(destinationPath) {
|
||||
w.rename(destinationPath, index+1)
|
||||
}
|
||||
|
||||
os.Rename(sourcePath, destinationPath)
|
||||
}
|
||||
|
||||
// return the path to the log file and if it is a single file or not.
|
||||
// true means a single file. false means a rolled file
|
||||
func buildPath(directory, fileName string) (string, bool) {
|
||||
if !isDirectory(directory) { // not a directory, so try and treat it as a single file for backwards compatibility sake
|
||||
return directory, true
|
||||
}
|
||||
return filepath.Join(directory, fileName+".log"), false
|
||||
}
|
||||
|
||||
func exists(filePath string) bool {
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isDirectory(path string) bool {
|
||||
if path == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
fileInfo, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return fileInfo.IsDir()
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFileWrite(t *testing.T) {
|
||||
fileName := "test_file"
|
||||
fileLog := fileName + ".log"
|
||||
testData := []byte(string("hello Dalton, how are you doing?"))
|
||||
defer func() {
|
||||
os.Remove(fileLog)
|
||||
}()
|
||||
|
||||
w := NewFileRollingWriter("", fileName, 1000, 2)
|
||||
defer w.Close()
|
||||
|
||||
l, err := w.Write(testData)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, l, len(testData), "expected write length and data length to match")
|
||||
|
||||
d, err := ioutil.ReadFile(fileLog)
|
||||
assert.FileExists(t, fileLog, "file doesn't exist at expected path")
|
||||
assert.Equal(t, d, testData, "expected data in file to match test data")
|
||||
}
|
||||
|
||||
func TestRolling(t *testing.T) {
|
||||
dirName := "testdir"
|
||||
err := os.Mkdir(dirName, 0755)
|
||||
assert.NoError(t, err)
|
||||
|
||||
fileName := "test_file"
|
||||
firstFile := filepath.Join(dirName, fileName+".log")
|
||||
secondFile := filepath.Join(dirName, fileName+"-1.log")
|
||||
thirdFile := filepath.Join(dirName, fileName+"-2.log")
|
||||
|
||||
defer func() {
|
||||
os.RemoveAll(dirName)
|
||||
os.Remove(firstFile)
|
||||
os.Remove(secondFile)
|
||||
os.Remove(thirdFile)
|
||||
}()
|
||||
|
||||
w := NewFileRollingWriter(dirName, fileName, 1000, 2)
|
||||
defer w.Close()
|
||||
|
||||
for i := 99; i >= 1; i-- {
|
||||
testData := []byte(fmt.Sprintf("%d bottles of beer on the wall...", i))
|
||||
w.Write(testData)
|
||||
}
|
||||
assert.FileExists(t, firstFile, "first file doesn't exist as expected")
|
||||
assert.FileExists(t, secondFile, "second file doesn't exist as expected")
|
||||
assert.FileExists(t, thirdFile, "third file doesn't exist as expected")
|
||||
assert.False(t, exists(filepath.Join(dirName, fileName+"-3.log")), "limited to two files and there is more")
|
||||
}
|
||||
|
||||
func TestSingleFile(t *testing.T) {
|
||||
fileName := "test_file"
|
||||
testData := []byte(string("hello Dalton, how are you doing?"))
|
||||
defer func() {
|
||||
os.Remove(fileName)
|
||||
}()
|
||||
|
||||
w := NewFileRollingWriter(fileName, fileName, 1000, 2)
|
||||
defer w.Close()
|
||||
|
||||
l, err := w.Write(testData)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, l, len(testData), "expected write length and data length to match")
|
||||
|
||||
d, err := ioutil.ReadFile(fileName)
|
||||
assert.FileExists(t, fileName, "file doesn't exist at expected path")
|
||||
assert.Equal(t, d, testData, "expected data in file to match test data")
|
||||
}
|
||||
|
||||
func TestSingleFileInDirectory(t *testing.T) {
|
||||
dirName := "testdir"
|
||||
err := os.Mkdir(dirName, 0755)
|
||||
assert.NoError(t, err)
|
||||
|
||||
fileName := "test_file"
|
||||
fullPath := filepath.Join(dirName, fileName+".log")
|
||||
testData := []byte(string("hello Dalton, how are you doing?"))
|
||||
defer func() {
|
||||
os.Remove(fullPath)
|
||||
os.RemoveAll(dirName)
|
||||
}()
|
||||
|
||||
w := NewFileRollingWriter(fullPath, fileName, 1000, 2)
|
||||
defer w.Close()
|
||||
|
||||
l, err := w.Write(testData)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, l, len(testData), "expected write length and data length to match")
|
||||
|
||||
d, err := ioutil.ReadFile(fullPath)
|
||||
assert.FileExists(t, fullPath, "file doesn't exist at expected path")
|
||||
assert.Equal(t, d, testData, "expected data in file to match test data")
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/acmacalister/skittles"
|
||||
)
|
||||
|
||||
// Level of logging
|
||||
type Level int
|
||||
|
||||
const (
|
||||
// InfoLevel is for standard log messages
|
||||
InfoLevel Level = iota
|
||||
|
||||
// DebugLevel is for messages that are intended for purposes debugging only
|
||||
DebugLevel
|
||||
|
||||
// ErrorLevel is for error message to indicte something has gone wrong
|
||||
ErrorLevel
|
||||
|
||||
// FatalLevel is for error message that log and kill the program with an os.exit(1)
|
||||
FatalLevel
|
||||
)
|
||||
|
||||
// Formatter is the base interface for formatting logging messages before writing them out
|
||||
type Formatter interface {
|
||||
Timestamp(Level, time.Time) string // format the timestamp string
|
||||
Content(Level, string) string // format content string (color for terminal, etc)
|
||||
}
|
||||
|
||||
// DefaultFormatter writes a simple structure timestamp and the message per log line
|
||||
type DefaultFormatter struct {
|
||||
format string
|
||||
}
|
||||
|
||||
// NewDefaultFormatter creates the standard log formatter
|
||||
// format is the time format to use for timestamp formatting
|
||||
func NewDefaultFormatter(format string) Formatter {
|
||||
return &DefaultFormatter{
|
||||
format: format,
|
||||
}
|
||||
}
|
||||
|
||||
// Timestamp formats a log line timestamp with a brackets around them
|
||||
func (f *DefaultFormatter) Timestamp(l Level, d time.Time) string {
|
||||
if f.format == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("[%s]: ", d.Format(f.format))
|
||||
}
|
||||
|
||||
// Content just writes the log line straight to the sources
|
||||
func (f *DefaultFormatter) Content(l Level, c string) string {
|
||||
return c
|
||||
}
|
||||
|
||||
// TerminalFormatter is setup for colored output
|
||||
type TerminalFormatter struct {
|
||||
format string
|
||||
supportsColor bool
|
||||
}
|
||||
|
||||
// NewTerminalFormatter creates a Terminal formatter for colored output
|
||||
// format is the time format to use for timestamp formatting
|
||||
func NewTerminalFormatter(format string) Formatter {
|
||||
supportsColor := (runtime.GOOS != "windows")
|
||||
return &TerminalFormatter{
|
||||
format: format,
|
||||
supportsColor: supportsColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Timestamp returns the log level with a matching color to the log type
|
||||
func (f *TerminalFormatter) Timestamp(l Level, d time.Time) string {
|
||||
t := ""
|
||||
dateStr := "[" + d.Format(f.format) + "] "
|
||||
switch l {
|
||||
case InfoLevel:
|
||||
t = f.output("INFO", skittles.Cyan)
|
||||
case ErrorLevel:
|
||||
t = f.output("ERROR", skittles.Red)
|
||||
case DebugLevel:
|
||||
t = f.output("DEBUG", skittles.Yellow)
|
||||
case FatalLevel:
|
||||
t = f.output("FATAL", skittles.Red)
|
||||
}
|
||||
return t + dateStr
|
||||
}
|
||||
|
||||
// Content just writes the log line straight to the sources
|
||||
func (f *TerminalFormatter) Content(l Level, c string) string {
|
||||
return c
|
||||
}
|
||||
|
||||
func (f *TerminalFormatter) output(msg string, colorFunc func(interface{}) string) string {
|
||||
if f.supportsColor {
|
||||
return colorFunc(msg)
|
||||
}
|
||||
return msg
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package logger
|
||||
|
||||
import "sync"
|
||||
|
||||
// SharedWriteManager is a package level variable to allows multiple loggers to use the same write manager.
|
||||
// This is useful when multiple loggers will write to the same file to ensure they don't clobber each other.
|
||||
var SharedWriteManager = NewWriteManager()
|
||||
|
||||
type writeData struct {
|
||||
writeFunc func([]byte)
|
||||
data []byte
|
||||
}
|
||||
|
||||
// WriteManager is a logging service that handles managing multiple writing streams
|
||||
type WriteManager struct {
|
||||
shutdown chan struct{}
|
||||
writeChan chan writeData
|
||||
writers map[string]Service
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewWriteManager creates a write manager that implements OutputManager
|
||||
func NewWriteManager() OutputManager {
|
||||
m := &WriteManager{
|
||||
shutdown: make(chan struct{}),
|
||||
writeChan: make(chan writeData, 1000),
|
||||
}
|
||||
|
||||
go m.run()
|
||||
return m
|
||||
}
|
||||
|
||||
// Append adds a message to the writer runloop
|
||||
func (m *WriteManager) Append(data []byte, callback func([]byte)) {
|
||||
m.wg.Add(1)
|
||||
m.writeChan <- writeData{data: data, writeFunc: callback}
|
||||
}
|
||||
|
||||
// Shutdown stops the sync manager service
|
||||
func (m *WriteManager) Shutdown() {
|
||||
m.wg.Wait()
|
||||
close(m.shutdown)
|
||||
close(m.writeChan)
|
||||
}
|
||||
|
||||
// run is the main runloop that schedules log messages
|
||||
func (m *WriteManager) run() {
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-m.writeChan:
|
||||
if ok {
|
||||
event.writeFunc(event.data)
|
||||
m.wg.Done()
|
||||
}
|
||||
case <-m.shutdown:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWriteManger(t *testing.T) {
|
||||
testData := []byte(string("hello Austin, how are you doing?"))
|
||||
waitChan := make(chan []byte)
|
||||
m := NewWriteManager()
|
||||
m.Append(testData, func(b []byte) {
|
||||
waitChan <- b
|
||||
})
|
||||
resp := <-waitChan
|
||||
assert.Equal(t, testData, resp)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package logger
|
||||
|
||||
// MockWriteManager does nothing and is provided for testing purposes
|
||||
type MockWriteManager struct {
|
||||
}
|
||||
|
||||
// NewMockWriteManager creates an OutputManager that does nothing for testing purposes
|
||||
func NewMockWriteManager() OutputManager {
|
||||
return &MockWriteManager{}
|
||||
}
|
||||
|
||||
// Append is a mock stub
|
||||
func (m *MockWriteManager) Append(data []byte, callback func([]byte)) {
|
||||
}
|
||||
|
||||
// Shutdown is a mock stub
|
||||
func (m *MockWriteManager) Shutdown() {
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// provided for testing
|
||||
var osExit = os.Exit
|
||||
|
||||
// OutputManager is used to sync data of Output
|
||||
type OutputManager interface {
|
||||
Append([]byte, func([]byte))
|
||||
Shutdown()
|
||||
}
|
||||
|
||||
// Service is the logging service that is either a group or single log writer
|
||||
type Service interface {
|
||||
Error(message string)
|
||||
Info(message string)
|
||||
Debug(message string)
|
||||
Fatal(message string)
|
||||
|
||||
Errorf(format string, args ...interface{})
|
||||
Infof(format string, args ...interface{})
|
||||
Debugf(format string, args ...interface{})
|
||||
Fatalf(format string, args ...interface{})
|
||||
}
|
||||
|
||||
type sourceGroup struct {
|
||||
writer io.Writer
|
||||
formatter Formatter
|
||||
levelsSupported []Level
|
||||
}
|
||||
|
||||
// OutputWriter is the standard logging implementation
|
||||
type OutputWriter struct {
|
||||
groups []sourceGroup
|
||||
syncWriter OutputManager
|
||||
}
|
||||
|
||||
// NewOutputWriter create a new logger
|
||||
func NewOutputWriter(syncWriter OutputManager) *OutputWriter {
|
||||
return &OutputWriter{
|
||||
syncWriter: syncWriter,
|
||||
groups: make([]sourceGroup, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Add a writer and formatter to output to
|
||||
func (s *OutputWriter) Add(writer io.Writer, formatter Formatter, levels ...Level) {
|
||||
s.groups = append(s.groups, sourceGroup{writer: writer, formatter: formatter, levelsSupported: levels})
|
||||
}
|
||||
|
||||
// Error writes an error to the logging sources
|
||||
func (s *OutputWriter) Error(message string) {
|
||||
s.output(ErrorLevel, message)
|
||||
}
|
||||
|
||||
// Info writes an info string to the logging sources
|
||||
func (s *OutputWriter) Info(message string) {
|
||||
s.output(InfoLevel, message)
|
||||
}
|
||||
|
||||
// Debug writes a debug string to the logging sources
|
||||
func (s *OutputWriter) Debug(message string) {
|
||||
s.output(DebugLevel, message)
|
||||
}
|
||||
|
||||
// Fatal writes a error string to the logging sources and runs does an os.exit()
|
||||
func (s *OutputWriter) Fatal(message string) {
|
||||
s.output(FatalLevel, message)
|
||||
s.syncWriter.Shutdown() // waits for the pending logging to finish
|
||||
osExit(1)
|
||||
}
|
||||
|
||||
// Errorf writes a formatted error to the logging sources
|
||||
func (s *OutputWriter) Errorf(format string, args ...interface{}) {
|
||||
s.output(ErrorLevel, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Infof writes a formatted info statement to the logging sources
|
||||
func (s *OutputWriter) Infof(format string, args ...interface{}) {
|
||||
s.output(InfoLevel, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Debugf writes a formatted debug statement to the logging sources
|
||||
func (s *OutputWriter) Debugf(format string, args ...interface{}) {
|
||||
s.output(DebugLevel, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Fatalf writes a writes a formatted error statement and runs does an os.exit()
|
||||
func (s *OutputWriter) Fatalf(format string, args ...interface{}) {
|
||||
s.output(FatalLevel, fmt.Sprintf(format, args...))
|
||||
s.syncWriter.Shutdown() // waits for the pending logging to finish
|
||||
osExit(1)
|
||||
}
|
||||
|
||||
// output does the actual write to the sync manager
|
||||
func (s *OutputWriter) output(l Level, content string) {
|
||||
for _, group := range s.groups {
|
||||
if isSupported(group, l) {
|
||||
logLine := fmt.Sprintf("%s%s\n", group.formatter.Timestamp(l, time.Now()),
|
||||
group.formatter.Content(l, content))
|
||||
s.append(group, []byte(logLine))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *OutputWriter) append(group sourceGroup, logLine []byte) {
|
||||
s.syncWriter.Append(logLine, func(b []byte) {
|
||||
group.writer.Write(b)
|
||||
})
|
||||
}
|
||||
|
||||
// isSupported checks if the log level is supported
|
||||
func isSupported(group sourceGroup, l Level) bool {
|
||||
for _, level := range group.levelsSupported {
|
||||
if l == level {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Write implements io.Writer to support SetOutput of the log package
|
||||
func (s *OutputWriter) Write(p []byte) (n int, err error) {
|
||||
s.Info(string(p))
|
||||
return len(p), nil
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLogLevel(t *testing.T) {
|
||||
timeFormat := "2006-01-02"
|
||||
f := NewDefaultFormatter(timeFormat)
|
||||
m := NewWriteManager()
|
||||
|
||||
var testBuffer bytes.Buffer
|
||||
logger := NewOutputWriter(m)
|
||||
logger.Add(&testBuffer, f, InfoLevel, DebugLevel)
|
||||
|
||||
testTime := f.Timestamp(InfoLevel, time.Now())
|
||||
|
||||
testInfo := "hello Dalton, how are you doing?"
|
||||
logger.Info(testInfo)
|
||||
|
||||
tesErr := "hello Austin, how did it break today?"
|
||||
logger.Error(tesErr)
|
||||
|
||||
testDebug := "hello Bill, who are you?"
|
||||
logger.Debug(testDebug)
|
||||
|
||||
m.Shutdown()
|
||||
|
||||
lines := strings.Split(testBuffer.String(), "\n")
|
||||
assert.Len(t, lines, 3, "only expected two strings in the buffer")
|
||||
|
||||
infoLine := lines[0]
|
||||
debugLine := lines[1]
|
||||
|
||||
compareInfo := fmt.Sprintf("%s%s", testTime, testInfo)
|
||||
assert.Equal(t, compareInfo, infoLine, "expect the strings to match")
|
||||
|
||||
compareDebug := fmt.Sprintf("%s%s", testTime, testDebug)
|
||||
assert.Equal(t, compareDebug, debugLine, "expect the strings to match")
|
||||
}
|
||||
|
||||
func TestOutputWrite(t *testing.T) {
|
||||
timeFormat := "2006-01-02"
|
||||
f := NewDefaultFormatter(timeFormat)
|
||||
m := NewWriteManager()
|
||||
|
||||
var testBuffer bytes.Buffer
|
||||
logger := NewOutputWriter(m)
|
||||
logger.Add(&testBuffer, f, InfoLevel)
|
||||
|
||||
testData := "hello Bob Bork, how are you doing?"
|
||||
logger.Info(testData)
|
||||
testTime := f.Timestamp(InfoLevel, time.Now())
|
||||
|
||||
m.Shutdown()
|
||||
|
||||
scanner := bufio.NewScanner(&testBuffer)
|
||||
scanner.Scan()
|
||||
line := scanner.Text()
|
||||
assert.NoError(t, scanner.Err())
|
||||
|
||||
compareLine := fmt.Sprintf("%s%s", testTime, testData)
|
||||
assert.Equal(t, compareLine, line, "expect the strings to match")
|
||||
}
|
||||
|
||||
func TestFatalWrite(t *testing.T) {
|
||||
timeFormat := "2006-01-02"
|
||||
f := NewDefaultFormatter(timeFormat)
|
||||
m := NewWriteManager()
|
||||
|
||||
var testBuffer bytes.Buffer
|
||||
logger := NewOutputWriter(m)
|
||||
logger.Add(&testBuffer, f, FatalLevel)
|
||||
|
||||
oldOsExit := osExit
|
||||
defer func() { osExit = oldOsExit }()
|
||||
|
||||
var got int
|
||||
myExit := func(code int) {
|
||||
got = code
|
||||
}
|
||||
|
||||
osExit = myExit
|
||||
|
||||
testData := "so long y'all"
|
||||
logger.Fatal(testData)
|
||||
testTime := f.Timestamp(FatalLevel, time.Now())
|
||||
|
||||
scanner := bufio.NewScanner(&testBuffer)
|
||||
scanner.Scan()
|
||||
line := scanner.Text()
|
||||
assert.NoError(t, scanner.Err())
|
||||
|
||||
compareLine := fmt.Sprintf("%s%s", testTime, testData)
|
||||
assert.Equal(t, compareLine, line, "expect the strings to match")
|
||||
assert.Equal(t, got, 1, "exit code should be one for a fatal log")
|
||||
}
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
TARGET_DIRECTORY=".build"
|
||||
BINARY_NAME="cloudflared"
|
||||
VERSION=$(git describe --tags --always --dirty="-dev")
|
||||
PRODUCT="cloudflared"
|
||||
|
||||
|
||||
echo "building cloudflared"
|
||||
make cloudflared
|
||||
|
||||
echo "creating build directory"
|
||||
mkdir ${TARGET_DIRECTORY}
|
||||
mkdir ${TARGET_DIRECTORY}/contents
|
||||
cp -r .mac_resources/scripts ${TARGET_DIRECTORY}/scripts
|
||||
|
||||
echo "move cloudflared into the build directory"
|
||||
mv $BINARY_NAME {$TARGET_DIRECTORY}/contents/${PRODUCT}
|
||||
|
||||
echo "build the installer package"
|
||||
pkgbuild --identifier com.cloudflare.${PRODUCT} \
|
||||
--version ${VERSION} \
|
||||
--scripts ${TARGET_DIRECTORY}/scripts \
|
||||
--root ${TARGET_DIRECTORY}/contents \
|
||||
--install-location /usr/local/bin \
|
||||
${PRODUCT}.pkg
|
||||
# TODO: our iOS/Mac account doesn't have this installer certificate type.
|
||||
# need to find how we can get it --sign "Developer ID Installer: Cloudflare" \
|
||||
|
||||
echo "cleaning up the build directory"
|
||||
rm -rf $TARGET_DIRECTORY
|
||||
+4
-4
@@ -12,9 +12,9 @@ import (
|
||||
|
||||
"golang.org/x/net/trace"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -22,7 +22,7 @@ const (
|
||||
startupTime = time.Millisecond * 500
|
||||
)
|
||||
|
||||
func ServeMetrics(l net.Listener, shutdownC <-chan struct{}, logger *logrus.Logger) (err error) {
|
||||
func ServeMetrics(l net.Listener, shutdownC <-chan struct{}, 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 }
|
||||
@@ -43,7 +43,7 @@ func ServeMetrics(l net.Listener, shutdownC <-chan struct{}, logger *logrus.Logg
|
||||
defer wg.Done()
|
||||
err = server.Serve(l)
|
||||
}()
|
||||
logger.WithField("addr", fmt.Sprintf("%v/metrics", l.Addr())).Info("Starting metrics server")
|
||||
logger.Infof("Starting metrics server on %s", fmt.Sprintf("%v/metrics", l.Addr()))
|
||||
// server.Serve will hang if server.Shutdown is called before the server is
|
||||
// fully started up. So add artificial delay.
|
||||
time.Sleep(startupTime)
|
||||
@@ -58,7 +58,7 @@ func ServeMetrics(l net.Listener, shutdownC <-chan struct{}, logger *logrus.Logg
|
||||
logger.Info("Metrics server stopped")
|
||||
return nil
|
||||
}
|
||||
logger.WithError(err).Error("Metrics server quit with error")
|
||||
logger.Errorf("Metrics server quit with error: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
+23
-23
@@ -9,12 +9,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/cloudflare/cloudflared/buffer"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/signal"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
@@ -56,7 +56,7 @@ type Supervisor struct {
|
||||
nextConnectedIndex int
|
||||
nextConnectedSignal chan struct{}
|
||||
|
||||
logger *logrus.Entry
|
||||
logger logger.Service
|
||||
|
||||
jwtLock sync.RWMutex
|
||||
jwt []byte
|
||||
@@ -80,7 +80,7 @@ type tunnelError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func NewSupervisor(config *TunnelConfig, u uuid.UUID) (*Supervisor, error) {
|
||||
func NewSupervisor(config *TunnelConfig, cloudflaredUUID uuid.UUID) (*Supervisor, error) {
|
||||
var (
|
||||
edgeIPs *edgediscovery.Edge
|
||||
err error
|
||||
@@ -93,13 +93,14 @@ func NewSupervisor(config *TunnelConfig, u uuid.UUID) (*Supervisor, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Supervisor{
|
||||
cloudflaredUUID: u,
|
||||
cloudflaredUUID: cloudflaredUUID,
|
||||
config: config,
|
||||
edgeIPs: edgeIPs,
|
||||
tunnelErrors: make(chan tunnelError),
|
||||
tunnelsConnecting: map[int]chan struct{}{},
|
||||
logger: config.Logger.WithField("subsystem", "supervisor"),
|
||||
logger: config.Logger,
|
||||
connDigest: make(map[uint8][]byte),
|
||||
bufferPool: buffer.NewPool(512 * 1024),
|
||||
}, nil
|
||||
@@ -123,7 +124,7 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal, re
|
||||
if timer, err := s.refreshAuth(ctx, refreshAuthBackoff, s.authenticate); err == nil {
|
||||
refreshAuthBackoffTimer = timer
|
||||
} else {
|
||||
logger.WithError(err).Errorf("initial refreshAuth failed, retrying in %v", refreshAuthRetryDuration)
|
||||
logger.Errorf("supervisor: initial refreshAuth failed, retrying in %v: %s", refreshAuthRetryDuration, err)
|
||||
refreshAuthBackoffTimer = time.After(refreshAuthRetryDuration)
|
||||
}
|
||||
}
|
||||
@@ -142,7 +143,7 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal, re
|
||||
case tunnelError := <-s.tunnelErrors:
|
||||
tunnelsActive--
|
||||
if tunnelError.err != nil {
|
||||
logger.WithError(tunnelError.err).Warn("Tunnel disconnected due to error")
|
||||
logger.Infof("supervisor: Tunnel disconnected due to error: %s", tunnelError.err)
|
||||
tunnelsWaiting = append(tunnelsWaiting, tunnelError.index)
|
||||
s.waitForNextTunnel(tunnelError.index)
|
||||
|
||||
@@ -165,7 +166,7 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal, re
|
||||
case <-refreshAuthBackoffTimer:
|
||||
newTimer, err := s.refreshAuth(ctx, refreshAuthBackoff, s.authenticate)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Authentication failed")
|
||||
logger.Errorf("supervisor: Authentication failed: %s", err)
|
||||
// Permanent failure. Leave the `select` without setting the
|
||||
// channel to be non-null, so we'll never hit this case of the `select` again.
|
||||
continue
|
||||
@@ -182,9 +183,9 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal, re
|
||||
s.lastResolve = time.Now()
|
||||
s.resolverC = nil
|
||||
if result.err == nil {
|
||||
logger.Debug("Service discovery refresh complete")
|
||||
logger.Debug("supervisor: Service discovery refresh complete")
|
||||
} else {
|
||||
logger.WithError(result.err).Error("Service discovery error")
|
||||
logger.Errorf("supervisor: Service discovery error: %s", result.err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,7 +198,7 @@ func (s *Supervisor) initialize(ctx context.Context, connectedSignal *signal.Sig
|
||||
s.lastResolve = time.Now()
|
||||
availableAddrs := int(s.edgeIPs.AvailableAddrs())
|
||||
if s.config.HAConnections > availableAddrs {
|
||||
logger.Warnf("You requested %d HA connections but I can give you at most %d.", s.config.HAConnections, availableAddrs)
|
||||
logger.Infof("You requested %d HA connections but I can give you at most %d.", s.config.HAConnections, availableAddrs)
|
||||
s.config.HAConnections = availableAddrs
|
||||
}
|
||||
|
||||
@@ -226,17 +227,17 @@ func (s *Supervisor) startFirstTunnel(ctx context.Context, connectedSignal *sign
|
||||
addr *net.TCPAddr
|
||||
err error
|
||||
)
|
||||
const thisConnID = 0
|
||||
const firstConnIndex = 0
|
||||
defer func() {
|
||||
s.tunnelErrors <- tunnelError{index: thisConnID, addr: addr, err: err}
|
||||
s.tunnelErrors <- tunnelError{index: firstConnIndex, addr: addr, err: err}
|
||||
}()
|
||||
|
||||
addr, err = s.edgeIPs.GetAddr(thisConnID)
|
||||
addr, err = s.edgeIPs.GetAddr(firstConnIndex)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = ServeTunnelLoop(ctx, s, s.config, addr, thisConnID, connectedSignal, s.cloudflaredUUID, s.bufferPool, reconnectCh)
|
||||
err = ServeTunnelLoop(ctx, s, s.config, addr, firstConnIndex, connectedSignal, s.cloudflaredUUID, s.bufferPool, reconnectCh)
|
||||
// If the first tunnel disconnects, keep restarting it.
|
||||
edgeErrors := 0
|
||||
for s.unusedIPs() {
|
||||
@@ -254,12 +255,12 @@ func (s *Supervisor) startFirstTunnel(ctx context.Context, connectedSignal *sign
|
||||
return
|
||||
}
|
||||
if edgeErrors >= 2 {
|
||||
addr, err = s.edgeIPs.GetDifferentAddr(thisConnID)
|
||||
addr, err = s.edgeIPs.GetDifferentAddr(firstConnIndex)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
err = ServeTunnelLoop(ctx, s, s.config, addr, thisConnID, connectedSignal, s.cloudflaredUUID, s.bufferPool, reconnectCh)
|
||||
err = ServeTunnelLoop(ctx, s, s.config, addr, firstConnIndex, connectedSignal, s.cloudflaredUUID, s.bufferPool, reconnectCh)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,12 +356,12 @@ func (s *Supervisor) refreshAuth(
|
||||
backoff *BackoffHandler,
|
||||
authenticate func(ctx context.Context, numPreviousAttempts int) (tunnelpogs.AuthOutcome, error),
|
||||
) (retryTimer <-chan time.Time, err error) {
|
||||
logger := s.config.Logger.WithField("subsystem", subsystemRefreshAuth)
|
||||
logger := s.config.Logger
|
||||
authOutcome, err := authenticate(ctx, backoff.Retries())
|
||||
if err != nil {
|
||||
s.config.Metrics.authFail.WithLabelValues(err.Error()).Inc()
|
||||
if duration, ok := backoff.GetBackoffDuration(ctx); ok {
|
||||
logger.WithError(err).Warnf("Retrying in %v", duration)
|
||||
logger.Debugf("refresh_auth: Retrying in %v: %s", duration, err)
|
||||
return backoff.BackoffTimer(), nil
|
||||
}
|
||||
return nil, err
|
||||
@@ -376,13 +377,13 @@ func (s *Supervisor) refreshAuth(
|
||||
case tunnelpogs.AuthUnknown:
|
||||
duration := outcome.RefreshAfter()
|
||||
s.config.Metrics.authFail.WithLabelValues(outcome.Error()).Inc()
|
||||
logger.WithError(outcome).Warnf("Retrying in %v", duration)
|
||||
logger.Debugf("refresh_auth: Retrying in %v: %s", duration, outcome)
|
||||
return timeAfter(duration), nil
|
||||
case tunnelpogs.AuthFail:
|
||||
s.config.Metrics.authFail.WithLabelValues(outcome.Error()).Inc()
|
||||
return nil, outcome
|
||||
default:
|
||||
err := fmt.Errorf("Unexpected outcome type %T", authOutcome)
|
||||
err := fmt.Errorf("refresh_auth: Unexpected outcome type %T", authOutcome)
|
||||
s.config.Metrics.authFail.WithLabelValues(err.Error()).Inc()
|
||||
return nil, err
|
||||
}
|
||||
@@ -405,7 +406,6 @@ func (s *Supervisor) authenticate(ctx context.Context, numPreviousAttempts int)
|
||||
return nil // noop
|
||||
})
|
||||
muxerConfig := s.config.muxerConfig(handler)
|
||||
muxerConfig.Logger = muxerConfig.Logger.WithField("subsystem", subsystemRefreshAuth)
|
||||
muxer, err := h2mux.Handshake(edgeConn, edgeConn, muxerConfig, s.config.Metrics.activeStreams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -417,7 +417,7 @@ func (s *Supervisor) authenticate(ctx context.Context, numPreviousAttempts int)
|
||||
<-muxer.Shutdown()
|
||||
}()
|
||||
|
||||
tunnelServer, err := connection.NewRPCClient(ctx, muxer, s.logger.WithField("subsystem", subsystemRefreshAuth), openStreamTimeout)
|
||||
tunnelServer, err := connection.NewRPCClient(ctx, muxer, s.logger, openStreamTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -9,13 +9,13 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
func testConfig(logger *logrus.Logger) *TunnelConfig {
|
||||
func testConfig(logger logger.Service) *TunnelConfig {
|
||||
metrics := TunnelMetrics{}
|
||||
|
||||
metrics.authSuccess = prometheus.NewCounter(
|
||||
@@ -40,8 +40,7 @@ func testConfig(logger *logrus.Logger) *TunnelConfig {
|
||||
}
|
||||
|
||||
func TestRefreshAuthBackoff(t *testing.T) {
|
||||
logger := logrus.New()
|
||||
logger.Level = logrus.ErrorLevel
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
var wait time.Duration
|
||||
timeAfter = func(d time.Duration) <-chan time.Time {
|
||||
@@ -85,8 +84,7 @@ func TestRefreshAuthBackoff(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRefreshAuthSuccess(t *testing.T) {
|
||||
logger := logrus.New()
|
||||
logger.Level = logrus.ErrorLevel
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
var wait time.Duration
|
||||
timeAfter = func(d time.Duration) <-chan time.Time {
|
||||
@@ -114,8 +112,7 @@ func TestRefreshAuthSuccess(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRefreshAuthUnknown(t *testing.T) {
|
||||
logger := logrus.New()
|
||||
logger.Level = logrus.ErrorLevel
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
var wait time.Duration
|
||||
timeAfter = func(d time.Duration) <-chan time.Time {
|
||||
@@ -143,8 +140,7 @@ func TestRefreshAuthUnknown(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRefreshAuthFail(t *testing.T) {
|
||||
logger := logrus.New()
|
||||
logger.Level = logrus.ErrorLevel
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
s, err := NewSupervisor(testConfig(logger), uuid.New())
|
||||
if !assert.NoError(t, err) {
|
||||
|
||||
+204
-114
@@ -17,16 +17,16 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/cloudflare/cloudflared/buffer"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/signal"
|
||||
"github.com/cloudflare/cloudflared/streamhandler"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
@@ -48,47 +48,46 @@ type registerRPCName string
|
||||
const (
|
||||
register registerRPCName = "register"
|
||||
reconnect registerRPCName = "reconnect"
|
||||
unknown registerRPCName = "unknown"
|
||||
)
|
||||
|
||||
type TunnelConfig struct {
|
||||
BuildInfo *buildinfo.BuildInfo
|
||||
ClientID string
|
||||
ClientTlsConfig *tls.Config
|
||||
CloseConnOnce *sync.Once // Used to close connectedSignal no more than once
|
||||
CompressionQuality uint64
|
||||
EdgeAddrs []string
|
||||
GracePeriod time.Duration
|
||||
HAConnections int
|
||||
HTTPTransport http.RoundTripper
|
||||
HeartbeatInterval time.Duration
|
||||
Hostname string
|
||||
HTTPHostHeader string
|
||||
IncidentLookup IncidentLookup
|
||||
IsAutoupdated bool
|
||||
IsFreeTunnel bool
|
||||
LBPool string
|
||||
Logger *log.Logger
|
||||
MaxHeartbeats uint64
|
||||
Metrics *TunnelMetrics
|
||||
MetricsUpdateFreq time.Duration
|
||||
NoChunkedEncoding bool
|
||||
OriginCert []byte
|
||||
ReportedVersion string
|
||||
Retries uint
|
||||
RunFromTerminal bool
|
||||
Tags []tunnelpogs.Tag
|
||||
TlsConfig *tls.Config
|
||||
TransportLogger *log.Logger
|
||||
UseDeclarativeTunnel bool
|
||||
WSGI bool
|
||||
BuildInfo *buildinfo.BuildInfo
|
||||
ClientID string
|
||||
ClientTlsConfig *tls.Config
|
||||
CloseConnOnce *sync.Once // Used to close connectedSignal no more than once
|
||||
CompressionQuality uint64
|
||||
EdgeAddrs []string
|
||||
GracePeriod time.Duration
|
||||
HAConnections int
|
||||
HTTPTransport http.RoundTripper
|
||||
HeartbeatInterval time.Duration
|
||||
Hostname string
|
||||
HTTPHostHeader string
|
||||
IncidentLookup IncidentLookup
|
||||
IsAutoupdated bool
|
||||
IsFreeTunnel bool
|
||||
LBPool string
|
||||
Logger logger.Service
|
||||
TransportLogger logger.Service
|
||||
MaxHeartbeats uint64
|
||||
Metrics *TunnelMetrics
|
||||
MetricsUpdateFreq time.Duration
|
||||
NoChunkedEncoding bool
|
||||
OriginCert []byte
|
||||
ReportedVersion string
|
||||
Retries uint
|
||||
RunFromTerminal bool
|
||||
Tags []tunnelpogs.Tag
|
||||
TlsConfig *tls.Config
|
||||
WSGI bool
|
||||
// OriginUrl may not be used if a user specifies a unix socket.
|
||||
OriginUrl string
|
||||
|
||||
// feature-flag to use new edge reconnect tokens
|
||||
UseReconnectToken bool
|
||||
// feature-flag for using ConnectionDigest
|
||||
UseQuickReconnects bool
|
||||
|
||||
NamedTunnel *NamedTunnelConfig
|
||||
ReplaceExisting bool
|
||||
}
|
||||
|
||||
// ReconnectTunnelCredentialManager is invoked by functions in this file to
|
||||
@@ -103,6 +102,8 @@ type ReconnectTunnelCredentialManager interface {
|
||||
|
||||
type dupConnRegisterTunnelError struct{}
|
||||
|
||||
var errDuplicationConnection = &dupConnRegisterTunnelError{}
|
||||
|
||||
func (e dupConnRegisterTunnelError) Error() string {
|
||||
return "already connected to this server"
|
||||
}
|
||||
@@ -144,7 +145,7 @@ func (c *TunnelConfig) muxerConfig(handler h2mux.MuxedStreamHandler) h2mux.Muxer
|
||||
IsClient: true,
|
||||
HeartbeatInterval: c.HeartbeatInterval,
|
||||
MaxHeartbeats: c.MaxHeartbeats,
|
||||
Logger: c.TransportLogger.WithFields(log.Fields{}),
|
||||
Logger: c.TransportLogger,
|
||||
CompressionQuality: h2mux.CompressionSetting(c.CompressionQuality),
|
||||
}
|
||||
}
|
||||
@@ -171,12 +172,31 @@ func (c *TunnelConfig) RegistrationOptions(connectionID uint8, OriginLocalIP str
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TunnelConfig) SupportedFeatures() []string {
|
||||
basic := []string{FeatureSerializedHeaders}
|
||||
if c.UseQuickReconnects {
|
||||
basic = append(basic, FeatureQuickReconnects)
|
||||
func (c *TunnelConfig) ConnectionOptions(originLocalAddr string) *tunnelpogs.ConnectionOptions {
|
||||
// attempt to parse out origin IP, but don't fail since it's informational field
|
||||
host, _, _ := net.SplitHostPort(originLocalAddr)
|
||||
originIP := net.ParseIP(host)
|
||||
|
||||
return &tunnelpogs.ConnectionOptions{
|
||||
Client: c.NamedTunnel.Client,
|
||||
OriginLocalIP: originIP,
|
||||
ReplaceExisting: c.ReplaceExisting,
|
||||
CompressionQuality: uint8(c.CompressionQuality),
|
||||
}
|
||||
return basic
|
||||
}
|
||||
|
||||
func (c *TunnelConfig) SupportedFeatures() []string {
|
||||
features := []string{FeatureSerializedHeaders}
|
||||
if c.NamedTunnel == nil {
|
||||
features = append(features, FeatureQuickReconnects)
|
||||
}
|
||||
return features
|
||||
}
|
||||
|
||||
type NamedTunnelConfig struct {
|
||||
Auth pogs.TunnelAuth
|
||||
ID uuid.UUID
|
||||
Client pogs.ClientInfo
|
||||
}
|
||||
|
||||
func StartTunnelDaemon(ctx context.Context, config *TunnelConfig, connectedSignal *signal.Signal, cloudflaredID uuid.UUID, reconnectCh chan ReconnectSignal) error {
|
||||
@@ -191,13 +211,12 @@ func ServeTunnelLoop(ctx context.Context,
|
||||
credentialManager ReconnectTunnelCredentialManager,
|
||||
config *TunnelConfig,
|
||||
addr *net.TCPAddr,
|
||||
connectionID uint8,
|
||||
connectionIndex uint8,
|
||||
connectedSignal *signal.Signal,
|
||||
u uuid.UUID,
|
||||
cloudflaredUUID uuid.UUID,
|
||||
bufferPool *buffer.Pool,
|
||||
reconnectCh chan ReconnectSignal,
|
||||
) error {
|
||||
connectionLogger := config.Logger.WithField("connectionID", connectionID)
|
||||
config.Metrics.incrementHaConnections()
|
||||
defer config.Metrics.decrementHaConnections()
|
||||
backoff := BackoffHandler{MaxRetries: config.Retries}
|
||||
@@ -214,17 +233,17 @@ func ServeTunnelLoop(ctx context.Context,
|
||||
ctx,
|
||||
credentialManager,
|
||||
config,
|
||||
connectionLogger,
|
||||
addr, connectionID,
|
||||
config.Logger,
|
||||
addr, connectionIndex,
|
||||
connectedFuse,
|
||||
&backoff,
|
||||
u,
|
||||
cloudflaredUUID,
|
||||
bufferPool,
|
||||
reconnectCh,
|
||||
)
|
||||
if recoverable {
|
||||
if duration, ok := backoff.GetBackoffDuration(ctx); ok {
|
||||
connectionLogger.Infof("Retrying in %s seconds", duration)
|
||||
config.Logger.Infof("Retrying connection %d in %s seconds", connectionIndex, duration)
|
||||
backoff.Backoff(ctx)
|
||||
continue
|
||||
}
|
||||
@@ -237,12 +256,12 @@ func ServeTunnel(
|
||||
ctx context.Context,
|
||||
credentialManager ReconnectTunnelCredentialManager,
|
||||
config *TunnelConfig,
|
||||
logger *log.Entry,
|
||||
logger logger.Service,
|
||||
addr *net.TCPAddr,
|
||||
connectionID uint8,
|
||||
connectionIndex uint8,
|
||||
connectedFuse *h2mux.BooleanFuse,
|
||||
backoff *BackoffHandler,
|
||||
u uuid.UUID,
|
||||
cloudflaredUUID uuid.UUID,
|
||||
bufferPool *buffer.Pool,
|
||||
reconnectCh chan ReconnectSignal,
|
||||
) (err error, recoverable bool) {
|
||||
@@ -258,23 +277,18 @@ func ServeTunnel(
|
||||
}
|
||||
}()
|
||||
|
||||
connectionTag := uint8ToString(connectionID)
|
||||
|
||||
// additional tags to send other than hostname which is set in cloudflared main package
|
||||
tags := make(map[string]string)
|
||||
tags["ha"] = connectionTag
|
||||
connectionTag := uint8ToString(connectionIndex)
|
||||
|
||||
// Returns error from parsing the origin URL or handshake errors
|
||||
handler, originLocalIP, err := NewTunnelHandler(ctx, config, addr, connectionID, bufferPool)
|
||||
handler, originLocalAddr, err := NewTunnelHandler(ctx, config, addr, connectionIndex, bufferPool)
|
||||
if err != nil {
|
||||
errLog := logger.WithError(err)
|
||||
switch err.(type) {
|
||||
case connection.DialError:
|
||||
errLog.Error("Unable to dial edge")
|
||||
logger.Errorf("Connection %d unable to dial edge: %s", connectionIndex, err)
|
||||
case h2mux.MuxerHandshakeError:
|
||||
errLog.Error("Handshake failed with edge server")
|
||||
logger.Errorf("Connection %d handshake with edge server failed: %s", connectionIndex, err)
|
||||
default:
|
||||
errLog.Error("Tunnel creation failure")
|
||||
logger.Errorf("Connection %d failed: %s", connectionIndex, err)
|
||||
return err, false
|
||||
}
|
||||
return err, true
|
||||
@@ -290,30 +304,31 @@ func ServeTunnel(
|
||||
}
|
||||
}()
|
||||
|
||||
if config.NamedTunnel != nil {
|
||||
return RegisterConnection(ctx, handler.muxer, config, connectionIndex, originLocalAddr)
|
||||
}
|
||||
|
||||
if config.UseReconnectToken && connectedFuse.Value() {
|
||||
token, tokenErr := credentialManager.ReconnectToken()
|
||||
eventDigest, eventDigestErr := credentialManager.EventDigest()
|
||||
// if we have both credentials, we can reconnect
|
||||
if tokenErr == nil && eventDigestErr == nil {
|
||||
var connDigest []byte
|
||||
|
||||
// check if we can use Quick Reconnects
|
||||
if config.UseQuickReconnects {
|
||||
if digest, connDigestErr := credentialManager.ConnDigest(connectionID); connDigestErr == nil {
|
||||
connDigest = digest
|
||||
}
|
||||
if digest, connDigestErr := credentialManager.ConnDigest(connectionIndex); connDigestErr == nil {
|
||||
connDigest = digest
|
||||
}
|
||||
return ReconnectTunnel(serveCtx, token, eventDigest, connDigest, handler.muxer, config, logger, connectionID, originLocalIP, u, credentialManager)
|
||||
|
||||
return ReconnectTunnel(serveCtx, token, eventDigest, connDigest, handler.muxer, config, logger, connectionIndex, originLocalAddr, cloudflaredUUID, credentialManager)
|
||||
}
|
||||
// log errors and proceed to RegisterTunnel
|
||||
if tokenErr != nil {
|
||||
logger.WithError(tokenErr).Error("Couldn't get reconnect token")
|
||||
logger.Errorf("Couldn't get reconnect token: %s", tokenErr)
|
||||
}
|
||||
if eventDigestErr != nil {
|
||||
logger.WithError(eventDigestErr).Error("Couldn't get event digest")
|
||||
logger.Errorf("Couldn't get event digest: %s", eventDigestErr)
|
||||
}
|
||||
}
|
||||
return RegisterTunnel(serveCtx, credentialManager, handler.muxer, config, logger, connectionID, originLocalIP, u)
|
||||
return RegisterTunnel(serveCtx, credentialManager, handler.muxer, config, logger, connectionIndex, originLocalAddr, cloudflaredUUID)
|
||||
})
|
||||
|
||||
errGroup.Go(func() error {
|
||||
@@ -322,12 +337,15 @@ func ServeTunnel(
|
||||
select {
|
||||
case <-serveCtx.Done():
|
||||
// UnregisterTunnel blocks until the RPC call returns
|
||||
var err error
|
||||
if connectedFuse.Value() {
|
||||
err = UnregisterTunnel(handler.muxer, config.GracePeriod, config.TransportLogger)
|
||||
if config.NamedTunnel != nil {
|
||||
_ = UnregisterConnection(ctx, handler.muxer, config)
|
||||
} else {
|
||||
_ = UnregisterTunnel(handler.muxer, config.GracePeriod, config.TransportLogger)
|
||||
}
|
||||
}
|
||||
handler.muxer.Shutdown()
|
||||
return err
|
||||
return nil
|
||||
case <-updateMetricsTickC:
|
||||
handler.UpdateMetrics(connectionTag)
|
||||
}
|
||||
@@ -358,14 +376,12 @@ func ServeTunnel(
|
||||
|
||||
err = errGroup.Wait()
|
||||
if err != nil {
|
||||
_ = newClientRegisterTunnelError(err, config.Metrics.regFail, unknown)
|
||||
|
||||
switch castedErr := err.(type) {
|
||||
case dupConnRegisterTunnelError:
|
||||
logger.Info("Already connected to this server, selecting a different one")
|
||||
return err, true
|
||||
case serverRegisterTunnelError:
|
||||
logger.WithError(castedErr.cause).Error("Register tunnel error from server side")
|
||||
logger.Errorf("Register tunnel error from server side: %s", castedErr.cause)
|
||||
// Don't send registration error return from server to Sentry. They are
|
||||
// logged on server side
|
||||
if incidents := config.IncidentLookup.ActiveIncidents(); len(incidents) > 0 {
|
||||
@@ -373,35 +389,103 @@ func ServeTunnel(
|
||||
}
|
||||
return castedErr.cause, !castedErr.permanent
|
||||
case clientRegisterTunnelError:
|
||||
logger.WithError(castedErr.cause).Error("Register tunnel error on client side")
|
||||
logger.Errorf("Register tunnel error on client side: %s", castedErr.cause)
|
||||
return err, true
|
||||
case muxerShutdownError:
|
||||
logger.Info("Muxer shutdown")
|
||||
return err, true
|
||||
case *ReconnectSignal:
|
||||
logger.Warnf("Restarting due to reconnect signal in %d seconds", castedErr.Delay)
|
||||
logger.Infof("Restarting connection %d due to reconnect signal in %d seconds", connectionIndex, castedErr.Delay)
|
||||
castedErr.DelayBeforeReconnect()
|
||||
return err, true
|
||||
default:
|
||||
logger.WithError(err).Error("Serve tunnel error")
|
||||
logger.Errorf("Serve tunnel error: %s", err)
|
||||
return err, true
|
||||
}
|
||||
}
|
||||
return nil, true
|
||||
}
|
||||
|
||||
func RegisterConnection(
|
||||
ctx context.Context,
|
||||
muxer *h2mux.Muxer,
|
||||
config *TunnelConfig,
|
||||
connectionIndex uint8,
|
||||
originLocalAddr string,
|
||||
) error {
|
||||
const registerConnection = "registerConnection"
|
||||
|
||||
config.TransportLogger.Debug("initiating RPC stream for RegisterConnection")
|
||||
rpc, err := connection.NewRPCClient(ctx, muxer, config.TransportLogger, openStreamTimeout)
|
||||
if err != nil {
|
||||
// RPC stream open error
|
||||
return newClientRegisterTunnelError(err, config.Metrics.rpcFail, registerConnection)
|
||||
}
|
||||
defer rpc.Close()
|
||||
|
||||
conn, err := rpc.RegisterConnection(
|
||||
ctx,
|
||||
config.NamedTunnel.Auth,
|
||||
config.NamedTunnel.ID,
|
||||
connectionIndex,
|
||||
config.ConnectionOptions(originLocalAddr),
|
||||
)
|
||||
if err != nil {
|
||||
if err.Error() == DuplicateConnectionError {
|
||||
config.Metrics.regFail.WithLabelValues("dup_edge_conn", registerConnection).Inc()
|
||||
return errDuplicationConnection
|
||||
}
|
||||
config.Metrics.regFail.WithLabelValues("server_error", registerConnection).Inc()
|
||||
return serverRegistrationErrorFromRPC(err)
|
||||
}
|
||||
|
||||
config.Metrics.regSuccess.WithLabelValues(registerConnection).Inc()
|
||||
config.Logger.Infof("Connection %d registered with %s using ID %s", connectionIndex, conn.Location, conn.UUID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func serverRegistrationErrorFromRPC(err error) *serverRegisterTunnelError {
|
||||
if retryable, ok := err.(*tunnelpogs.RetryableError); ok {
|
||||
return &serverRegisterTunnelError{
|
||||
cause: retryable.Unwrap(),
|
||||
permanent: false,
|
||||
}
|
||||
}
|
||||
return &serverRegisterTunnelError{
|
||||
cause: err,
|
||||
permanent: true,
|
||||
}
|
||||
}
|
||||
|
||||
func UnregisterConnection(
|
||||
ctx context.Context,
|
||||
muxer *h2mux.Muxer,
|
||||
config *TunnelConfig,
|
||||
) error {
|
||||
config.TransportLogger.Debug("initiating RPC stream for UnregisterConnection")
|
||||
rpc, err := connection.NewRPCClient(ctx, muxer, config.TransportLogger, openStreamTimeout)
|
||||
if err != nil {
|
||||
// RPC stream open error
|
||||
return newClientRegisterTunnelError(err, config.Metrics.rpcFail, register)
|
||||
}
|
||||
defer rpc.Close()
|
||||
|
||||
return rpc.UnregisterConnection(ctx)
|
||||
}
|
||||
|
||||
func RegisterTunnel(
|
||||
ctx context.Context,
|
||||
credentialManager ReconnectTunnelCredentialManager,
|
||||
muxer *h2mux.Muxer,
|
||||
config *TunnelConfig,
|
||||
logger *log.Entry,
|
||||
logger logger.Service,
|
||||
connectionID uint8,
|
||||
originLocalIP string,
|
||||
uuid uuid.UUID,
|
||||
) error {
|
||||
config.TransportLogger.Debug("initiating RPC stream to register")
|
||||
tunnelServer, err := connection.NewRPCClient(ctx, muxer, config.TransportLogger.WithField("subsystem", "rpc-register"), openStreamTimeout)
|
||||
tunnelServer, err := connection.NewRPCClient(ctx, muxer, config.TransportLogger, openStreamTimeout)
|
||||
if err != nil {
|
||||
// RPC stream open error
|
||||
return newClientRegisterTunnelError(err, config.Metrics.rpcFail, register)
|
||||
@@ -432,14 +516,14 @@ func ReconnectTunnel(
|
||||
eventDigest, connDigest []byte,
|
||||
muxer *h2mux.Muxer,
|
||||
config *TunnelConfig,
|
||||
logger *log.Entry,
|
||||
logger logger.Service,
|
||||
connectionID uint8,
|
||||
originLocalIP string,
|
||||
originLocalAddr string,
|
||||
uuid uuid.UUID,
|
||||
credentialManager ReconnectTunnelCredentialManager,
|
||||
) error {
|
||||
config.TransportLogger.Debug("initiating RPC stream to reconnect")
|
||||
tunnelServer, err := connection.NewRPCClient(ctx, muxer, config.TransportLogger.WithField("subsystem", "rpc-reconnect"), openStreamTimeout)
|
||||
tunnelServer, err := connection.NewRPCClient(ctx, muxer, config.TransportLogger, openStreamTimeout)
|
||||
if err != nil {
|
||||
// RPC stream open error
|
||||
return newClientRegisterTunnelError(err, config.Metrics.rpcFail, reconnect)
|
||||
@@ -456,7 +540,7 @@ func ReconnectTunnel(
|
||||
eventDigest,
|
||||
connDigest,
|
||||
config.Hostname,
|
||||
config.RegistrationOptions(connectionID, originLocalIP, uuid),
|
||||
config.RegistrationOptions(connectionID, originLocalAddr, uuid),
|
||||
)
|
||||
if registrationErr := registration.DeserializeError(); registrationErr != nil {
|
||||
// ReconnectTunnel RPC failure
|
||||
@@ -467,7 +551,7 @@ func ReconnectTunnel(
|
||||
|
||||
func processRegistrationSuccess(
|
||||
config *TunnelConfig,
|
||||
logger *log.Entry,
|
||||
logger logger.Service,
|
||||
connectionID uint8,
|
||||
registration *tunnelpogs.TunnelRegistration,
|
||||
name registerRPCName,
|
||||
@@ -486,10 +570,10 @@ func processRegistrationSuccess(
|
||||
if isTrialTunnel := config.Hostname == ""; isTrialTunnel {
|
||||
if url, err := url.Parse(registration.Url); err == nil {
|
||||
for _, line := range asciiBox(trialZoneMsg(url.String()), 2) {
|
||||
logger.Infoln(line)
|
||||
logger.Info(line)
|
||||
}
|
||||
} else {
|
||||
logger.Errorln("Failed to connect tunnel, please try again.")
|
||||
logger.Error("Failed to connect tunnel, please try again.")
|
||||
return fmt.Errorf("empty URL in response from Cloudflare edge")
|
||||
}
|
||||
}
|
||||
@@ -505,19 +589,19 @@ func processRegistrationSuccess(
|
||||
func processRegisterTunnelError(err tunnelpogs.TunnelRegistrationError, metrics *TunnelMetrics, name registerRPCName) error {
|
||||
if err.Error() == DuplicateConnectionError {
|
||||
metrics.regFail.WithLabelValues("dup_edge_conn", string(name)).Inc()
|
||||
return dupConnRegisterTunnelError{}
|
||||
return errDuplicationConnection
|
||||
}
|
||||
metrics.regFail.WithLabelValues("server_error", string(name)).Inc()
|
||||
return serverRegisterTunnelError{
|
||||
cause: fmt.Errorf("Server error: %s", err.Error()),
|
||||
cause: err,
|
||||
permanent: err.IsPermanent(),
|
||||
}
|
||||
}
|
||||
|
||||
func UnregisterTunnel(muxer *h2mux.Muxer, gracePeriod time.Duration, logger *log.Logger) error {
|
||||
func UnregisterTunnel(muxer *h2mux.Muxer, gracePeriod time.Duration, logger logger.Service) error {
|
||||
logger.Debug("initiating RPC stream to unregister")
|
||||
ctx := context.Background()
|
||||
tunnelServer, err := connection.NewRPCClient(ctx, muxer, logger.WithField("subsystem", "rpc-unregister"), openStreamTimeout)
|
||||
tunnelServer, err := connection.NewRPCClient(ctx, muxer, logger, openStreamTimeout)
|
||||
if err != nil {
|
||||
// RPC stream open error
|
||||
return err
|
||||
@@ -532,16 +616,16 @@ func LogServerInfo(
|
||||
promise tunnelrpc.ServerInfo_Promise,
|
||||
connectionID uint8,
|
||||
metrics *TunnelMetrics,
|
||||
logger *log.Entry,
|
||||
logger logger.Service,
|
||||
) {
|
||||
serverInfoMessage, err := promise.Struct()
|
||||
if err != nil {
|
||||
logger.WithError(err).Warn("Failed to retrieve server information")
|
||||
logger.Errorf("Failed to retrieve server information: %s", err)
|
||||
return
|
||||
}
|
||||
serverInfo, err := tunnelpogs.UnmarshalServerInfo(serverInfoMessage)
|
||||
if err != nil {
|
||||
logger.WithError(err).Warn("Failed to retrieve server information")
|
||||
logger.Errorf("Failed to retrieve server information: %s", err)
|
||||
return
|
||||
}
|
||||
logger.Infof("Connected to %s", serverInfo.LocationName)
|
||||
@@ -558,7 +642,7 @@ type TunnelHandler struct {
|
||||
metrics *TunnelMetrics
|
||||
// connectionID is only used by metrics, and prometheus requires labels to be string
|
||||
connectionID string
|
||||
logger *log.Logger
|
||||
logger logger.Service
|
||||
noChunkedEncoding bool
|
||||
|
||||
bufferPool *buffer.Pool
|
||||
@@ -620,8 +704,8 @@ func (h *TunnelHandler) ServeStream(stream *h2mux.MuxedStream) error {
|
||||
return reqErr
|
||||
}
|
||||
|
||||
cfRay := streamhandler.FindCfRayHeader(req)
|
||||
lbProbe := streamhandler.IsLBProbeRequest(req)
|
||||
cfRay := findCfRayHeader(req)
|
||||
lbProbe := isLBProbeRequest(req)
|
||||
h.logRequest(req, cfRay, lbProbe)
|
||||
|
||||
var resp *http.Response
|
||||
@@ -736,7 +820,7 @@ func (h *TunnelHandler) isEventStream(response *http.Response) bool {
|
||||
}
|
||||
|
||||
func (h *TunnelHandler) writeErrorResponse(stream *h2mux.MuxedStream, err error) {
|
||||
h.logger.WithError(err).Error("HTTP request error")
|
||||
h.logger.Errorf("HTTP request error: %s", err)
|
||||
stream.WriteHeaders([]h2mux.Header{
|
||||
{Name: ":status", Value: "502"},
|
||||
h2mux.CreateResponseMetaHeader(h2mux.ResponseMetaHeaderField, h2mux.ResponseSourceCloudflared),
|
||||
@@ -746,41 +830,39 @@ func (h *TunnelHandler) writeErrorResponse(stream *h2mux.MuxedStream, err error)
|
||||
}
|
||||
|
||||
func (h *TunnelHandler) logRequest(req *http.Request, cfRay string, lbProbe bool) {
|
||||
logger := log.NewEntry(h.logger)
|
||||
logger := h.logger
|
||||
if cfRay != "" {
|
||||
logger = logger.WithField("CF-RAY", cfRay)
|
||||
logger.Debugf("%s %s %s", req.Method, req.URL, req.Proto)
|
||||
logger.Debugf("CF-RAY: %s %s %s %s", cfRay, req.Method, req.URL, req.Proto)
|
||||
} else if lbProbe {
|
||||
logger.Debugf("Load Balancer health check %s %s %s", req.Method, req.URL, req.Proto)
|
||||
logger.Debugf("CF-RAY: %s Load Balancer health check %s %s %s", cfRay, req.Method, req.URL, req.Proto)
|
||||
} else {
|
||||
logger.Warnf("All requests should have a CF-RAY header. Please open a support ticket with Cloudflare. %s %s %s ", req.Method, req.URL, req.Proto)
|
||||
logger.Infof("CF-RAY: %s All requests should have a CF-RAY header. Please open a support ticket with Cloudflare. %s %s %s ", cfRay, req.Method, req.URL, req.Proto)
|
||||
}
|
||||
logger.Debugf("Request Headers %+v", req.Header)
|
||||
logger.Debugf("CF-RAY: %s Request Headers %+v", cfRay, req.Header)
|
||||
|
||||
if contentLen := req.ContentLength; contentLen == -1 {
|
||||
logger.Debugf("Request Content length unknown")
|
||||
logger.Debugf("CF-RAY: %s Request Content length unknown", cfRay)
|
||||
} else {
|
||||
logger.Debugf("Request content length %d", contentLen)
|
||||
logger.Debugf("CF-RAY: %s Request content length %d", cfRay, contentLen)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *TunnelHandler) logResponseOk(r *http.Response, cfRay string, lbProbe bool) {
|
||||
h.metrics.incrementResponses(h.connectionID, "200")
|
||||
logger := log.NewEntry(h.logger)
|
||||
logger := h.logger
|
||||
if cfRay != "" {
|
||||
logger = logger.WithField("CF-RAY", cfRay)
|
||||
logger.Debugf("%s", r.Status)
|
||||
logger.Debugf("CF-RAY: %s %s", cfRay, r.Status)
|
||||
} else if lbProbe {
|
||||
logger.Debugf("Response to Load Balancer health check %s", r.Status)
|
||||
} else {
|
||||
logger.Infof("%s", r.Status)
|
||||
}
|
||||
logger.Debugf("Response Headers %+v", r.Header)
|
||||
logger.Debugf("CF-RAY: %s Response Headers %+v", cfRay, r.Header)
|
||||
|
||||
if contentLen := r.ContentLength; contentLen == -1 {
|
||||
logger.Debugf("Response content length unknown")
|
||||
logger.Debugf("CF-RAY: %s Response content length unknown", cfRay)
|
||||
} else {
|
||||
logger.Debugf("Response content length %d", contentLen)
|
||||
logger.Debugf("CF-RAY: %s Response content length %d", cfRay, contentLen)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -837,3 +919,11 @@ func activeIncidentsMsg(incidents []Incident) string {
|
||||
return preamble + " " + strings.Join(incidentStrings, "; ")
|
||||
|
||||
}
|
||||
|
||||
func findCfRayHeader(h1 *http.Request) string {
|
||||
return h1.Header.Get("Cf-Ray")
|
||||
}
|
||||
|
||||
func isLBProbeRequest(req *http.Request) bool {
|
||||
return strings.HasPrefix(req.UserAgent(), lbProbeUserAgentPrefix)
|
||||
}
|
||||
|
||||
@@ -1,247 +0,0 @@
|
||||
// Package client defines and implements interface to proxy to HTTP, websocket and hello world origins
|
||||
package originservice
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/buffer"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/hello"
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// OriginService is an interface to proxy requests to different type of origins
|
||||
type OriginService interface {
|
||||
Proxy(stream *h2mux.MuxedStream, req *http.Request) (resp *http.Response, err error)
|
||||
URL() *url.URL
|
||||
Summary() string
|
||||
Shutdown()
|
||||
}
|
||||
|
||||
// HTTPService talks to origin using HTTP/HTTPS
|
||||
type HTTPService struct {
|
||||
client http.RoundTripper
|
||||
originURL *url.URL
|
||||
chunkedEncoding bool
|
||||
bufferPool *buffer.Pool
|
||||
}
|
||||
|
||||
func NewHTTPService(transport http.RoundTripper, url *url.URL, chunkedEncoding bool) OriginService {
|
||||
return &HTTPService{
|
||||
client: transport,
|
||||
originURL: url,
|
||||
chunkedEncoding: chunkedEncoding,
|
||||
bufferPool: buffer.NewPool(512 * 1024),
|
||||
}
|
||||
}
|
||||
|
||||
func (hc *HTTPService) Proxy(stream *h2mux.MuxedStream, req *http.Request) (*http.Response, error) {
|
||||
const responseSourceOrigin = "origin"
|
||||
|
||||
// Support for WSGI Servers by switching transfer encoding from chunked to gzip/deflate
|
||||
if !hc.chunkedEncoding {
|
||||
req.TransferEncoding = []string{"gzip", "deflate"}
|
||||
cLength, err := strconv.Atoi(req.Header.Get("Content-Length"))
|
||||
if err == nil {
|
||||
req.ContentLength = int64(cLength)
|
||||
}
|
||||
}
|
||||
|
||||
// Request origin to keep connection alive to improve performance
|
||||
req.Header.Set("Connection", "keep-alive")
|
||||
|
||||
resp, err := hc.client.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error proxying request to HTTP origin")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
responseHeaders := h1ResponseToH2Response(resp)
|
||||
responseHeaders = append(responseHeaders, h2mux.CreateResponseMetaHeader(h2mux.ResponseMetaHeaderField, responseSourceOrigin))
|
||||
err = stream.WriteHeaders(responseHeaders)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error writing response header to HTTP origin")
|
||||
}
|
||||
if isEventStream(resp) {
|
||||
writeEventStream(stream, resp.Body)
|
||||
} else {
|
||||
// Use CopyBuffer, because Copy only allocates a 32KiB buffer, and cross-stream
|
||||
// compression generates dictionary on first write
|
||||
buf := hc.bufferPool.Get()
|
||||
defer hc.bufferPool.Put(buf)
|
||||
io.CopyBuffer(stream, resp.Body, buf)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (hc *HTTPService) URL() *url.URL {
|
||||
return hc.originURL
|
||||
}
|
||||
|
||||
func (hc *HTTPService) Summary() string {
|
||||
return fmt.Sprintf("HTTP service listening on %s", hc.originURL)
|
||||
}
|
||||
|
||||
func (hc *HTTPService) Shutdown() {}
|
||||
|
||||
// WebsocketService talks to origin using WS/WSS
|
||||
type WebsocketService struct {
|
||||
tlsConfig *tls.Config
|
||||
originURL *url.URL
|
||||
shutdownC chan struct{}
|
||||
}
|
||||
|
||||
func NewWebSocketService(tlsConfig *tls.Config, url *url.URL) (OriginService, error) {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot start Websocket Proxy Server")
|
||||
}
|
||||
shutdownC := make(chan struct{})
|
||||
go func() {
|
||||
websocket.StartProxyServer(log.CreateLogger(), listener, url.String(), shutdownC, websocket.DefaultStreamHandler)
|
||||
}()
|
||||
return &WebsocketService{
|
||||
tlsConfig: tlsConfig,
|
||||
originURL: url,
|
||||
shutdownC: shutdownC,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (wsc *WebsocketService) Proxy(stream *h2mux.MuxedStream, req *http.Request) (*http.Response, error) {
|
||||
if !websocket.IsWebSocketUpgrade(req) {
|
||||
return nil, fmt.Errorf("request is not a websocket connection")
|
||||
}
|
||||
conn, response, err := websocket.ClientConnect(req, wsc.tlsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
err = stream.WriteHeaders(h1ResponseToH2Response(response))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error writing response header to websocket origin")
|
||||
}
|
||||
// Copy to/from stream to the undelying connection. Use the underlying
|
||||
// connection because cloudflared doesn't operate on the message themselves
|
||||
websocket.Stream(conn.UnderlyingConn(), stream)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (wsc *WebsocketService) URL() *url.URL {
|
||||
return wsc.originURL
|
||||
}
|
||||
|
||||
func (wsc *WebsocketService) Summary() string {
|
||||
return fmt.Sprintf("Websocket listening on %s", wsc.originURL)
|
||||
}
|
||||
|
||||
func (wsc *WebsocketService) Shutdown() {
|
||||
close(wsc.shutdownC)
|
||||
}
|
||||
|
||||
// HelloWorldService talks to the hello world example origin
|
||||
type HelloWorldService struct {
|
||||
client http.RoundTripper
|
||||
listener net.Listener
|
||||
originURL *url.URL
|
||||
shutdownC chan struct{}
|
||||
bufferPool *buffer.Pool
|
||||
}
|
||||
|
||||
func NewHelloWorldService(transport http.RoundTripper) (OriginService, error) {
|
||||
listener, err := hello.CreateTLSListener("127.0.0.1:")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot start Hello World Server")
|
||||
}
|
||||
shutdownC := make(chan struct{})
|
||||
go func() {
|
||||
hello.StartHelloWorldServer(log.CreateLogger(), listener, shutdownC)
|
||||
}()
|
||||
return &HelloWorldService{
|
||||
client: transport,
|
||||
listener: listener,
|
||||
originURL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: listener.Addr().String(),
|
||||
},
|
||||
shutdownC: shutdownC,
|
||||
bufferPool: buffer.NewPool(512 * 1024),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (hwc *HelloWorldService) Proxy(stream *h2mux.MuxedStream, req *http.Request) (*http.Response, error) {
|
||||
// Request origin to keep connection alive to improve performance
|
||||
req.Header.Set("Connection", "keep-alive")
|
||||
resp, err := hwc.client.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error proxying request to Hello World origin")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
err = stream.WriteHeaders(h1ResponseToH2Response(resp))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error writing response header to Hello World origin")
|
||||
}
|
||||
|
||||
// Use CopyBuffer, because Copy only allocates a 32KiB buffer, and cross-stream
|
||||
// compression generates dictionary on first write
|
||||
buf := hwc.bufferPool.Get()
|
||||
defer hwc.bufferPool.Put(buf)
|
||||
io.CopyBuffer(stream, resp.Body, buf)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (hwc *HelloWorldService) URL() *url.URL {
|
||||
return hwc.originURL
|
||||
}
|
||||
|
||||
func (hwc *HelloWorldService) Summary() string {
|
||||
return fmt.Sprintf("Hello World service listening on %s", hwc.originURL)
|
||||
}
|
||||
|
||||
func (hwc *HelloWorldService) Shutdown() {
|
||||
hwc.listener.Close()
|
||||
}
|
||||
|
||||
func isEventStream(resp *http.Response) bool {
|
||||
// Check if content-type is text/event-stream. We need to check if the header value starts with text/event-stream
|
||||
// because text/event-stream; charset=UTF-8 is also valid
|
||||
// Ref: https://tools.ietf.org/html/rfc7231#section-3.1.1.1
|
||||
for _, contentType := range resp.Header["content-type"] {
|
||||
if strings.HasPrefix(strings.ToLower(contentType), "text/event-stream") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func writeEventStream(stream *h2mux.MuxedStream, respBody io.ReadCloser) {
|
||||
reader := bufio.NewReader(respBody)
|
||||
for {
|
||||
line, err := reader.ReadBytes('\n')
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
stream.Write(line)
|
||||
}
|
||||
}
|
||||
|
||||
func h1ResponseToH2Response(h1 *http.Response) (h2 []h2mux.Header) {
|
||||
h2 = []h2mux.Header{{Name: ":status", Value: fmt.Sprintf("%d", h1.StatusCode)}}
|
||||
for headerName, headerValues := range h1.Header {
|
||||
for _, headerValue := range headerValues {
|
||||
h2 = append(h2, h2mux.Header{Name: strings.ToLower(headerName), Value: headerValue})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package originservice
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsEventStream(t *testing.T) {
|
||||
tests := []struct {
|
||||
resp *http.Response
|
||||
isEventStream bool
|
||||
}{
|
||||
{
|
||||
resp: &http.Response{},
|
||||
isEventStream: false,
|
||||
},
|
||||
{
|
||||
// isEventStream checks all headers
|
||||
resp: &http.Response{
|
||||
Header: http.Header{
|
||||
"accept": []string{"text/html"},
|
||||
"content-type": []string{"text/event-stream"},
|
||||
},
|
||||
},
|
||||
isEventStream: true,
|
||||
},
|
||||
{
|
||||
// Content-Type and text/event-stream are case-insensitive. text/event-stream can be followed by OWS parameter
|
||||
resp: &http.Response{
|
||||
Header: http.Header{
|
||||
"content-type": []string{"Text/event-stream;charset=utf-8"},
|
||||
},
|
||||
},
|
||||
isEventStream: true,
|
||||
},
|
||||
{
|
||||
// Content-Type and text/event-stream are case-insensitive. text/event-stream can be followed by OWS parameter
|
||||
resp: &http.Response{
|
||||
Header: http.Header{
|
||||
"content-type": []string{"appication/json", "text/html", "Text/event-stream;charset=utf-8"},
|
||||
},
|
||||
},
|
||||
isEventStream: true,
|
||||
},
|
||||
{
|
||||
// Not an event stream because the content-type value doesn't start with text/event-stream
|
||||
resp: &http.Response{
|
||||
Header: http.Header{
|
||||
"content-type": []string{" text/event-stream"},
|
||||
},
|
||||
},
|
||||
isEventStream: false,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
assert.Equal(t, test.isEventStream, isEventStream(test.resp), "Header: %v", test.resp.Header)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,20 @@
|
||||
package overwatch
|
||||
|
||||
// ServiceCallback is a service notify it's runloop finished.
|
||||
// the first parameter is the service type
|
||||
// the second parameter is the service name
|
||||
// the third parameter is an optional error if the service failed
|
||||
type ServiceCallback func(string, string, error)
|
||||
|
||||
// AppManager is the default implementation of overwatch service management
|
||||
type AppManager struct {
|
||||
services map[string]Service
|
||||
errorChan chan error
|
||||
services map[string]Service
|
||||
callback ServiceCallback
|
||||
}
|
||||
|
||||
// NewAppManager creates a new overwatch manager
|
||||
func NewAppManager(errorChan chan error) Manager {
|
||||
return &AppManager{services: make(map[string]Service), errorChan: errorChan}
|
||||
func NewAppManager(callback ServiceCallback) Manager {
|
||||
return &AppManager{services: make(map[string]Service), callback: callback}
|
||||
}
|
||||
|
||||
// Add takes in a new service to manage.
|
||||
@@ -47,7 +53,7 @@ func (m *AppManager) Services() []Service {
|
||||
|
||||
func (m *AppManager) serviceRun(service Service) {
|
||||
err := service.Run()
|
||||
if err != nil && m.errorChan != nil {
|
||||
m.errorChan <- err
|
||||
if m.callback != nil {
|
||||
m.callback(service.Type(), service.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,10 @@ func TestManagerDuplicate(t *testing.T) {
|
||||
|
||||
func TestManagerErrorChannel(t *testing.T) {
|
||||
errChan := make(chan error)
|
||||
m := NewAppManager(errChan)
|
||||
serviceCallback := func(t string, name string, err error) {
|
||||
errChan <- err
|
||||
}
|
||||
m := NewAppManager(serviceCallback)
|
||||
|
||||
err := errors.New("test error")
|
||||
first := &mockService{serviceName: "first", serviceType: "mock", runError: err}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/proxy"
|
||||
@@ -58,7 +59,6 @@ func startTestServer(t *testing.T, httpHandler func(w http.ResponseWriter, r *ht
|
||||
|
||||
// start the servers
|
||||
go http.ListenAndServe(":8085", mux)
|
||||
|
||||
}
|
||||
|
||||
func respondWithJSON(w http.ResponseWriter, v interface{}, status int) {
|
||||
@@ -78,6 +78,7 @@ func OkJSONResponseHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func TestSocksConnection(t *testing.T) {
|
||||
startTestServer(t, OkJSONResponseHandler)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
b := sendSocksRequest(t)
|
||||
assert.True(t, len(b) > 0, "no data returned!")
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package sshlog
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
//empty manager implements the Manager but does nothing (for testing and to disable logging unless the logs are set)
|
||||
@@ -18,11 +18,11 @@ func NewEmptyManager() Manager {
|
||||
return &emptyManager{}
|
||||
}
|
||||
|
||||
func (m *emptyManager) NewLogger(name string, logger *logrus.Logger) (io.WriteCloser, error) {
|
||||
func (m *emptyManager) NewLogger(name string, logger logger.Service) (io.WriteCloser, error) {
|
||||
return &emptyWriteCloser{}, nil
|
||||
}
|
||||
|
||||
func (m *emptyManager) NewSessionLogger(name string, logger *logrus.Logger) (io.WriteCloser, error) {
|
||||
func (m *emptyManager) NewSessionLogger(name string, logger logger.Service) (io.WriteCloser, error) {
|
||||
return &emptyWriteCloser{}, nil
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -9,7 +9,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -24,7 +24,7 @@ type Logger struct {
|
||||
filename string
|
||||
file *os.File
|
||||
writeBuffer *bufio.Writer
|
||||
logger *logrus.Logger
|
||||
logger logger.Service
|
||||
flushInterval time.Duration
|
||||
maxFileSize int64
|
||||
done chan struct{}
|
||||
@@ -35,10 +35,10 @@ type Logger struct {
|
||||
// drained and closed when the caller is finished, so instances should call
|
||||
// Close when finished with this Logger instance. Writes will be flushed to disk
|
||||
// every second (fsync). filename is the name of the logfile to be created. The
|
||||
// logger variable is a logrus that will log all i/o, filesystem error etc, that
|
||||
// logger variable is a logger service that will log all i/o, filesystem error etc, that
|
||||
// that shouldn't end execution of the logger, but are useful to report to the
|
||||
// caller.
|
||||
func NewLogger(filename string, logger *logrus.Logger, flushInterval time.Duration, maxFileSize int64) (*Logger, error) {
|
||||
func NewLogger(filename string, logger logger.Service, flushInterval time.Duration, maxFileSize int64) (*Logger, error) {
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger can't be nil")
|
||||
}
|
||||
@@ -87,7 +87,7 @@ func (l *Logger) writer() {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := l.write(); err != nil {
|
||||
l.logger.Errorln(err)
|
||||
l.logger.Errorf("%s", err)
|
||||
}
|
||||
case <-l.done:
|
||||
return
|
||||
|
||||
@@ -8,20 +8,21 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
const logFileName = "test-logger.log"
|
||||
|
||||
func createLogger(t *testing.T) *Logger {
|
||||
os.Remove(logFileName)
|
||||
l := logrus.New()
|
||||
l := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
logger, err := NewLogger(logFileName, l, time.Millisecond, 1024)
|
||||
if err != nil {
|
||||
t.Fatal("couldn't create the logger!", err)
|
||||
}
|
||||
return logger
|
||||
}
|
||||
|
||||
// AUTH-2115 TODO: fix this test
|
||||
//func TestWrite(t *testing.T) {
|
||||
// testStr := "hi"
|
||||
|
||||
+5
-5
@@ -5,13 +5,13 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
// Manager be managing logs bruh
|
||||
type Manager interface {
|
||||
NewLogger(string, *logrus.Logger) (io.WriteCloser, error)
|
||||
NewSessionLogger(string, *logrus.Logger) (io.WriteCloser, error)
|
||||
NewLogger(string, logger.Service) (io.WriteCloser, error)
|
||||
NewSessionLogger(string, logger.Service) (io.WriteCloser, error)
|
||||
}
|
||||
|
||||
type manager struct {
|
||||
@@ -25,10 +25,10 @@ func New(baseDirectory string) Manager {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *manager) NewLogger(name string, logger *logrus.Logger) (io.WriteCloser, error) {
|
||||
func (m *manager) NewLogger(name string, logger logger.Service) (io.WriteCloser, error) {
|
||||
return NewLogger(filepath.Join(m.baseDirectory, name), logger, time.Second, defaultFileSizeLimit)
|
||||
}
|
||||
|
||||
func (m *manager) NewSessionLogger(name string, logger *logrus.Logger) (io.WriteCloser, error) {
|
||||
func (m *manager) NewSessionLogger(name string, logger logger.Service) (io.WriteCloser, error) {
|
||||
return NewSessionLogger(filepath.Join(m.baseDirectory, name), logger, time.Second, defaultFileSizeLimit)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package sshlog
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
capnp "zombiezen.com/go/capnproto2"
|
||||
"zombiezen.com/go/capnproto2/pogs"
|
||||
)
|
||||
@@ -20,7 +20,7 @@ type sessionLogData struct {
|
||||
}
|
||||
|
||||
// NewSessionLogger creates a new session logger by encapsulating a Logger object and writing capnp encoded messages to it
|
||||
func NewSessionLogger(filename string, logger *logrus.Logger, flushInterval time.Duration, maxFileSize int64) (*SessionLogger, error) {
|
||||
func NewSessionLogger(filename string, logger logger.Service, flushInterval time.Duration, maxFileSize int64) (*SessionLogger, error) {
|
||||
l, err := NewLogger(filename, logger, flushInterval, maxFileSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
capnp "zombiezen.com/go/capnproto2"
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ const sessionLogFileName = "test-session-logger.log"
|
||||
|
||||
func createSessionLogger(t *testing.T) *SessionLogger {
|
||||
os.Remove(sessionLogFileName)
|
||||
l := logrus.New()
|
||||
l := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
logger, err := NewSessionLogger(sessionLogFileName, l, time.Millisecond, 1024)
|
||||
if err != nil {
|
||||
t.Fatal("couldn't create the logger!", err)
|
||||
@@ -25,12 +25,12 @@ func TestSessionLogWrite(t *testing.T) {
|
||||
testStr := "hi"
|
||||
logger := createSessionLogger(t)
|
||||
defer func() {
|
||||
logger.Close()
|
||||
os.Remove(sessionLogFileName)
|
||||
}()
|
||||
|
||||
logger.Write([]byte(testStr))
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
logger.Close()
|
||||
|
||||
f, err := os.Open(sessionLogFileName)
|
||||
if err != nil {
|
||||
t.Fatal("couldn't read the log file!", err)
|
||||
|
||||
@@ -19,8 +19,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
rsaFilename = "ssh_host_rsa_key"
|
||||
ecdsaFilename = "ssh_host_ecdsa_key"
|
||||
rsaFilename = "ssh_host_rsa_key"
|
||||
ecdsaFilename = "ssh_host_ecdsa_key"
|
||||
)
|
||||
|
||||
var defaultHostKeyDir = filepath.Join(".cloudflared", "host_keys")
|
||||
@@ -68,7 +68,7 @@ func (s *SSHProxy) configureRSAKey(basePath string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Debug("Created new RSA SSH host key: ", keyPath)
|
||||
s.logger.Debugf("Created new RSA SSH host key: %s", keyPath)
|
||||
}
|
||||
if err := s.SetOption(ssh.HostKeyFile(keyPath)); err != nil {
|
||||
return errors.Wrap(err, "Could not set SSH RSA host key")
|
||||
@@ -98,7 +98,7 @@ func (s *SSHProxy) configureECDSAKey(basePath string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Debug("Created new ECDSA SSH host key: ", keyPath)
|
||||
s.logger.Debugf("Created new ECDSA SSH host key: %s", keyPath)
|
||||
}
|
||||
if err := s.SetOption(ssh.HostKeyFile(keyPath)); err != nil {
|
||||
return errors.Wrap(err, "Could not set SSH ECDSA host key")
|
||||
|
||||
+23
-22
@@ -15,12 +15,12 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/sshgen"
|
||||
"github.com/cloudflare/cloudflared/sshlog"
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
@@ -66,7 +66,7 @@ func (c sshConn) Close() error {
|
||||
type SSHProxy struct {
|
||||
ssh.Server
|
||||
hostname string
|
||||
logger *logrus.Logger
|
||||
logger logger.Service
|
||||
shutdownC chan struct{}
|
||||
caCert ssh.PublicKey
|
||||
logManager sshlog.Manager
|
||||
@@ -78,7 +78,7 @@ type SSHPreamble struct {
|
||||
}
|
||||
|
||||
// New creates a new SSHProxy and configures its host keys and authentication by the data provided
|
||||
func New(logManager sshlog.Manager, logger *logrus.Logger, version, localAddress, hostname, hostKeyDir string, shutdownC chan struct{}, idleTimeout, maxTimeout time.Duration) (*SSHProxy, error) {
|
||||
func New(logManager sshlog.Manager, logger logger.Service, version, localAddress, hostname, hostKeyDir string, shutdownC chan struct{}, idleTimeout, maxTimeout time.Duration) (*SSHProxy, error) {
|
||||
sshProxy := SSHProxy{
|
||||
hostname: hostname,
|
||||
logger: logger,
|
||||
@@ -112,7 +112,7 @@ func (s *SSHProxy) Start() error {
|
||||
go func() {
|
||||
<-s.shutdownC
|
||||
if err := s.Close(); err != nil {
|
||||
s.logger.WithError(err).Error("Cannot close SSH server")
|
||||
s.logger.Errorf("Cannot close SSH server: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -141,9 +141,9 @@ func (s *SSHProxy) connCallback(ctx ssh.Context, conn net.Conn) net.Conn {
|
||||
preamble, err := s.readPreamble(conn)
|
||||
if err != nil {
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
s.logger.Warn("Could not establish session. Client likely does not have --destination set and is using old-style ssh config")
|
||||
s.logger.Info("Could not establish session. Client likely does not have --destination set and is using old-style ssh config")
|
||||
} else if err != io.EOF {
|
||||
s.logger.WithError(err).Error("failed to read SSH preamble")
|
||||
s.logger.Errorf("failed to read SSH preamble: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -151,7 +151,7 @@ func (s *SSHProxy) connCallback(ctx ssh.Context, conn net.Conn) net.Conn {
|
||||
|
||||
logger, sessionID, err := s.auditLogger()
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("failed to configure logger")
|
||||
s.logger.Errorf("failed to configure logger: %s", err)
|
||||
return nil
|
||||
}
|
||||
ctx.SetValue(sshContextEventLogger, logger)
|
||||
@@ -175,14 +175,14 @@ func (s *SSHProxy) channelHandler(srv *ssh.Server, conn *gossh.ServerConn, newCh
|
||||
msg := fmt.Sprintf("channel type %s is not supported", newChan.ChannelType())
|
||||
s.logger.Info(msg)
|
||||
if err := newChan.Reject(gossh.UnknownChannelType, msg); err != nil {
|
||||
s.logger.WithError(err).Error("Error rejecting SSH channel")
|
||||
s.logger.Errorf("Error rejecting SSH channel: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
localChan, localChanReqs, err := newChan.Accept()
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to accept session channel")
|
||||
s.logger.Errorf("Failed to accept session channel: %s", err)
|
||||
return
|
||||
}
|
||||
defer localChan.Close()
|
||||
@@ -196,7 +196,7 @@ func (s *SSHProxy) channelHandler(srv *ssh.Server, conn *gossh.ServerConn, newCh
|
||||
|
||||
remoteChan, remoteChanReqs, err := client.OpenChannel(newChan.ChannelType(), newChan.ExtraData())
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to open remote channel")
|
||||
s.logger.Errorf("Failed to open remote channel: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -211,13 +211,13 @@ func (s *SSHProxy) proxyChannel(localChan, remoteChan gossh.Channel, localChanRe
|
||||
done := make(chan struct{}, 2)
|
||||
go func() {
|
||||
if _, err := io.Copy(localChan, remoteChan); err != nil {
|
||||
s.logger.WithError(err).Error("remote to local copy error")
|
||||
s.logger.Errorf("remote to local copy error: %s", err)
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
go func() {
|
||||
if _, err := io.Copy(remoteChan, localChan); err != nil {
|
||||
s.logger.WithError(err).Error("local to remote copy error")
|
||||
s.logger.Errorf("local to remote copy error: %s", err)
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
@@ -227,12 +227,12 @@ func (s *SSHProxy) proxyChannel(localChan, remoteChan gossh.Channel, localChanRe
|
||||
localStderr := localChan.Stderr()
|
||||
go func() {
|
||||
if _, err := io.Copy(remoteStderr, localStderr); err != nil {
|
||||
s.logger.WithError(err).Error("stderr local to remote copy error")
|
||||
s.logger.Errorf("stderr local to remote copy error: %s", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
if _, err := io.Copy(localStderr, remoteStderr); err != nil {
|
||||
s.logger.WithError(err).Error("stderr remote to local copy error")
|
||||
s.logger.Errorf("stderr remote to local copy error: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -247,7 +247,7 @@ func (s *SSHProxy) proxyChannel(localChan, remoteChan gossh.Channel, localChanRe
|
||||
return
|
||||
}
|
||||
if err := s.forwardChannelRequest(remoteChan, req); err != nil {
|
||||
s.logger.WithError(err).Error("Failed to forward request")
|
||||
s.logger.Errorf("Failed to forward request: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -258,7 +258,7 @@ func (s *SSHProxy) proxyChannel(localChan, remoteChan gossh.Channel, localChanRe
|
||||
return
|
||||
}
|
||||
if err := s.forwardChannelRequest(localChan, req); err != nil {
|
||||
s.logger.WithError(err).Error("Failed to forward request")
|
||||
s.logger.Errorf("Failed to forward request: %s", err)
|
||||
return
|
||||
}
|
||||
case <-done:
|
||||
@@ -267,6 +267,7 @@ func (s *SSHProxy) proxyChannel(localChan, remoteChan gossh.Channel, localChanRe
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// readPreamble reads a preamble from the SSH connection before any SSH traffic is sent.
|
||||
// This preamble is a JSON encoded struct containing the users JWT and ultimate destination.
|
||||
// The first 4 bytes contain the length of the preamble which follows immediately.
|
||||
@@ -277,7 +278,7 @@ func (s *SSHProxy) readPreamble(conn net.Conn) (*SSHPreamble, error) {
|
||||
}
|
||||
defer func() {
|
||||
if err := conn.SetReadDeadline(time.Time{}); err != nil {
|
||||
s.logger.WithError(err).Error("Failed to unset conn read deadline")
|
||||
s.logger.Errorf("Failed to unset conn read deadline: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -340,7 +341,7 @@ func (s *SSHProxy) dialDestination(ctx ssh.Context) (*gossh.Client, error) {
|
||||
|
||||
signer, err := s.genSSHSigner(preamble.JWT)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to generate signed short lived cert")
|
||||
s.logger.Errorf("Failed to generate signed short lived cert: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
s.logger.Debugf("Short lived certificate for %s connecting to %s:\n\n%s", ctx.User(), preamble.Destination, gossh.MarshalAuthorizedKey(signer.PublicKey()))
|
||||
@@ -355,7 +356,7 @@ func (s *SSHProxy) dialDestination(ctx ssh.Context) (*gossh.Client, error) {
|
||||
|
||||
client, err := gossh.Dial("tcp", preamble.Destination, clientConfig)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Info("Failed to connect to destination SSH server")
|
||||
s.logger.Errorf("Failed to connect to destination SSH server: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
@@ -420,7 +421,7 @@ func (s *SSHProxy) logChannelRequest(req *gossh.Request, conn *gossh.ServerConn,
|
||||
case "exec":
|
||||
var payload struct{ Value string }
|
||||
if err := gossh.Unmarshal(req.Payload, &payload); err != nil {
|
||||
s.logger.WithError(err).Errorf("Failed to unmarshal channel request payload: %s:%s", req.Type, req.Payload)
|
||||
s.logger.Errorf("Failed to unmarshal channel request payload: %s:%s with error: %s", req.Type, req.Payload, err)
|
||||
}
|
||||
event = payload.Value
|
||||
|
||||
@@ -480,11 +481,11 @@ func (s *SSHProxy) logAuditEvent(conn *gossh.ServerConn, event, eventType string
|
||||
}
|
||||
data, err := json.Marshal(&ae)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to marshal audit event. malformed audit object")
|
||||
s.logger.Errorf("Failed to marshal audit event. malformed audit object: %s", err)
|
||||
return
|
||||
}
|
||||
line := string(data) + "\n"
|
||||
if _, err := writer.Write([]byte(line)); err != nil {
|
||||
s.logger.WithError(err).Error("Failed to write audit event.")
|
||||
s.logger.Errorf("Failed to write audit event: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/sshlog"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const SSHPreambleLength = 2
|
||||
@@ -20,7 +20,7 @@ type SSHPreamble struct {
|
||||
JWT string
|
||||
}
|
||||
|
||||
func New(_ sshlog.Manager, _ *logrus.Logger, _, _, _, _ string, _ chan struct{}, _, _ time.Duration) (*SSHServer, error) {
|
||||
func New(_ sshlog.Manager, _ logger.Service, _, _, _, _ string, _ chan struct{}, _, _ time.Duration) (*SSHServer, error) {
|
||||
return nil, errors.New("cloudflared ssh server is not supported on windows")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package streamhandler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
lbProbeUserAgentPrefix = "Mozilla/5.0 (compatible; Cloudflare-Traffic-Manager/1.0; +https://www.cloudflare.com/traffic-manager/;"
|
||||
)
|
||||
|
||||
func FindCfRayHeader(h1 *http.Request) string {
|
||||
return h1.Header.Get("Cf-Ray")
|
||||
}
|
||||
|
||||
func IsLBProbeRequest(req *http.Request) bool {
|
||||
return strings.HasPrefix(req.UserAgent(), lbProbeUserAgentPrefix)
|
||||
}
|
||||
|
||||
func createRequest(stream *h2mux.MuxedStream, url *url.URL) (*http.Request, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, url.String(), h2mux.MuxedStreamReader{MuxedStream: stream})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unexpected error from http.NewRequest")
|
||||
}
|
||||
err = h2mux.H2RequestHeadersToH1Request(stream.Headers, req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "invalid request received")
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
package streamhandler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/tunnelhostnamemapper"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"zombiezen.com/go/capnproto2/rpc"
|
||||
)
|
||||
|
||||
const (
|
||||
statusPseudoHeader = ":status"
|
||||
)
|
||||
|
||||
type httpErrorStatus struct {
|
||||
status string
|
||||
text []byte
|
||||
}
|
||||
|
||||
var (
|
||||
statusBadRequest = newHTTPErrorStatus(http.StatusBadRequest)
|
||||
statusNotFound = newHTTPErrorStatus(http.StatusNotFound)
|
||||
statusBadGateway = newHTTPErrorStatus(http.StatusBadGateway)
|
||||
)
|
||||
|
||||
func newHTTPErrorStatus(status int) *httpErrorStatus {
|
||||
return &httpErrorStatus{
|
||||
status: strconv.Itoa(status),
|
||||
text: []byte(http.StatusText(status)),
|
||||
}
|
||||
}
|
||||
|
||||
// StreamHandler handles new stream opened by the edge. The streams can be used to proxy requests or make RPC.
|
||||
type StreamHandler struct {
|
||||
// newConfigChan is a send-only channel to notify Supervisor of a new ClientConfig
|
||||
newConfigChan chan<- *pogs.ClientConfig
|
||||
// useConfigResultChan is a receive-only channel for Supervisor to communicate the result of applying a new ClientConfig
|
||||
useConfigResultChan <-chan *pogs.UseConfigurationResult
|
||||
// originMapper maps tunnel hostname to origin service
|
||||
tunnelHostnameMapper *tunnelhostnamemapper.TunnelHostnameMapper
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
// NewStreamHandler creates a new StreamHandler
|
||||
func NewStreamHandler(newConfigChan chan<- *pogs.ClientConfig,
|
||||
useConfigResultChan <-chan *pogs.UseConfigurationResult,
|
||||
logger *logrus.Logger,
|
||||
) *StreamHandler {
|
||||
return &StreamHandler{
|
||||
newConfigChan: newConfigChan,
|
||||
useConfigResultChan: useConfigResultChan,
|
||||
tunnelHostnameMapper: tunnelhostnamemapper.NewTunnelHostnameMapper(),
|
||||
logger: logger.WithField("subsystem", "streamHandler"),
|
||||
}
|
||||
}
|
||||
|
||||
// UseConfiguration implements ClientService
|
||||
func (s *StreamHandler) UseConfiguration(ctx context.Context, config *pogs.ClientConfig) (*pogs.UseConfigurationResult, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
err := fmt.Errorf("Timeout while sending new config to Supervisor")
|
||||
s.logger.Error(err)
|
||||
return nil, err
|
||||
case s.newConfigChan <- config:
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
err := fmt.Errorf("Timeout applying new configuration")
|
||||
s.logger.Error(err)
|
||||
return nil, err
|
||||
case result := <-s.useConfigResultChan:
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateConfig replaces current originmapper mapping with mappings from newConfig
|
||||
func (s *StreamHandler) UpdateConfig(newConfig []*pogs.ReverseProxyConfig) (failedConfigs []*pogs.FailedConfig) {
|
||||
|
||||
// Delete old configs that aren't in the `newConfig`
|
||||
toRemove := s.tunnelHostnameMapper.ToRemove(newConfig)
|
||||
for _, hostnameToRemove := range toRemove {
|
||||
s.tunnelHostnameMapper.Delete(hostnameToRemove)
|
||||
}
|
||||
|
||||
// Add new configs that weren't in the old mapper
|
||||
toAdd := s.tunnelHostnameMapper.ToAdd(newConfig)
|
||||
for _, tunnelConfig := range toAdd {
|
||||
tunnelHostname := tunnelConfig.TunnelHostname
|
||||
originSerice, err := tunnelConfig.OriginConfig.Service()
|
||||
if err != nil {
|
||||
s.logger.WithField("tunnelHostname", tunnelHostname).WithError(err).Error("Invalid origin service config")
|
||||
failedConfigs = append(failedConfigs, &pogs.FailedConfig{
|
||||
Config: tunnelConfig,
|
||||
Reason: tunnelConfig.FailReason(err),
|
||||
})
|
||||
continue
|
||||
}
|
||||
s.tunnelHostnameMapper.Add(tunnelConfig.TunnelHostname, originSerice)
|
||||
s.logger.WithField("tunnelHostname", tunnelHostname).Infof("New origin service config: %v", originSerice.Summary())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ServeStream implements MuxedStreamHandler interface
|
||||
func (s *StreamHandler) ServeStream(stream *h2mux.MuxedStream) error {
|
||||
if stream.IsRPCStream() {
|
||||
return s.serveRPC(stream)
|
||||
}
|
||||
if err := s.serveRequest(stream); err != nil {
|
||||
s.logger.Error(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StreamHandler) serveRPC(stream *h2mux.MuxedStream) error {
|
||||
stream.WriteHeaders([]h2mux.Header{{Name: ":status", Value: "200"}})
|
||||
main := pogs.ClientService_ServerToClient(s)
|
||||
rpcLogger := s.logger.WithField("subsystem", "clientserver-rpc")
|
||||
rpcConn := rpc.NewConn(
|
||||
tunnelrpc.NewTransportLogger(rpcLogger, rpc.StreamTransport(stream)),
|
||||
rpc.MainInterface(main.Client),
|
||||
tunnelrpc.ConnLog(s.logger.WithField("subsystem", "clientserver-rpc-transport")),
|
||||
)
|
||||
return rpcConn.Wait()
|
||||
}
|
||||
|
||||
func (s *StreamHandler) serveRequest(stream *h2mux.MuxedStream) error {
|
||||
tunnelHostname := stream.TunnelHostname()
|
||||
if !tunnelHostname.IsSet() {
|
||||
s.writeErrorStatus(stream, statusBadRequest)
|
||||
return fmt.Errorf("stream doesn't have tunnelHostname")
|
||||
}
|
||||
|
||||
originService, ok := s.tunnelHostnameMapper.Get(tunnelHostname)
|
||||
if !ok {
|
||||
s.writeErrorStatus(stream, statusNotFound)
|
||||
return fmt.Errorf("cannot map tunnel hostname %s to origin", tunnelHostname)
|
||||
}
|
||||
|
||||
req, err := createRequest(stream, originService.URL())
|
||||
if err != nil {
|
||||
s.writeErrorStatus(stream, statusBadRequest)
|
||||
return errors.Wrap(err, "cannot create request")
|
||||
}
|
||||
|
||||
logger := s.requestLogger(req, tunnelHostname)
|
||||
logger.Debugf("Request Headers %+v", req.Header)
|
||||
|
||||
resp, err := originService.Proxy(stream, req)
|
||||
if err != nil {
|
||||
s.writeErrorStatus(stream, statusBadGateway)
|
||||
return errors.Wrap(err, "cannot proxy request")
|
||||
}
|
||||
|
||||
logger.WithField("status", resp.Status).Debugf("Response Headers %+v", resp.Header)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StreamHandler) requestLogger(req *http.Request, tunnelHostname h2mux.TunnelHostname) *logrus.Entry {
|
||||
cfRay := FindCfRayHeader(req)
|
||||
lbProbe := IsLBProbeRequest(req)
|
||||
logger := s.logger.WithField("tunnelHostname", tunnelHostname)
|
||||
if cfRay != "" {
|
||||
logger = logger.WithField("CF-RAY", cfRay)
|
||||
logger.Debugf("%s %s %s", req.Method, req.URL, req.Proto)
|
||||
} else if lbProbe {
|
||||
logger.Debugf("Load Balancer health check %s %s %s", req.Method, req.URL, req.Proto)
|
||||
} else {
|
||||
logger.Warnf("Requests %v does not have CF-RAY header. Please open a support ticket with Cloudflare.", req)
|
||||
}
|
||||
return logger
|
||||
}
|
||||
|
||||
func (s *StreamHandler) writeErrorStatus(stream *h2mux.MuxedStream, status *httpErrorStatus) {
|
||||
_ = stream.WriteHeaders([]h2mux.Header{
|
||||
{
|
||||
Name: statusPseudoHeader,
|
||||
Value: status.status,
|
||||
},
|
||||
h2mux.CreateResponseMetaHeader(h2mux.ResponseMetaHeaderField, h2mux.ResponseSourceCloudflared),
|
||||
})
|
||||
_, _ = stream.Write(status.text)
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
package streamhandler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const (
|
||||
testOpenStreamTimeout = time.Millisecond * 5000
|
||||
testHandshakeTimeout = time.Millisecond * 1000
|
||||
)
|
||||
|
||||
var (
|
||||
testTunnelHostname = h2mux.TunnelHostname("123.cftunnel.com")
|
||||
baseHeaders = []h2mux.Header{
|
||||
{Name: ":method", Value: "GET"},
|
||||
{Name: ":scheme", Value: "http"},
|
||||
{Name: ":authority", Value: "example.com"},
|
||||
{Name: ":path", Value: "/"},
|
||||
|
||||
// Regular headers must always come after the pseudoheaders
|
||||
{Name: h2mux.RequestUserHeadersField, Value: ""},
|
||||
}
|
||||
tunnelHostnameHeader = h2mux.Header{Name: h2mux.CloudflaredProxyTunnelHostnameHeader, Value: testTunnelHostname.String()}
|
||||
)
|
||||
|
||||
func TestServeRequest(t *testing.T) {
|
||||
configChan := make(chan *pogs.ClientConfig)
|
||||
useConfigResultChan := make(chan *pogs.UseConfigurationResult)
|
||||
streamHandler := NewStreamHandler(configChan, useConfigResultChan, logrus.New())
|
||||
|
||||
message := []byte("Hello cloudflared")
|
||||
httpServer := httptest.NewServer(&mockHTTPHandler{message})
|
||||
|
||||
reverseProxyConfigs := []*pogs.ReverseProxyConfig{
|
||||
{
|
||||
TunnelHostname: testTunnelHostname,
|
||||
OriginConfig: &pogs.HTTPOriginConfig{
|
||||
URLString: httpServer.URL,
|
||||
},
|
||||
},
|
||||
}
|
||||
streamHandler.UpdateConfig(reverseProxyConfigs)
|
||||
|
||||
muxPair := NewDefaultMuxerPair(t, streamHandler)
|
||||
muxPair.Serve(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testOpenStreamTimeout)
|
||||
defer cancel()
|
||||
|
||||
headers := append(baseHeaders, tunnelHostnameHeader)
|
||||
stream, err := muxPair.EdgeMux.OpenStream(ctx, headers, nil)
|
||||
assert.NoError(t, err)
|
||||
assertStatusHeader(t, http.StatusOK, stream.Headers)
|
||||
assertRespBody(t, message, stream)
|
||||
}
|
||||
|
||||
func createStreamHandler() *StreamHandler {
|
||||
configChan := make(chan *pogs.ClientConfig)
|
||||
useConfigResultChan := make(chan *pogs.UseConfigurationResult)
|
||||
|
||||
return NewStreamHandler(configChan, useConfigResultChan, logrus.New())
|
||||
}
|
||||
|
||||
func createRequestMuxPair(t *testing.T, streamHandler *StreamHandler) *DefaultMuxerPair {
|
||||
muxPair := NewDefaultMuxerPair(t, streamHandler)
|
||||
muxPair.Serve(t)
|
||||
|
||||
return muxPair
|
||||
}
|
||||
|
||||
func TestServeStatusBadRequest(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testOpenStreamTimeout)
|
||||
defer cancel()
|
||||
|
||||
// No tunnel hostname header, expect to get 400 Bad Request
|
||||
stream, err := createRequestMuxPair(t, createStreamHandler()).EdgeMux.OpenStream(ctx, baseHeaders, nil)
|
||||
assert.NoError(t, err)
|
||||
assertStatusHeader(t, http.StatusBadRequest, stream.Headers)
|
||||
assertRespBody(t, statusBadRequest.text, stream)
|
||||
}
|
||||
|
||||
func TestServeInvalidContentLength(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testOpenStreamTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Invalid content-length, wouldn't be able to create a request
|
||||
// Expect to get 400 Bad Request
|
||||
headers := append(baseHeaders, tunnelHostnameHeader)
|
||||
headers = append(headers, h2mux.Header{
|
||||
Name: "content-length",
|
||||
Value: "x",
|
||||
})
|
||||
streamHandler := createStreamHandler()
|
||||
streamHandler.UpdateConfig([]*pogs.ReverseProxyConfig{
|
||||
{
|
||||
TunnelHostname: testTunnelHostname,
|
||||
OriginConfig: &pogs.HTTPOriginConfig{
|
||||
URLString: "",
|
||||
},
|
||||
},
|
||||
})
|
||||
mux := createRequestMuxPair(t, streamHandler).EdgeMux
|
||||
stream, err := mux.OpenStream(ctx, headers, nil)
|
||||
assert.NoError(t, err)
|
||||
assertStatusHeader(t, http.StatusBadRequest, stream.Headers)
|
||||
assertRespBody(t, statusBadRequest.text, stream)
|
||||
}
|
||||
|
||||
func TestServeStatusNotFound(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testOpenStreamTimeout)
|
||||
defer cancel()
|
||||
|
||||
// No mapping for the tunnel hostname, expect to get 404 Not Found
|
||||
headers := append(baseHeaders, tunnelHostnameHeader)
|
||||
stream, err := createRequestMuxPair(t, createStreamHandler()).EdgeMux.OpenStream(ctx, headers, nil)
|
||||
assert.NoError(t, err)
|
||||
assertStatusHeader(t, http.StatusNotFound, stream.Headers)
|
||||
assertRespBody(t, statusNotFound.text, stream)
|
||||
}
|
||||
|
||||
func TestServeStatusBadGateway(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testOpenStreamTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Nothing listening on empty url, so proxy would fail. Expect to get 502 Bad Gateway
|
||||
reverseProxyConfigs := []*pogs.ReverseProxyConfig{
|
||||
{
|
||||
TunnelHostname: testTunnelHostname,
|
||||
OriginConfig: &pogs.HTTPOriginConfig{
|
||||
URLString: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
streamHandler := createStreamHandler()
|
||||
streamHandler.UpdateConfig(reverseProxyConfigs)
|
||||
headers := append(baseHeaders, tunnelHostnameHeader)
|
||||
stream, err := createRequestMuxPair(t, streamHandler).EdgeMux.OpenStream(ctx, headers, nil)
|
||||
assert.NoError(t, err)
|
||||
assertStatusHeader(t, http.StatusBadGateway, stream.Headers)
|
||||
assertRespBody(t, statusBadGateway.text, stream)
|
||||
}
|
||||
|
||||
func assertStatusHeader(t *testing.T, expectedStatus int, headers []h2mux.Header) {
|
||||
assert.Equal(t, statusPseudoHeader, headers[0].Name)
|
||||
assert.Equal(t, strconv.Itoa(expectedStatus), headers[0].Value)
|
||||
}
|
||||
|
||||
func assertRespBody(t *testing.T, expectedRespBody []byte, stream *h2mux.MuxedStream) {
|
||||
respBody := make([]byte, len(expectedRespBody))
|
||||
_, err := stream.Read(respBody)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedRespBody, respBody)
|
||||
}
|
||||
|
||||
type DefaultMuxerPair struct {
|
||||
OriginMuxConfig h2mux.MuxerConfig
|
||||
OriginMux *h2mux.Muxer
|
||||
OriginConn net.Conn
|
||||
EdgeMuxConfig h2mux.MuxerConfig
|
||||
EdgeMux *h2mux.Muxer
|
||||
EdgeConn net.Conn
|
||||
doneC chan struct{}
|
||||
}
|
||||
|
||||
func NewDefaultMuxerPair(t *testing.T, h h2mux.MuxedStreamHandler) *DefaultMuxerPair {
|
||||
origin, edge := net.Pipe()
|
||||
p := &DefaultMuxerPair{
|
||||
OriginMuxConfig: h2mux.MuxerConfig{
|
||||
Timeout: testHandshakeTimeout,
|
||||
Handler: h,
|
||||
IsClient: true,
|
||||
Name: "origin",
|
||||
Logger: logrus.NewEntry(logrus.New()),
|
||||
DefaultWindowSize: (1 << 8) - 1,
|
||||
MaxWindowSize: (1 << 15) - 1,
|
||||
StreamWriteBufferMaxLen: 1024,
|
||||
},
|
||||
OriginConn: origin,
|
||||
EdgeMuxConfig: h2mux.MuxerConfig{
|
||||
Timeout: testHandshakeTimeout,
|
||||
IsClient: false,
|
||||
Name: "edge",
|
||||
Logger: logrus.NewEntry(logrus.New()),
|
||||
DefaultWindowSize: (1 << 8) - 1,
|
||||
MaxWindowSize: (1 << 15) - 1,
|
||||
StreamWriteBufferMaxLen: 1024,
|
||||
},
|
||||
EdgeConn: edge,
|
||||
doneC: make(chan struct{}),
|
||||
}
|
||||
assert.NoError(t, p.Handshake(t.Name()))
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *DefaultMuxerPair) Handshake(testName string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testHandshakeTimeout)
|
||||
defer cancel()
|
||||
errGroup, _ := errgroup.WithContext(ctx)
|
||||
errGroup.Go(func() (err error) {
|
||||
p.EdgeMux, err = h2mux.Handshake(p.EdgeConn, p.EdgeConn, p.EdgeMuxConfig, h2mux.NewActiveStreamsMetrics(testName, "edge"))
|
||||
return errors.Wrap(err, "edge handshake failure")
|
||||
})
|
||||
errGroup.Go(func() (err error) {
|
||||
p.OriginMux, err = h2mux.Handshake(p.OriginConn, p.OriginConn, p.OriginMuxConfig, h2mux.NewActiveStreamsMetrics(testName, "origin"))
|
||||
return errors.Wrap(err, "origin handshake failure")
|
||||
})
|
||||
|
||||
return errGroup.Wait()
|
||||
}
|
||||
|
||||
func (p *DefaultMuxerPair) Serve(t assert.TestingT) {
|
||||
ctx := context.Background()
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
err := p.EdgeMux.Serve(ctx)
|
||||
if err != nil && err != io.EOF && err != io.ErrClosedPipe {
|
||||
t.Errorf("error in edge muxer Serve(): %s", err)
|
||||
}
|
||||
p.OriginMux.Shutdown()
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
err := p.OriginMux.Serve(ctx)
|
||||
if err != nil && err != io.EOF && err != io.ErrClosedPipe {
|
||||
t.Errorf("error in origin muxer Serve(): %s", err)
|
||||
}
|
||||
p.EdgeMux.Shutdown()
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
// notify when both muxes have stopped serving
|
||||
wg.Wait()
|
||||
close(p.doneC)
|
||||
}()
|
||||
}
|
||||
|
||||
type mockHTTPHandler struct {
|
||||
message []byte
|
||||
}
|
||||
|
||||
func (mth *mockHTTPHandler) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write(mth.message)
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
// Package supervisor is used by declarative tunnels to get/apply new config from the edge.
|
||||
package supervisor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/streamhandler"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Supervisor struct {
|
||||
connManager *connection.EdgeManager
|
||||
streamHandler *streamhandler.StreamHandler
|
||||
autoupdater *updater.AutoUpdater
|
||||
supportAutoupdate bool
|
||||
newConfigChan <-chan *pogs.ClientConfig
|
||||
useConfigResultChan chan<- *pogs.UseConfigurationResult
|
||||
state *state
|
||||
logger *logrus.Entry
|
||||
metrics metrics
|
||||
}
|
||||
|
||||
type metrics struct {
|
||||
configVersion prometheus.Gauge
|
||||
}
|
||||
|
||||
func newMetrics() metrics {
|
||||
configVersion := prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "supervisor",
|
||||
Subsystem: "supervisor",
|
||||
Name: "config_version",
|
||||
Help: "Latest configuration version received from Cloudflare",
|
||||
},
|
||||
)
|
||||
prometheus.MustRegister(
|
||||
configVersion,
|
||||
)
|
||||
return metrics{
|
||||
configVersion: configVersion,
|
||||
}
|
||||
}
|
||||
|
||||
func NewSupervisor(
|
||||
defaultClientConfig *pogs.ClientConfig,
|
||||
userCredential []byte,
|
||||
tlsConfig *tls.Config,
|
||||
serviceDiscoverer *edgediscovery.Edge,
|
||||
cloudflaredConfig *connection.CloudflaredConfig,
|
||||
autoupdater *updater.AutoUpdater,
|
||||
supportAutoupdate bool,
|
||||
logger *logrus.Logger,
|
||||
) (*Supervisor, error) {
|
||||
newConfigChan := make(chan *pogs.ClientConfig)
|
||||
useConfigResultChan := make(chan *pogs.UseConfigurationResult)
|
||||
streamHandler := streamhandler.NewStreamHandler(newConfigChan, useConfigResultChan, logger)
|
||||
invalidConfigs := streamHandler.UpdateConfig(defaultClientConfig.ReverseProxyConfigs)
|
||||
|
||||
if len(invalidConfigs) > 0 {
|
||||
for _, invalidConfig := range invalidConfigs {
|
||||
logger.Errorf("Tunnel %+v is invalid, reason: %s", invalidConfig.Config, invalidConfig.Reason)
|
||||
}
|
||||
return nil, fmt.Errorf("At least 1 Tunnel config is invalid")
|
||||
}
|
||||
|
||||
tunnelHostnames := make([]h2mux.TunnelHostname, len(defaultClientConfig.ReverseProxyConfigs))
|
||||
for i, reverseProxyConfig := range defaultClientConfig.ReverseProxyConfigs {
|
||||
tunnelHostnames[i] = reverseProxyConfig.TunnelHostname
|
||||
}
|
||||
defaultEdgeMgrConfigurable := &connection.EdgeManagerConfigurable{
|
||||
TunnelHostnames: tunnelHostnames,
|
||||
EdgeConnectionConfig: defaultClientConfig.EdgeConnectionConfig,
|
||||
}
|
||||
return &Supervisor{
|
||||
connManager: connection.NewEdgeManager(streamHandler, defaultEdgeMgrConfigurable, userCredential, tlsConfig,
|
||||
serviceDiscoverer, cloudflaredConfig, logger),
|
||||
streamHandler: streamHandler,
|
||||
autoupdater: autoupdater,
|
||||
supportAutoupdate: supportAutoupdate,
|
||||
newConfigChan: newConfigChan,
|
||||
useConfigResultChan: useConfigResultChan,
|
||||
state: newState(defaultClientConfig),
|
||||
logger: logger.WithField("subsystem", "supervisor"),
|
||||
metrics: newMetrics(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Supervisor) Run(ctx context.Context) error {
|
||||
errGroup, groupCtx := errgroup.WithContext(ctx)
|
||||
|
||||
errGroup.Go(func() error {
|
||||
return s.connManager.Run(groupCtx)
|
||||
})
|
||||
|
||||
errGroup.Go(func() error {
|
||||
return s.listenToNewConfig(groupCtx)
|
||||
})
|
||||
|
||||
errGroup.Go(func() error {
|
||||
return s.listenToShutdownSignal(groupCtx)
|
||||
})
|
||||
|
||||
if s.supportAutoupdate {
|
||||
errGroup.Go(func() error {
|
||||
return s.autoupdater.Run(groupCtx)
|
||||
})
|
||||
}
|
||||
|
||||
err := errGroup.Wait()
|
||||
s.logger.Warnf("Supervisor terminated, reason: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Supervisor) listenToShutdownSignal(serveCtx context.Context) error {
|
||||
signals := make(chan os.Signal, 10)
|
||||
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
|
||||
defer signal.Stop(signals)
|
||||
|
||||
select {
|
||||
case <-serveCtx.Done():
|
||||
return serveCtx.Err()
|
||||
case sig := <-signals:
|
||||
return fmt.Errorf("received %v signal", sig)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Supervisor) listenToNewConfig(ctx context.Context) error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case newConfig := <-s.newConfigChan:
|
||||
s.useConfigResultChan <- s.notifySubsystemsNewConfig(newConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Supervisor) notifySubsystemsNewConfig(newConfig *pogs.ClientConfig) *pogs.UseConfigurationResult {
|
||||
s.logger.Infof("Received configuration %v", newConfig.Version)
|
||||
if s.state.hasAppliedVersion(newConfig.Version) {
|
||||
s.logger.Infof("%v has been applied", newConfig.Version)
|
||||
return &pogs.UseConfigurationResult{
|
||||
Success: true,
|
||||
}
|
||||
}
|
||||
s.metrics.configVersion.Set(float64(newConfig.Version))
|
||||
|
||||
s.state.updateConfig(newConfig)
|
||||
var tunnelHostnames []h2mux.TunnelHostname
|
||||
for _, tunnelConfig := range newConfig.ReverseProxyConfigs {
|
||||
tunnelHostnames = append(tunnelHostnames, tunnelConfig.TunnelHostname)
|
||||
}
|
||||
// Update connManager configurable
|
||||
s.connManager.UpdateConfigurable(&connection.EdgeManagerConfigurable{
|
||||
TunnelHostnames: tunnelHostnames,
|
||||
EdgeConnectionConfig: newConfig.EdgeConnectionConfig,
|
||||
})
|
||||
// Update streamHandler tunnelHostnameMapper mapping
|
||||
failedConfigs := s.streamHandler.UpdateConfig(newConfig.ReverseProxyConfigs)
|
||||
|
||||
if s.supportAutoupdate {
|
||||
s.autoupdater.Update(newConfig.SupervisorConfig.AutoUpdateFrequency)
|
||||
}
|
||||
|
||||
return &pogs.UseConfigurationResult{
|
||||
Success: len(failedConfigs) == 0,
|
||||
FailedConfigs: failedConfigs,
|
||||
}
|
||||
}
|
||||
|
||||
type state struct {
|
||||
sync.RWMutex
|
||||
currentConfig *pogs.ClientConfig
|
||||
}
|
||||
|
||||
func newState(currentConfig *pogs.ClientConfig) *state {
|
||||
return &state{
|
||||
currentConfig: currentConfig,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *state) hasAppliedVersion(incomingVersion pogs.Version) bool {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
return s.currentConfig.Version.IsNewerOrEqual(incomingVersion)
|
||||
}
|
||||
|
||||
func (s *state) updateConfig(newConfig *pogs.ClientConfig) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
s.currentConfig = newConfig
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user