mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-08-07 23:31:56 +00:00
Compare commits
253 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e2b43adb5 | |||
| c7dca16300 | |||
| 9d5bd256be | |||
| e9c2afec56 | |||
| 4e33281337 | |||
| ac559f86c9 | |||
| 117766562b | |||
| 5d76e940c7 | |||
| 1670ee87fb | |||
| a8ae6de213 | |||
| d7c4a89106 | |||
| e7354f4768 | |||
| 18ec338d4c | |||
| 7f97e2f030 | |||
| a278753bbf | |||
| 3004703074 | |||
| 67680f5536 | |||
| d8bee0b4d9 | |||
| a4f185fd28 | |||
| cf562ef8c8 | |||
| 820e0dfe51 | |||
| 0b16a473da | |||
| dbd90f270e | |||
| 7420439ed2 | |||
| 352207e933 | |||
| 5eb2874a13 | |||
| dca77ee13e | |||
| 0d22106416 | |||
| 88b53eb886 | |||
| b72ee69eef | |||
| 8a020d12e1 | |||
| a945518404 | |||
| 6cdd20e820 | |||
| d87bfcbe55 | |||
| 0df4f7dd24 | |||
| 7df3a1ab67 | |||
| c4fbb05c1b | |||
| d503aeaf77 | |||
| db0562c7b8 | |||
| ce22dd681a | |||
| a129572749 | |||
| 2d0b86f2e4 | |||
| ffac598fab | |||
| 4a76ed12e7 | |||
| 04b1e4f859 | |||
| 8c9d725eeb | |||
| de27361ffa | |||
| 8da61274b8 | |||
| 146c2d944a | |||
| d90a111c1d | |||
| d26df1c248 | |||
| 42cdb557a0 | |||
| 7c3ceeeaef | |||
| 391facbedf | |||
| 1c9f3ac7d4 | |||
| 6852047ef1 | |||
| a2109e4a78 | |||
| 01f0d67875 | |||
| 33d9b4027b | |||
| 9ed536c990 | |||
| 119d96cdc1 | |||
| 94ca4f98dd | |||
| b601b24f52 | |||
| b40d8557cf | |||
| a34604cfc7 | |||
| 1c1a8f21b2 | |||
| 78ffb1b846 | |||
| 92fd039440 | |||
| 55bf904689 | |||
| abab78730d | |||
| b855e33327 | |||
| 32336859f8 | |||
| 94c639d225 | |||
| 2ea491b1d0 | |||
| 9bc1c0c70b | |||
| 870f5fa907 | |||
| 06404bf3e8 | |||
| 3b4ed70cf3 | |||
| d45ca67498 | |||
| 1c0dac77d7 | |||
| 65e70650dc | |||
| 38fb0b28b6 | |||
| bda8fe2fbe | |||
| 6a8c7c0727 | |||
| 69fd502db3 | |||
| fcc393e2f0 | |||
| cad58b9b57 | |||
| 78cb60b85f | |||
| a08a7030d1 | |||
| 87203bbe25 | |||
| 11acb50cf7 | |||
| 23f2a04ed7 | |||
| 1805261263 | |||
| 53de779a0a | |||
| b7e91466f5 | |||
| 4c1b89576c | |||
| a1a554a29d | |||
| fdb1f961b3 | |||
| 293b9af4a7 | |||
| 029f7e0378 | |||
| 58c5e25b9a | |||
| 25e72f7760 | |||
| 7613410855 | |||
| c40cb7dc56 | |||
| 9ae5f306bf | |||
| 6159cb536f | |||
| ae4f687754 | |||
| ce7d0572fe | |||
| 030b768eeb | |||
| f36dc6cfd8 | |||
| 6dad2bf9c4 | |||
| 1475cf61ee | |||
| ebc003d478 | |||
| 543169c893 | |||
| d5769519b2 | |||
| 5974fb4cfd | |||
| eef5b78eac | |||
| 6b86f81c4a | |||
| a490443630 | |||
| b5cdf3b2c7 | |||
| 6886e5f90a | |||
| 9ac40dcf04 | |||
| a5a5b93b64 | |||
| cb39f26f27 | |||
| 8d7b2575ba | |||
| d7498b0c03 | |||
| 59051ba425 | |||
| d7268af555 | |||
| 8c1deb4064 | |||
| 986102401b | |||
| 196762d9d3 | |||
| 350a6f2bf5 | |||
| 3de3b2f734 | |||
| 4698ec8dee | |||
| 8c6181db9f | |||
| 64d3836645 | |||
| 87e2679744 | |||
| 61c814bd79 | |||
| be9a558867 | |||
| b00bfe8e0b | |||
| bc015995d8 | |||
| d01770107e | |||
| 18c359cb86 | |||
| e933ef9e1a | |||
| ea71b78e6d | |||
| 04fa10ea7b | |||
| 8e0be7cff1 | |||
| 3c13080fe1 | |||
| b6cd54d854 | |||
| f0cfad8efa | |||
| ed54d150fe | |||
| ca4887fb19 | |||
| eaf03305bd | |||
| 051908aaef | |||
| acd03e36e6 | |||
| c96b9e8d8f | |||
| 4a4a1bb6b1 | |||
| 0eebc7cef9 | |||
| 53a1fa46a8 | |||
| 86a7af3dc4 | |||
| 407c9550d7 | |||
| 2319003e10 | |||
| 1e6399c2f0 | |||
| b05d826d22 | |||
| d21989dba4 | |||
| a986355a61 | |||
| 812244d79f | |||
| be7b7c7149 | |||
| 03d7320a44 | |||
| ba4c8d8849 | |||
| 2c9b7361b7 | |||
| 8e8513e325 | |||
| 607dcff697 | |||
| 1d1cf6a827 | |||
| 61f3fab757 | |||
| e2ff7f65fc | |||
| da0792a321 | |||
| fa061ab54e | |||
| 197d65659a | |||
| 85d0afd3b0 | |||
| 20623255dd | |||
| afa5e68fe5 | |||
| 747427f816 | |||
| f99b6c6421 | |||
| 250bc54110 | |||
| bb530b87dd | |||
| 02587c1edc | |||
| 26fc20d406 | |||
| 8a829b773a | |||
| 71b98e6111 | |||
| fee13dc62f | |||
| 094e0c7592 | |||
| b57a953caa | |||
| d8ebde37ca | |||
| 0708a49848 | |||
| b26f3082e6 | |||
| cb6d424765 | |||
| 5753aa9f18 | |||
| bfae12008d | |||
| c52e0dc8ef | |||
| 55346444c9 | |||
| ba785ec58d | |||
| 3be2545ad4 | |||
| 4a8597c245 | |||
| 741cd66c9e | |||
| 7acea1ac99 | |||
| 22d771b51d | |||
| 00d6ab2eb7 | |||
| cc0a5ac3df | |||
| 5fb938d6d6 | |||
| cd5bdb837e | |||
| 107abf9d29 | |||
| 218ee30206 | |||
| 8764fbfdfa | |||
| 2463d6b92c | |||
| 810d268c99 | |||
| f5b479dbbc | |||
| b52728e829 | |||
| 8eeb452cce | |||
| 9323844ea7 | |||
| b698fe5ef3 | |||
| edb980d439 | |||
| 310bd0dbf1 | |||
| 1a96889141 | |||
| 60de05bfc1 | |||
| 3deef6197f | |||
| 70114c2145 | |||
| 5499c77e62 | |||
| 292a7f07a2 | |||
| 679f36303a | |||
| 1b61d699c4 | |||
| a7562dff68 | |||
| 8daf1e882f | |||
| 20466dacb7 | |||
| 5b3b592108 | |||
| 9952ce0069 | |||
| 1cbc8fb8ac | |||
| edc69694cb | |||
| bd15c6b8c3 | |||
| 42fe2e7266 | |||
| 44e3be2c88 | |||
| 61d5461138 | |||
| b696ca8b1c | |||
| 6a4d020c27 | |||
| 4791ba3b87 | |||
| cf1c9a3083 | |||
| d61e3fb130 | |||
| ca7d6797e1 | |||
| af0d04d0f3 | |||
| 6274567e16 | |||
| 8836ee1dda | |||
| 7afde79600 | |||
| 3d782f7162 |
@@ -0,0 +1 @@
|
||||
.git
|
||||
@@ -5,13 +5,17 @@ guide/public
|
||||
/.GOPATH
|
||||
/bin
|
||||
.idea
|
||||
.build
|
||||
.vscode
|
||||
\#*\#
|
||||
cscope.*
|
||||
cloudflared
|
||||
cloudflared.pkg
|
||||
cloudflared.exe
|
||||
cloudflared.msi
|
||||
cloudflared-x86-64*
|
||||
!cmd/cloudflared/
|
||||
.DS_Store
|
||||
*-session.log
|
||||
ssh_server_tests/.env
|
||||
.cover
|
||||
|
||||
Vendored
+169
-3
@@ -1,7 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "$(uname)" != "Darwin" ]] ; then
|
||||
echo "This should be run on macOS"
|
||||
exit 1
|
||||
@@ -13,9 +11,177 @@ export GO111MODULE=on
|
||||
# build 'cloudflared-darwin-amd64.tgz'
|
||||
mkdir -p artifacts
|
||||
FILENAME="$(pwd)/artifacts/cloudflared-darwin-amd64.tgz"
|
||||
PKGNAME="$(pwd)/artifacts/cloudflared-amd64.pkg"
|
||||
TARGET_DIRECTORY=".build"
|
||||
BINARY_NAME="cloudflared"
|
||||
VERSION=$(git describe --tags --always --dirty="-dev")
|
||||
PRODUCT="cloudflared"
|
||||
CODE_SIGN_PRIV="code_sign.p12"
|
||||
CODE_SIGN_CERT="code_sign.cer"
|
||||
INSTALLER_PRIV="installer.p12"
|
||||
INSTALLER_CERT="installer.cer"
|
||||
BUNDLE_ID="com.cloudflare.cloudflared"
|
||||
SEC_DUP_MSG="security: SecKeychainItemImport: The specified item already exists in the keychain."
|
||||
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
|
||||
|
||||
# Add code signing private key to the key chain
|
||||
if [[ ! -z "$CFD_CODE_SIGN_KEY" ]]; then
|
||||
if [[ ! -z "$CFD_CODE_SIGN_PASS" ]]; then
|
||||
# write private key to disk and then import it keychain
|
||||
echo -n -e ${CFD_CODE_SIGN_KEY} | base64 -D > ${CODE_SIGN_PRIV}
|
||||
out=$(security import ${CODE_SIGN_PRIV} -A -P "${CFD_CODE_SIGN_PASS}" 2>&1)
|
||||
exitcode=$?
|
||||
if [ -n "$out" ]; then
|
||||
if [ $exitcode -eq 0 ]; then
|
||||
echo "$out"
|
||||
else
|
||||
if [ "$out" != "${SEC_DUP_MSG}" ]; then
|
||||
echo "$out" >&2
|
||||
exit $exitcode
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
rm ${CODE_SIGN_PRIV}
|
||||
fi
|
||||
fi
|
||||
|
||||
# Add code signing certificate to the key chain
|
||||
if [[ ! -z "$CFD_CODE_SIGN_CERT" ]]; then
|
||||
# write certificate to disk and then import it keychain
|
||||
echo -n -e ${CFD_CODE_SIGN_CERT} | base64 -D > ${CODE_SIGN_CERT}
|
||||
out1=$(security import ${CODE_SIGN_CERT} -A 2>&1)
|
||||
exitcode1=$?
|
||||
if [ -n "$out1" ]; then
|
||||
if [ $exitcode1 -eq 0 ]; then
|
||||
echo "$out1"
|
||||
else
|
||||
if [ "$out1" != "${SEC_DUP_MSG}" ]; then
|
||||
echo "$out1" >&2
|
||||
exit $exitcode1
|
||||
else
|
||||
echo "already imported code signing certificate"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
rm ${CODE_SIGN_CERT}
|
||||
fi
|
||||
|
||||
# Add package signing private key to the key chain
|
||||
if [[ ! -z "$CFD_INSTALLER_KEY" ]]; then
|
||||
if [[ ! -z "$CFD_INSTALLER_PASS" ]]; then
|
||||
# write private key to disk and then import it into the keychain
|
||||
echo -n -e ${CFD_INSTALLER_KEY} | base64 -D > ${INSTALLER_PRIV}
|
||||
out2=$(security import ${INSTALLER_PRIV} -A -P "${CFD_INSTALLER_PASS}" 2>&1)
|
||||
exitcode2=$?
|
||||
if [ -n "$out2" ]; then
|
||||
if [ $exitcode2 -eq 0 ]; then
|
||||
echo "$out2"
|
||||
else
|
||||
if [ "$out2" != "${SEC_DUP_MSG}" ]; then
|
||||
echo "$out2" >&2
|
||||
exit $exitcode2
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
rm ${INSTALLER_PRIV}
|
||||
fi
|
||||
fi
|
||||
|
||||
# Add package signing certificate to the key chain
|
||||
if [[ ! -z "$CFD_INSTALLER_CERT" ]]; then
|
||||
# write certificate to disk and then import it keychain
|
||||
echo -n -e ${CFD_INSTALLER_CERT} | base64 -D > ${INSTALLER_CERT}
|
||||
out3=$(security import ${INSTALLER_CERT} -A 2>&1)
|
||||
exitcode3=$?
|
||||
if [ -n "$out3" ]; then
|
||||
if [ $exitcode3 -eq 0 ]; then
|
||||
echo "$out3"
|
||||
else
|
||||
if [ "$out3" != "${SEC_DUP_MSG}" ]; then
|
||||
echo "$out3" >&2
|
||||
exit $exitcode3
|
||||
else
|
||||
echo "already imported installer certificate"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
rm ${INSTALLER_CERT}
|
||||
fi
|
||||
|
||||
# get the code signing certificate name
|
||||
if [[ ! -z "$CFD_CODE_SIGN_NAME" ]]; then
|
||||
CODE_SIGN_NAME="${CFD_CODE_SIGN_NAME}"
|
||||
else
|
||||
if [[ -n "$(security find-certificate -c "Developer ID Application" | cut -d'"' -f 4 -s | grep "Developer ID Application:" | head -1)" ]]; then
|
||||
CODE_SIGN_NAME=$(security find-certificate -c "Developer ID Application" | cut -d'"' -f 4 -s | grep "Developer ID Application:" | head -1)
|
||||
else
|
||||
CODE_SIGN_NAME=""
|
||||
fi
|
||||
fi
|
||||
|
||||
# get the package signing certificate name
|
||||
if [[ ! -z "$CFD_INSTALLER_NAME" ]]; then
|
||||
PKG_SIGN_NAME="${CFD_INSTALLER_NAME}"
|
||||
else
|
||||
if [[ -n "$(security find-certificate -c "Developer ID Installer" | cut -d'"' -f 4 -s | grep "Developer ID Installer:" | head -1)" ]]; then
|
||||
PKG_SIGN_NAME=$(security find-certificate -c "Developer ID Installer" | cut -d'"' -f 4 -s | grep "Developer ID Installer:" | head -1)
|
||||
else
|
||||
PKG_SIGN_NAME=""
|
||||
fi
|
||||
fi
|
||||
|
||||
# sign the cloudflared binary
|
||||
if [[ ! -z "$CODE_SIGN_NAME" ]]; then
|
||||
codesign -s "${CODE_SIGN_NAME}" -f -v --timestamp --options runtime ${BINARY_NAME}
|
||||
|
||||
# notarize the binary
|
||||
if [[ ! -z "$CFD_NOTE_PASSWORD" ]]; then
|
||||
zip "${BINARY_NAME}.zip" ${BINARY_NAME}
|
||||
xcrun altool --notarize-app -f "${BINARY_NAME}.zip" -t osx -u ${CFD_NOTE_USERNAME} -p ${CFD_NOTE_PASSWORD} --primary-bundle-id ${BUNDLE_ID}
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
# creating build directory
|
||||
rm -rf $TARGET_DIRECTORY
|
||||
mkdir "${TARGET_DIRECTORY}"
|
||||
mkdir "${TARGET_DIRECTORY}/contents"
|
||||
cp -r ".mac_resources/scripts" "${TARGET_DIRECTORY}/scripts"
|
||||
|
||||
# copy cloudflared into the build directory
|
||||
cp ${BINARY_NAME} "${TARGET_DIRECTORY}/contents/${PRODUCT}"
|
||||
|
||||
# compress cloudflared into a tar and gzipped file
|
||||
tar czf "$FILENAME" "${BINARY_NAME}"
|
||||
|
||||
# build the installer package
|
||||
if [[ ! -z "$PKG_SIGN_NAME" ]]; then
|
||||
pkgbuild --identifier com.cloudflare.${PRODUCT} \
|
||||
--version ${VERSION} \
|
||||
--scripts ${TARGET_DIRECTORY}/scripts \
|
||||
--root ${TARGET_DIRECTORY}/contents \
|
||||
--install-location /usr/local/bin \
|
||||
--sign "${PKG_SIGN_NAME}" \
|
||||
${PKGNAME}
|
||||
|
||||
# notarize the package
|
||||
if [[ ! -z "$CFD_NOTE_PASSWORD" ]]; then
|
||||
xcrun altool --notarize-app -f ${PKGNAME} -t osx -u ${CFD_NOTE_USERNAME} -p ${CFD_NOTE_PASSWORD} --primary-bundle-id ${BUNDLE_ID}
|
||||
xcrun stapler staple ${PKGNAME}
|
||||
fi
|
||||
else
|
||||
pkgbuild --identifier com.cloudflare.${PRODUCT} \
|
||||
--version ${VERSION} \
|
||||
--scripts ${TARGET_DIRECTORY}/scripts \
|
||||
--root ${TARGET_DIRECTORY}/contents \
|
||||
--install-location /usr/local/bin \
|
||||
${PKGNAME}
|
||||
fi
|
||||
|
||||
|
||||
# cleaning up the build directory
|
||||
rm -rf $TARGET_DIRECTORY
|
||||
|
||||
Vendored
+1
-1
@@ -43,7 +43,7 @@ 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"
|
||||
URL="https://packages.argotunnel.com/dl/cloudflared-$VERSION-darwin-amd64.tgz"
|
||||
tee cloudflared.rb <<EOF
|
||||
class Cloudflared < Formula
|
||||
desc 'Argo Tunnel'
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
# Experimental: This is a new format for release notes. The format and availability is subject to change.
|
||||
|
||||
## 2021.2.4
|
||||
|
||||
### Bug Fixes
|
||||
- [configuration] fixes a case where `cloudflared` failed to read URLs in configuration files
|
||||
- [logging] cloudflared now logs reason for failed connections if the error is recoverable
|
||||
|
||||
## 2021.2.3
|
||||
|
||||
### Backward Incompatible Changes
|
||||
- [db-connect] Removes db-connect from `cloudflared`. The Cloudflare Workers product will continue to support db-connect implementations with versions of `cloudflared` that predate this release and include support for db-connect.
|
||||
|
||||
### New Features
|
||||
- [TCP connects] Introduces support for proxy configurations with websockets in arbitrary TCP connections (#318)
|
||||
|
||||
### Improvements
|
||||
- [command line interface] Nested commands (such as cloudflared tunnel run) now consider CLI arguments even if they appear earlier in the command. E.g. `cloudflared --config config.yaml tunnel run` will now behave similarly to `cloudflared tunnel --config config.yaml run`
|
||||
|
||||
### Bug Fixes
|
||||
- [dns-proxy] The maximum number of upstream connections is now limited by default which should fix reported issues of cloudflared exhausting CPU usage when faced with connectivity issues.
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# use a builder image for building cloudflare
|
||||
ARG TARGET_GOOS
|
||||
ARG TARGET_GOARCH
|
||||
FROM golang:1.13.3 as builder
|
||||
FROM golang:1.15.7 as builder
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0 \
|
||||
TARGET_GOOS=${TARGET_GOOS} \
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
VERSION := $(shell git describe --tags --always --dirty="-dev")
|
||||
VERSION := $(shell git describe --tags --always --dirty="-dev" --match "[0-9][0-9][0-9][0-9].*.*")
|
||||
MSI_VERSION := $(shell git tag -l --sort=v:refname | grep "w" | tail -1 | cut -c2-)
|
||||
#MSI_VERSION expects the format of the tag to be: (wX.X.X). Starts with the w character to not break cfsetup.
|
||||
#e.g. w3.0.1 or w4.2.10. It trims off the w character when creating the MSI.
|
||||
|
||||
ifeq ($(FIPS), true)
|
||||
GO_BUILD_TAGS := $(GO_BUILD_TAGS) fips
|
||||
endif
|
||||
|
||||
ifneq ($(GO_BUILD_TAGS),)
|
||||
GO_BUILD_TAGS := -tags $(GO_BUILD_TAGS)
|
||||
endif
|
||||
|
||||
DATE := $(shell date -u '+%Y-%m-%d-%H%M UTC')
|
||||
VERSION_FLAGS := -ldflags='-X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)"'
|
||||
|
||||
@@ -22,6 +34,8 @@ ifneq ($(GOARCH),)
|
||||
TARGET_ARCH ?= $(GOARCH)
|
||||
else ifeq ($(LOCAL_ARCH),x86_64)
|
||||
TARGET_ARCH ?= amd64
|
||||
else ifeq ($(LOCAL_ARCH),i686)
|
||||
TARGET_ARCH ?= amd64
|
||||
else ifeq ($(shell echo $(LOCAL_ARCH) | head -c 5),armv8)
|
||||
TARGET_ARCH ?= arm64
|
||||
else ifeq ($(LOCAL_ARCH),aarch64)
|
||||
@@ -39,6 +53,8 @@ else ifeq ($(LOCAL_OS),darwin)
|
||||
TARGET_OS ?= darwin
|
||||
else ifeq ($(LOCAL_OS),windows)
|
||||
TARGET_OS ?= windows
|
||||
else ifeq ($(LOCAL_OS),freebsd)
|
||||
TARGET_OS ?= freebsd
|
||||
else
|
||||
$(error This system's OS $(LOCAL_OS) isn't supported)
|
||||
endif
|
||||
@@ -51,8 +67,6 @@ endif
|
||||
|
||||
ifeq ($(FLAVOR), centos-7)
|
||||
TARGET_PUBLIC_REPO ?= el7
|
||||
else ifeq ($(FLAVOR), centos-6)
|
||||
TARGET_PUBLIC_REPO ?= el6
|
||||
else
|
||||
TARGET_PUBLIC_REPO ?= $(FLAVOR)
|
||||
endif
|
||||
@@ -66,7 +80,7 @@ clean:
|
||||
|
||||
.PHONY: cloudflared
|
||||
cloudflared: tunnel-deps
|
||||
GOOS=$(TARGET_OS) GOARCH=$(TARGET_ARCH) go build -v -mod=vendor $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/cloudflared
|
||||
GOOS=$(TARGET_OS) GOARCH=$(TARGET_ARCH) go build -v -mod=vendor $(GO_BUILD_TAGS) $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/cloudflared
|
||||
|
||||
.PHONY: container
|
||||
container:
|
||||
@@ -74,7 +88,13 @@ container:
|
||||
|
||||
.PHONY: test
|
||||
test: vet
|
||||
ifndef CI
|
||||
go test -v -mod=vendor -race $(VERSION_FLAGS) ./...
|
||||
else
|
||||
@mkdir -p .cover
|
||||
go test -v -mod=vendor -race $(VERSION_FLAGS) -coverprofile=".cover/c.out" ./...
|
||||
go tool cover -html ".cover/c.out" -o .cover/all.html
|
||||
endif
|
||||
|
||||
.PHONY: test-ssh-server
|
||||
test-ssh-server:
|
||||
@@ -84,6 +104,7 @@ define publish_package
|
||||
for HOST in $(CF_PKG_HOSTS); do \
|
||||
ssh-keyscan -t rsa $$HOST >> ~/.ssh/known_hosts; \
|
||||
scp -4 cloudflared*.$(1) cfsync@$$HOST:/state/cf-pkg/staging/$(2)/$(TARGET_PUBLIC_REPO)/cloudflared/; \
|
||||
ssh cfsync@$$HOST 'chmod g+w /state/cf-pkg/staging/$(2)/$(TARGET_PUBLIC_REPO)/cloudflared/*.$(1)'; \
|
||||
done
|
||||
endef
|
||||
|
||||
@@ -99,7 +120,7 @@ define build_package
|
||||
mkdir -p $(PACKAGE_DIR)
|
||||
cp cloudflared $(PACKAGE_DIR)/cloudflared
|
||||
cat cloudflared_man_template | sed -e 's/\$${VERSION}/$(VERSION)/; s/\$${DATE}/$(DATE)/' > $(PACKAGE_DIR)/cloudflared.1
|
||||
fakeroot fpm -C $(PACKAGE_DIR) -s dir -t $(1) --$(1)-compression bzip2 \
|
||||
fakeroot fpm -C $(PACKAGE_DIR) -s dir -t $(1) \
|
||||
--description 'Cloudflare Argo tunnel daemon' \
|
||||
--vendor 'Cloudflare' \
|
||||
--license 'Cloudflare Service Agreement' \
|
||||
@@ -122,6 +143,58 @@ cloudflared-darwin-amd64.tgz: cloudflared
|
||||
tar czf cloudflared-darwin-amd64.tgz cloudflared
|
||||
rm cloudflared
|
||||
|
||||
.PHONY: cloudflared-junos
|
||||
cloudflared-junos: cloudflared jetez-certificate.pem jetez-key.pem
|
||||
jetez --source . \
|
||||
-j jet.yaml \
|
||||
--key jetez-key.pem \
|
||||
--cert jetez-certificate.pem \
|
||||
--version $(VERSION)
|
||||
rm jetez-*.pem
|
||||
|
||||
jetez-certificate.pem:
|
||||
ifndef JETEZ_CERT
|
||||
$(error JETEZ_CERT not defined)
|
||||
endif
|
||||
@echo "Writing JetEZ certificate"
|
||||
@echo "$$JETEZ_CERT" > jetez-certificate.pem
|
||||
|
||||
jetez-key.pem:
|
||||
ifndef JETEZ_KEY
|
||||
$(error JETEZ_KEY not defined)
|
||||
endif
|
||||
@echo "Writing JetEZ key"
|
||||
@echo "$$JETEZ_KEY" > jetez-key.pem
|
||||
|
||||
.PHONY: publish-cloudflared-junos
|
||||
publish-cloudflared-junos: cloudflared-junos cloudflared-x86-64.latest.s3
|
||||
ifndef S3_ENDPOINT
|
||||
$(error S3_HOST not defined)
|
||||
endif
|
||||
ifndef S3_URI
|
||||
$(error S3_URI not defined)
|
||||
endif
|
||||
ifndef S3_ACCESS_KEY
|
||||
$(error S3_ACCESS_KEY not defined)
|
||||
endif
|
||||
ifndef S3_SECRET_KEY
|
||||
$(error S3_SECRET_KEY not defined)
|
||||
endif
|
||||
sha256sum cloudflared-x86-64-$(VERSION).tgz | awk '{printf $$1}' > cloudflared-x86-64-$(VERSION).tgz.shasum
|
||||
s4cmd --endpoint-url $(S3_ENDPOINT) --force --API-GrantRead=uri=http://acs.amazonaws.com/groups/global/AllUsers \
|
||||
put cloudflared-x86-64-$(VERSION).tgz $(S3_URI)/cloudflared-x86-64-$(VERSION).tgz
|
||||
s4cmd --endpoint-url $(S3_ENDPOINT) --force --API-GrantRead=uri=http://acs.amazonaws.com/groups/global/AllUsers \
|
||||
put cloudflared-x86-64-$(VERSION).tgz.shasum $(S3_URI)/cloudflared-x86-64-$(VERSION).tgz.shasum
|
||||
dpkg --compare-versions "$(VERSION)" gt "$(shell cat cloudflared-x86-64.latest.s3)" && \
|
||||
echo -n "$(VERSION)" > cloudflared-x86-64.latest && \
|
||||
s4cmd --endpoint-url $(S3_ENDPOINT) --force --API-GrantRead=uri=http://acs.amazonaws.com/groups/global/AllUsers \
|
||||
put cloudflared-x86-64.latest $(S3_URI)/cloudflared-x86-64.latest || \
|
||||
echo "Latest version not updated"
|
||||
|
||||
cloudflared-x86-64.latest.s3:
|
||||
s4cmd --endpoint-url $(S3_ENDPOINT) --force \
|
||||
get $(S3_URI)/cloudflared-x86-64.latest cloudflared-x86-64.latest.s3
|
||||
|
||||
.PHONY: homebrew-upload
|
||||
homebrew-upload: cloudflared-darwin-amd64.tgz
|
||||
aws s3 --endpoint-url $(S3_ENDPOINT) cp --acl public-read $$^ $(S3_URI)/cloudflared-$$(VERSION)-$1.tgz
|
||||
@@ -139,6 +212,15 @@ release: bin/equinox
|
||||
github-release: cloudflared
|
||||
python3 github_release.py --path $(EXECUTABLE_PATH) --release-version $(VERSION)
|
||||
|
||||
.PHONY: github-message
|
||||
github-message:
|
||||
python3 github_message.py --release-version $(VERSION)
|
||||
|
||||
.PHONY: github-mac-upload
|
||||
github-mac-upload:
|
||||
python3 github_release.py --path artifacts/cloudflared-darwin-amd64.tgz --release-version $(VERSION) --name cloudflared-darwin-amd64.tgz
|
||||
python3 github_release.py --path artifacts/cloudflared-amd64.pkg --release-version $(VERSION) --name cloudflared-amd64.pkg
|
||||
|
||||
bin/equinox:
|
||||
mkdir -p bin
|
||||
curl -s https://bin.equinox.io/c/75JtLRTsJ3n/release-tool-beta-$(EQUINOX_PLATFORM).tgz | tar xz -C bin/
|
||||
@@ -156,3 +238,7 @@ vet:
|
||||
go vet -mod=vendor ./...
|
||||
which go-sumtype # go get github.com/BurntSushi/go-sumtype
|
||||
go-sumtype $$(go list -mod=vendor ./...)
|
||||
|
||||
.PHONY: msi
|
||||
msi: cloudflared
|
||||
go-msi make --msi cloudflared.msi --version $(MSI_VERSION)
|
||||
|
||||
@@ -1,9 +1,43 @@
|
||||
# Argo Tunnel client
|
||||
|
||||
Contains the command-line client and its libraries for Argo Tunnel, a tunneling daemon that proxies any local webserver through the Cloudflare network.
|
||||
Contains the command-line client for Argo Tunnel, a tunneling daemon that proxies any local webserver through the Cloudflare network. Extensive documentation can be found in the [Argo Tunnel section](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps) of the Cloudflare Docs.
|
||||
|
||||
## Getting started
|
||||
## Before you get started
|
||||
|
||||
go install github.com/cloudflare/cloudflared/cmd/cloudflared
|
||||
Before you use Argo Tunnel, you'll need to complete a few steps in the Cloudflare dashboard. The website you add to Cloudflare will be used to route traffic to your Tunnel.
|
||||
|
||||
User documentation for Argo Tunnel can be found at https://developers.cloudflare.com/argo-tunnel/
|
||||
1. [Add a website to Cloudflare](https://support.cloudflare.com/hc/en-us/articles/201720164-Creating-a-Cloudflare-account-and-adding-a-website)
|
||||
2. [Change your domain nameservers to Cloudflare](https://support.cloudflare.com/hc/en-us/articles/205195708)
|
||||
|
||||
## Installing `cloudflared`
|
||||
|
||||
Downloads are available as standalone binaries, a Docker image, and Debian, RPM, and Homebrew packages. You can also find releases here on the `cloudflared` GitHub repository.
|
||||
|
||||
* You can [install on macOS](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#macos) via Homebrew or by downloading the [latest Darwin amd64 release](https://github.com/cloudflare/cloudflared/releases)
|
||||
* Binaries, Debian, and RPM packages for Linux [can be found here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#linux)
|
||||
* A Docker image of `cloudflared` is [available on DockerHub](https://hub.docker.com/r/cloudflare/cloudflared)
|
||||
* You can install on Windows machines with the [steps here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#windows)
|
||||
|
||||
User documentation for Argo Tunnel can be found at https://developers.cloudflare.com/cloudflare-one/connections/connect-apps
|
||||
|
||||
## Creating Tunnels and routing traffic
|
||||
|
||||
Once installed, you can authenticate `cloudflared` into your Cloudflare account and begin creating Tunnels that serve traffic for hostnames in your account.
|
||||
|
||||
* Create a Tunnel with [these instructions](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/create-tunnel)
|
||||
* Route traffic to that Tunnel with [DNS records in Cloudflare](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/routing-to-tunnel/dns) or with a [Cloudflare Load Balancer](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/routing-to-tunnel/lb)
|
||||
|
||||
## TryCloudflare
|
||||
|
||||
Want to test Argo Tunnel before adding a website to Cloudflare? You can do so with TryCloudflare using the documentation [available here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/trycloudflare).
|
||||
|
||||
## Deprecated versions
|
||||
|
||||
Cloudflare currently supports all versions of `cloudflared`. Starting on March 20, 2021, Cloudflare will no longer support versions released prior to 2020.5.1.
|
||||
|
||||
All features available in versions released prior to 2020.5.1 are available in current versions. Breaking changes unrelated to feature availability may be introduced that will impact versions released prior to 2020.5.1. You can read more about upgrading `cloudflared` in our [developer documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#updating-cloudflared).
|
||||
|
||||
| Version(s) | Deprecation status |
|
||||
|---|---|
|
||||
| 2020.5.1 and later | Supported |
|
||||
| Versions prior to 2020.5.1 | Will no longer be supported starting March 20, 2021 |
|
||||
|
||||
+293
@@ -1,3 +1,296 @@
|
||||
2021.2.4
|
||||
- 2021-02-22 TUN-3948: Log error when retrying connection
|
||||
- 2021-02-23 TUN-3964: Revert "TUN-3922: Repoint urfave/cli/v2 library at patched branch at github.com/ipostelnik/cli/v2@fixed which correctly handles reading flags declared at multiple levels of subcommands."
|
||||
- 2021-02-23 Publish release notes for 2021.2.4
|
||||
|
||||
2021.2.3
|
||||
- 2021-02-23 Publish release notes for 2021.2.3
|
||||
- 2021-02-10 TUN-3902: Add jitter to backoffhandler
|
||||
- 2021-02-11 TUN-3913: Help gives wrong exit code for autoupdate
|
||||
- 2021-02-12 Add max upstream connections dns-proxy option (#290)
|
||||
- 2021-02-16 TUN-3924: Removed db-connect command. Added a placeholder handler for this command that informs users that command is no longer supported.
|
||||
- 2021-02-12 TUN-3922: Repoint urfave/cli/v2 library at patched branch at github.com/ipostelnik/cli/v2@fixed which correctly handles reading flags declared at multiple levels of subcommands.
|
||||
- 2021-02-19 Added support for proxy (#318)
|
||||
- 2021-02-19 TUN-3945: Fix runApp signature for generic service
|
||||
- 2021-02-09 Update README.md
|
||||
- 2021-02-09 Update the TryCloudflare link
|
||||
|
||||
2021.2.2
|
||||
- 2021-02-04 TUN-3864: Users can choose where credentials file is written after creating a tunnel
|
||||
- 2021-02-04 TUN-3869: Improve reliability of graceful shutdown.
|
||||
- 2021-02-07 TUN-3878: Do not supply -tags when none are specified
|
||||
- 2021-02-04 TUN-3635: Send event when unregistering tunnel for gracful shutdown so /ready endpoint reports down status befoe connections finish handling pending requests.
|
||||
- 2021-02-08 TUN-3890: Code coverage for cloudflared in CI
|
||||
- 2021-02-09 AUTH-3375 exchangeOrgToken deleted cookie fix
|
||||
- 2020-11-18 Update error message to use login command
|
||||
|
||||
2021.2.1
|
||||
- 2021-02-04 TUN-3858: Do not suffix cloudflared version with -fips
|
||||
|
||||
2021.2.0
|
||||
- 2021-02-01 TUN-3837: Remove automation_email from cloudflared status page test
|
||||
- 2021-02-03 TUN-3848: Use transport logger for h2mux
|
||||
- 2021-02-03 TUN-3854: cloudflared tunnel list flags to sort output
|
||||
- 2021-01-21 TUN-3195: Don't colorize console logs when stderr is not a terminal
|
||||
- 2021-01-20 Fixed connection error handling by removing duplicated errors, standardizing on non-pointer error types
|
||||
- 2021-01-20 TUN-3118: Changed graceful shutdown to immediately unregister tunnel from the edge, keep the connection open until the edge drops it or grace period expires
|
||||
- 2021-01-25 TUN-3165: Add reference to Argo Tunnel documentation in the help output
|
||||
- 2021-01-25 TUN-3806: Use a .dockerignore
|
||||
- 2021-01-21 TUN-3795: Use RFC-3339 style date format for logs, produce timestamp in UTC
|
||||
- 2021-01-26 TUN-3795: Removed errant test
|
||||
- 2021-01-25 TUN-3792: Handle graceful shutdown correctly when running as a windows service. Only expose one shutdown channel globally, which now triggers the graceful shutdown sequence across all modes. Removed separate handling of zero-duration grace period, instead it's checked only when we need to wait for exit.
|
||||
- 2021-01-27 TUN-3811: Better error reporting on http2 connection termination. Registration errors from control loop are now propagated out of the connection server code. Unified error handling between h2mux and http2 connections so we log and retry errors the same way, regardless of underlying transport.
|
||||
- 2021-01-28 TUN-3830: Use Go 1.15.7
|
||||
- 2021-01-28 TUN-3826: Use go-fips when building cloudflared for linux/amd64
|
||||
- 2021-01-19 TUN-3777: Fix /ready endpoint for classic tunnels
|
||||
- 2021-01-19 TUN-3773: Add back pprof endpoints
|
||||
|
||||
2021.1.5
|
||||
- 2021-01-15 TUN-3594: Log ingress response at debug level
|
||||
- 2021-01-15 TUN-3765: Fix doubly nested log output by `logfile` option
|
||||
- 2021-01-16 TUN-3767: Tolerate logging errors
|
||||
- 2021-01-17 TUN-3768: Reuse file loggers
|
||||
- 2021-01-14 TUN-3738: Refactor observer to avoid potential of blocking on tunnel notifications
|
||||
- 2021-01-15 TUN-3766: Print flags defined at all levels of command hierarchy, not just locally defined flags for a command. This fixes output of overriden settings for subcommand.
|
||||
|
||||
2021.1.4
|
||||
- 2021-01-14 TUN-3759: Single file logging output should always append
|
||||
|
||||
2021.1.3
|
||||
- 2021-01-14 TUN-3756: File logging output must consider the directory
|
||||
- 2021-01-14 TUN-3757: Fix legacy Uint flags that are incorrectly handled by ufarve library
|
||||
|
||||
2021.1.2
|
||||
- 2021-01-13 TUN-3747: Fix logging in Windows
|
||||
|
||||
2021.1.1
|
||||
- 2021-01-13 TUN-3744: Fix compilation error in windows service
|
||||
|
||||
2021.1.0
|
||||
- 2021-01-11 TUN-3670: Update Teamnet API gateway prefixes
|
||||
- 2021-01-13 TUN-3738: Consume UI events even when UI is disabled
|
||||
- 2021-01-06 TUN-3722: Teamnet API paths include /network
|
||||
- 2021-01-05 TUN-3688: Subcommand for users to check which route an IP proxies through
|
||||
- 2021-01-08 TUN-3691: Edit Teamnet help text
|
||||
- 2020-12-30 TUN-3706: Quit if any origin service fails to start
|
||||
- 2020-12-31 TUN-3708: Better info message about system root certpool on Windows
|
||||
- 2020-12-21 TUN-3669: Teamnet commands to add/show Teamnet routes.
|
||||
- 2020-12-29 TUN-3689: Delete routes via cloudflared CLI
|
||||
- 2020-12-28 TUN-3471: Add structured log context to logs
|
||||
- 2020-12-15 TUN-3650: Remove unused awsuploader package
|
||||
- 2020-12-03 Update to add deprecated version note (#271)
|
||||
- 2020-12-02 TUN-3472: Set up rolling logger with zerolog and lumberjack
|
||||
- 2020-12-08 TUN-3607: Set up single-file logger with zerolog
|
||||
- 2020-12-03 Update to add deprecated version note (#271)
|
||||
- 2020-11-25 TUN-3470: Replace in-house logger calls with zerolog
|
||||
|
||||
2020.12.0
|
||||
- 2020-12-04 TUN-3599: improved delete if credentials isnt found.
|
||||
- 2020-12-04 TUN-3612: Upgrade to Go 1.15.6
|
||||
- 2020-11-30 TUN-3593: /ready endpoint for k8s readiness. Move tunnel events out of UI package, into connection package.
|
||||
- 2020-11-27 TUN-3594: Log response status at debug level
|
||||
|
||||
2020.11.11
|
||||
- 2020-11-20 TUN-3578: cloudflared tunnel route dns should allow wildcard subdomains
|
||||
- 2020-11-21 EDGEPLAT-2958 remove deb-compression, defaulting to gzip
|
||||
- 2020-11-23 TUN-3581: Tunnels can be run by name using only --credentials-file, no origin cert necessary.
|
||||
- 2020-11-15 TUN-3561: Unified logger configuration
|
||||
- 2020-11-08 AUTH-3221: Saves org token to disk and uses it to refresh the app token
|
||||
|
||||
2020.11.10
|
||||
- 2020-11-20 TUN-3562: Fix panic when using bastion mode ingress rule
|
||||
- 2020-11-20 EDGEPLAT-2958 build cloudflared for Bullseye
|
||||
|
||||
2020.11.9
|
||||
- 2020-11-18 TUN-3557: Detect SSE if content-type starts with text/event-stream
|
||||
- 2020-11-18 TUN-3559: Share response meta header with other packages
|
||||
- 2020-11-18 DEVTOOLS-7936: Remove redundant chgrp from publish
|
||||
- 2020-11-18 TUN-3558: cloudflared allows empty config files
|
||||
- 2020-11-18 TUN-3544: Upgrade to Go 1.15.5
|
||||
|
||||
2020.11.8
|
||||
- 2020-11-17 TUN-3555: Single origin service should default to localhost:8080
|
||||
|
||||
2020.11.7
|
||||
- 2020-11-13 TUN-3514: Stop setting --is-autoupdated flag after autoupdate because it can break named tunnel running in k8s
|
||||
- 2020-11-15 TUN-3548, TUN-3547: Bastion mode can be specified as a service, doesn't require URL.
|
||||
- 2020-11-16 TUN-3549: Use a separate handler for each websocket proxy
|
||||
|
||||
2020.11.6
|
||||
- 2020-11-14 TUN-3546: Fix panic in tlsconfig.LoadOriginCA
|
||||
|
||||
2020.11.5
|
||||
- 2020-11-12 TUN-3540: Better copy in ingress rules error messages
|
||||
- 2020-11-12 DEVTOOLS-7936: Set permissions on public packages
|
||||
- 2020-11-13 TUN-3543: ProxyAddress not using default in single-origin mode
|
||||
|
||||
2020.11.4
|
||||
- 2020-11-11 TUN-3534: Specific error message when credentials file is a .pem not .json
|
||||
- 2020-11-02 TUN-3500: Integrate replace h2mux by http2 work with multiple origin support
|
||||
- 2020-11-09 TUN-3514: Transport logger write to UI when UI is enabled
|
||||
- 2020-10-30 TUN-3490: Make sure OriginClient implementation doesn't write after Proxy return
|
||||
- 2020-10-20 TUN-3403: Unit test for origin/proxy to test serving HTTP and Websocket
|
||||
- 2020-10-23 TUN-3480: Support SSE with http2 connection, and add SSE handler to hello-world server
|
||||
- 2020-10-27 TUN-3489: Add unit tests to cover proxy logic in connection package of cloudflared
|
||||
- 2020-10-16 TUN-3467: Serialize cf-cloudflared-response-meta during package initialization using jsoniter
|
||||
- 2020-10-14 TUN-3456: New protocol option auto to automatically select between http2 and h2mux
|
||||
- 2020-10-14 TUN-3458: Upgrade to http2 when available, fallback to h2mux when we reach max retries
|
||||
- 2020-10-08 TUN-3449: Use flag to select transport protocol implementation
|
||||
- 2020-10-08 TUN-3462: Refactor cloudflared to separate origin from connection
|
||||
- 2020-09-21 TUN-3406: Proxy websocket requests over Go http2
|
||||
- 2020-09-25 TUN-3420: Establish control plane and send RPC over control plane
|
||||
- 2020-09-11 TUN-3400: Use Go HTTP2 library as transport to connect with the edge
|
||||
|
||||
2020.11.3
|
||||
- 2020-11-11 TUN-3533: Set config for single origin ingress
|
||||
|
||||
2020.11.2
|
||||
|
||||
|
||||
2020.11.1
|
||||
- 2020-11-10 TUN-3527: More specific error for invalid YAML/JSON
|
||||
- 2020-11-06 Update README.md (#256)
|
||||
|
||||
2020.11.0
|
||||
- 2020-11-04 TUN-3484: OriginService that responds with configured HTTP status
|
||||
- 2020-11-05 TUN-3505: Response body for status code origin returns EOF on Read
|
||||
- 2020-11-04 TUN-3503: Matching ingress rule should not take port into account
|
||||
- 2020-11-05 TUN-3506: OriginService needs to set request host and scheme for websocket requests
|
||||
- 2020-11-09 TUN-3516: Better error message when parsing invalid YAML config
|
||||
- 2020-11-09 TUN-3522: ingress validate checks that the config file exists
|
||||
- 2020-11-09 TUN-3524: Don't ignore errors from app-level action handler (#248)
|
||||
- 2020-11-09 TUN-3461: Show all origin services in the UI
|
||||
- 2020-10-30 TUN-3494: Proceed to create tunnel if at least one edge address can be resolved
|
||||
- 2020-10-30 TUN-3492: Refactor OriginService, shrink its interface
|
||||
- 2020-10-22 TUN-3478: Increase download timeout to 60s
|
||||
- 2020-10-15 TUN-2640: Users can configure per-origin config. Unify single-rule CLI flow with multi-rule config file code.
|
||||
|
||||
2020.10.2
|
||||
- 2020-10-21 Release 2020.10.1
|
||||
- 2020-10-21 AUTH-3185 fixed indention error
|
||||
- 2020-10-19 TUN-3459: Make service install on linux use named tunnels
|
||||
|
||||
2020.10.1
|
||||
- 2020-10-20 Split out typed config from legacy command-line switches; refactor ingress commands and fix tests
|
||||
- 2020-10-20 Move raw ingress rules to config package
|
||||
- 2020-10-21 TUN-3476: Fix conversion to string and int slice
|
||||
- 2020-10-12 TUN-3441: Multiple-origin routing via ingress rules
|
||||
- 2020-10-15 TUN-3464: Newtype to wrap []ingress.Rule
|
||||
- 2020-10-15 TUN-3465: Use Go 1.15.3
|
||||
- 2020-10-15 TUN-3463: Let users run a named tunnel via config file setting
|
||||
- 2020-10-19 TUN-3475: Unify config file handling with typed config for new fields
|
||||
- 2020-10-19 TUN-3459: Make service install on linux use named tunnels
|
||||
- 2020-10-06 AUTH-3148 fixed cloudflared copy and match all the files in the checksum upload
|
||||
- 2020-10-06 TUN-3436, TUN-3437: Parse ingress from YAML, ensure last rule catches everything
|
||||
- 2020-10-06 TUN-3446: Use go 1.15.2 and add a step to build cloudflared in the dev Dockerfile
|
||||
- 2020-10-07 TUN-3439: 'tunnel validate' command to check ingress rules
|
||||
- 2020-10-07 TUN-3440: 'tunnel rule' command to test ingress rules
|
||||
- 2020-10-08 TUN-3451: Cloudflared tunnel ingress command
|
||||
- 2020-10-09 TUN-3452: Fix loading of flags from config file for tunnel run subcommand. This change also cleans up building of tunnel subcommand list, hides deprecated subcommands and improves help.
|
||||
- 2020-10-08 TUN-3438: move ingress into own package, read into TunnelConfig
|
||||
|
||||
2020.10.0
|
||||
- 2020-10-02 AUTH-2993 cleaned up worker service tests
|
||||
- 2020-10-02 TUN-3443: Decode as v4api response on non-200 status
|
||||
- 2020-09-24 TRAFFIC-448: allow the user to specify the proxy address and port to bind to, falling back to 127.0.0.1 and random port if not specified
|
||||
- 2020-09-28 TUN-3427: Define a struct that only implements RegistrationServer in tunnelpogs
|
||||
- 2020-09-29 TUN-3430: Copy flags to configure proxy to run subcommand, print relevant tunnel flags in help
|
||||
- 2020-08-12 AUTH-2993 added workers updater logic
|
||||
|
||||
2020.9.3
|
||||
- 2020-09-22 TRAFFIC-448: build cloudflare for junos and publish to s3
|
||||
- 2020-09-22 TUN-3410: Request the v1 Tunnelstore API
|
||||
- 2020-09-23 Release 2020.9.2
|
||||
- 2020-09-17 updater service exit code should be 11
|
||||
- 2020-09-18 AUTH-3109 upload the checksum to workers kv on github releases
|
||||
|
||||
2020.9.2
|
||||
- 2020-09-22 TRAFFIC-448: build cloudflare for junos and publish to s3
|
||||
- 2020-09-22 TUN-3410: Request the v1 Tunnelstore API
|
||||
- 2020-09-17 AUTH-3103 CI build fixes
|
||||
- 2020-09-18 AUTH-3110-use-cfsetup-precache
|
||||
- 2020-09-17 TUN-3295: Show route command results
|
||||
- 2020-09-16 TUN-3291: cloudflared tunnel run -h explains how to use flags from parent command
|
||||
- 2020-09-18 AUTH-3109 upload the checksum to workers kv on github releases
|
||||
- 2020-09-17 updater service exit code should be 11
|
||||
- 2020-09-01 TUN-3216: UI improvements
|
||||
- 2020-08-25 Rebased and passed TunnelEventChan to LogServerInfo in new ReconnectTunnel function
|
||||
- 2020-08-25 TUN-3321: Add box around logs on UI
|
||||
- 2020-08-26 TUN-3328: Filter out free tunnel has started log from UI
|
||||
- 2020-08-27 TUN-3333: Add text to UI explaining how to exit
|
||||
- 2020-08-27 TUN-3335: Dynamically set connection table size for UI
|
||||
- 2020-08-10 TUN-3238: Update UI when connection re-connects
|
||||
- 2020-08-17 TUN-3261: Display connections on UI for free classic tunnels
|
||||
- 2020-07-24 TUN-3201: Create base cloudflared UI structure
|
||||
- 2020-07-29 TUN-3200: Add connection information to UI
|
||||
- 2020-07-24 TUN-3255: Update UI to display URL instead of hostname
|
||||
- 2020-07-29 TUN-3198: Handle errors while running tunnel UI
|
||||
|
||||
2020.9.1
|
||||
- 2020-09-14 TUN-3395: Unhide named tunnel subcommands, tweak help
|
||||
- 2020-09-15 TUN-3395: Improve help for list command
|
||||
- 2020-09-14 TUN-3294: Perform basic validation on arguments of route command; remove default pool name which wasn't valid
|
||||
- 2020-09-15 TUN-3395: Improve help for list command
|
||||
- 2020-09-16 Use Go 1.15.2
|
||||
|
||||
2020.9.0
|
||||
- 2020-09-11 TUN-3293: Try to use error information from the body of a failed tunnelstore reresponse if available
|
||||
- 2020-09-04 AUTH-2653 renabled signing
|
||||
- 2020-09-04 TUN-3377: Tunnel route should check dns/lb before checking tunnel ID
|
||||
- 2020-09-04 AUTH-2653 changed to proper file extension
|
||||
- 2020-09-04 AUTH-2653 handle duplicate key import errors
|
||||
- 2020-09-04 TUN-3345: tunnel run accepts name of tunnel as argument
|
||||
- 2020-09-08 AUTH-2653 disble error pipe to see what is failing
|
||||
- 2020-09-08 AUTH-2653 search for the certificate and not the identity
|
||||
- 2020-09-09 TUN-3284: Use cloudflared/<version> as user agent of tunnelstore client
|
||||
- 2020-09-09 TUN-3375: Upgrade x/text and gorilla websocket deps
|
||||
- 2020-09-09 TUN-3375: Upgrade coredns and prometheus dependencies
|
||||
- 2020-09-09 AUTH-2653 add notarization to mac build
|
||||
- 2020-09-08 TUN-3292: Mention cleanup in tunnel run help.
|
||||
- 2020-08-20 AUTH-2016 fixed variable fail
|
||||
- 2020-08-12 TUN-3352 extra debug logging for websockets
|
||||
|
||||
2020.8.2
|
||||
- 2020-08-20 AUTH-3021 fixed the git version call by using the older flag
|
||||
- 2020-08-18 TUN-3268: Each connection has its own event digest to reconnect
|
||||
|
||||
2020.8.1
|
||||
- 2020-08-14 AUTH-2975 don't check /etc on windows
|
||||
- 2020-08-14 AUTH-2977 log file protection
|
||||
- 2020-08-18 TUN-3286: Use either ID or name in Named Tunnel subcommands.
|
||||
- 2020-08-18 AUTH-2712 fixed the mac build script
|
||||
- 2020-08-19 AUTH-2653 disabling signing until we can get the keys
|
||||
- 2020-08-05 TUN-3233: List tunnels support filtering by deleted, name, existed at and id
|
||||
- 2020-08-05 TUN-3237: By default, don't show connections that are pending reconnect
|
||||
- 2020-08-06 TUN-3242: Build with go 1.14
|
||||
- 2020-08-07 TUN-3243: Refactor tunnel subcommands to allow commands to compose better
|
||||
- 2020-08-07 AUTH-2864 - add macos build to github release
|
||||
- 2020-07-31 AUTH-2857 update homebrew script to use new url
|
||||
- 2020-07-30 TUN-3213: Create, route and run named tunnels in one command
|
||||
- 2020-07-07 AUTH-2712 added MSI build for a windows agent
|
||||
|
||||
2020.8.0
|
||||
- 2020-07-30 TUN-3220: tunnel route reports created route
|
||||
- 2020-07-31 TUN-3221: ConnectionOptions tracks numPreviousAttempts.
|
||||
- 2020-07-20 TUN-3190: Initialize logger using command line flags in tunnels subcommands
|
||||
- 2020-07-21 TUN-3192: Use zone ID in tunnelstore request path; improve debug logging
|
||||
- 2020-07-23 TUN-3194: Don't render log output when level is not enabled
|
||||
- 2020-07-22 AUTH-2016 adds sha256 hashes to releases
|
||||
- 2020-07-27 Removes centos 6 build
|
||||
- 2020-07-27 TUN-3209: Add benchmark for header serialization
|
||||
- 2020-07-24 TUN-3209: improve performance and reduce allocations during user header serialization from h1 to h2
|
||||
- 2020-07-26 TUN-3208: Add benchmark for large response write
|
||||
- 2020-07-27 TUN-3208: Reduce copies and allocations on h2mux write path. Pre-allocate 16KB write buffer on the first write if possible. Use explicit byte array for chunks on write thread to avoid copying through intermediate buffer due to io.CopyN.
|
||||
- 2020-07-28 AUTH-2714: Adds arm64 cloudflared build
|
||||
- 2020-07-29 AUTH-2927 run message update after all github builds are done
|
||||
- 2020-07-17 AUTH-2902 redirect with just the root host on curl commands
|
||||
|
||||
2020.7.4
|
||||
- 2020-07-20 Build cloudflared for arm64 on native agents
|
||||
- 2020-07-10 TUN-3048: Handle error when user tries to delete active tunnel
|
||||
- 2020-07-14 AUTH-2890: adds error handler to cli actions
|
||||
- 2020-07-06 TUN-3156: Add route subcommand under tunnel
|
||||
|
||||
2020.7.3
|
||||
- 2020-07-13 Change scp command to use file glob that matches both cloudflared rpms and debs
|
||||
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
package awsuploader
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
// DirectoryUploadManager is used to manage file uploads on an interval from a directory
|
||||
type DirectoryUploadManager struct {
|
||||
logger logger.Service
|
||||
uploader Uploader
|
||||
rootDirectory string
|
||||
sweepInterval time.Duration
|
||||
ticker *time.Ticker
|
||||
shutdownC chan struct{}
|
||||
workQueue chan string
|
||||
}
|
||||
|
||||
// NewDirectoryUploadManager create a new DirectoryUploadManager
|
||||
// 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 logger.Service, uploader Uploader, directory string, sweepInterval time.Duration, shutdownC chan struct{}) *DirectoryUploadManager {
|
||||
workerCount := 10
|
||||
manager := &DirectoryUploadManager{
|
||||
logger: logger,
|
||||
uploader: uploader,
|
||||
rootDirectory: directory,
|
||||
sweepInterval: sweepInterval,
|
||||
shutdownC: shutdownC,
|
||||
workQueue: make(chan string, workerCount),
|
||||
}
|
||||
|
||||
//start workers
|
||||
for i := 0; i < workerCount; i++ {
|
||||
go manager.worker()
|
||||
}
|
||||
|
||||
return manager
|
||||
}
|
||||
|
||||
// Upload a file using the uploader
|
||||
// This is useful for "out of band" uploads that need to be triggered immediately instead of waiting for the sweep
|
||||
func (m *DirectoryUploadManager) Upload(filepath string) error {
|
||||
return m.uploader.Upload(filepath)
|
||||
}
|
||||
|
||||
// Start the upload ticker to walk the directories
|
||||
func (m *DirectoryUploadManager) Start() {
|
||||
m.ticker = time.NewTicker(m.sweepInterval)
|
||||
go m.run()
|
||||
}
|
||||
|
||||
func (m *DirectoryUploadManager) run() {
|
||||
for {
|
||||
select {
|
||||
case <-m.shutdownC:
|
||||
m.ticker.Stop()
|
||||
return
|
||||
case <-m.ticker.C:
|
||||
m.sweep()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sweep the directory and kick off uploads
|
||||
func (m *DirectoryUploadManager) sweep() {
|
||||
filepath.Walk(m.rootDirectory, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
//30 days ago
|
||||
retentionTime := 30 * (time.Hour * 24)
|
||||
checkTime := time.Now().Add(-time.Duration(retentionTime))
|
||||
|
||||
//delete the file it is stale
|
||||
if info.ModTime().Before(checkTime) {
|
||||
os.Remove(path)
|
||||
return nil
|
||||
}
|
||||
//add the upload to the work queue
|
||||
go func() {
|
||||
m.workQueue <- path
|
||||
}()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// worker handles upload requests
|
||||
func (m *DirectoryUploadManager) worker() {
|
||||
for {
|
||||
select {
|
||||
case <-m.shutdownC:
|
||||
return
|
||||
case filepath := <-m.workQueue:
|
||||
if err := m.Upload(filepath); err != nil {
|
||||
m.logger.Errorf("Cannot upload file to s3 bucket: %s", err)
|
||||
} else {
|
||||
os.Remove(filepath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
package awsuploader
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
type MockUploader struct {
|
||||
shouldFail bool
|
||||
}
|
||||
|
||||
func (m *MockUploader) Upload(filepath string) error {
|
||||
if m.shouldFail {
|
||||
return errors.New("upload set to fail")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewMockUploader(shouldFail bool) Uploader {
|
||||
return &MockUploader{shouldFail: shouldFail}
|
||||
}
|
||||
|
||||
func getDirectoryPath(t *testing.T) string {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal("couldn't create the test directory!", err)
|
||||
}
|
||||
return filepath.Join(dir, "uploads")
|
||||
}
|
||||
|
||||
func setupTestDirectory(t *testing.T) string {
|
||||
path := getDirectoryPath(t)
|
||||
os.RemoveAll(path)
|
||||
time.Sleep(100 * time.Millisecond) //short way to wait for the OS to delete the folder
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatal("couldn't create the test directory!", err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func createUploadManager(t *testing.T, shouldFailUpload bool) *DirectoryUploadManager {
|
||||
rootDirectory := setupTestDirectory(t)
|
||||
uploader := NewMockUploader(shouldFailUpload)
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
shutdownC := make(chan struct{})
|
||||
return NewDirectoryUploadManager(logger, uploader, rootDirectory, 1*time.Second, shutdownC)
|
||||
}
|
||||
|
||||
func createFile(t *testing.T, fileName string) (*os.File, string) {
|
||||
path := filepath.Join(getDirectoryPath(t), fileName)
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
t.Fatal("upload to create file for sweep test", err)
|
||||
}
|
||||
return f, path
|
||||
}
|
||||
|
||||
func TestUploadSuccess(t *testing.T) {
|
||||
manager := createUploadManager(t, false)
|
||||
path := filepath.Join(getDirectoryPath(t), "test_file")
|
||||
if err := manager.Upload(path); err != nil {
|
||||
t.Fatal("the upload request method failed", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadFailure(t *testing.T) {
|
||||
manager := createUploadManager(t, true)
|
||||
path := filepath.Join(getDirectoryPath(t), "test_file")
|
||||
if err := manager.Upload(path); err == nil {
|
||||
t.Fatal("the upload request method should have failed and didn't", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSweepSuccess(t *testing.T) {
|
||||
manager := createUploadManager(t, false)
|
||||
f, path := createFile(t, "test_file")
|
||||
defer f.Close()
|
||||
|
||||
manager.Start()
|
||||
time.Sleep(2 * time.Second)
|
||||
if _, err := os.Stat(path); os.IsExist(err) {
|
||||
//the file should have been deleted
|
||||
t.Fatal("the manager failed to delete the file", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSweepFailure(t *testing.T) {
|
||||
manager := createUploadManager(t, true)
|
||||
f, path := createFile(t, "test_file")
|
||||
defer f.Close()
|
||||
|
||||
manager.Start()
|
||||
time.Sleep(2 * time.Second)
|
||||
_, serr := f.Stat()
|
||||
if serr != nil {
|
||||
//the file should still exist
|
||||
os.Remove(path)
|
||||
t.Fatal("the manager failed to delete the file", serr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHighLoad(t *testing.T) {
|
||||
manager := createUploadManager(t, false)
|
||||
for i := 0; i < 30; i++ {
|
||||
f, _ := createFile(t, randomString(6))
|
||||
defer f.Close()
|
||||
}
|
||||
manager.Start()
|
||||
time.Sleep(4 * time.Second)
|
||||
|
||||
directory := getDirectoryPath(t)
|
||||
files, err := ioutil.ReadDir(directory)
|
||||
if err != nil || len(files) > 0 {
|
||||
t.Fatalf("the manager failed to upload all the files: %s files left: %d", err, len(files))
|
||||
}
|
||||
}
|
||||
|
||||
// LowerCase [a-z]
|
||||
const randSet = "abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
// String returns a string of length 'n' from a set of letters 'lset'
|
||||
func randomString(n int) string {
|
||||
b := make([]byte, n)
|
||||
lsetLen := len(randSet)
|
||||
for i := range b {
|
||||
b[i] = randSet[rand.Intn(lsetLen)]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package awsuploader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
)
|
||||
|
||||
//FileUploader aws compliant bucket upload
|
||||
type FileUploader struct {
|
||||
storage *s3.S3
|
||||
bucketName string
|
||||
clientID string
|
||||
secretID string
|
||||
}
|
||||
|
||||
// NewFileUploader creates a new S3 compliant bucket uploader
|
||||
func NewFileUploader(bucketName, region, accessKeyID, secretID, token, s3Host string) (*FileUploader, error) {
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String(region),
|
||||
Credentials: credentials.NewStaticCredentials(accessKeyID, secretID, token),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var storage *s3.S3
|
||||
if s3Host != "" {
|
||||
storage = s3.New(sess, &aws.Config{Endpoint: aws.String(s3Host)})
|
||||
} else {
|
||||
storage = s3.New(sess)
|
||||
}
|
||||
|
||||
return &FileUploader{
|
||||
storage: storage,
|
||||
bucketName: bucketName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Upload a file to the bucket
|
||||
func (u *FileUploader) Upload(filepath string) error {
|
||||
info, err := os.Stat(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, serr := u.storage.PutObjectWithContext(context.Background(), &s3.PutObjectInput{
|
||||
Bucket: aws.String(u.bucketName),
|
||||
Key: aws.String(info.Name()),
|
||||
Body: file,
|
||||
})
|
||||
return serr
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package awsuploader
|
||||
|
||||
// UploadManager is used to manage file uploads on an interval
|
||||
type UploadManager interface {
|
||||
Upload(string) error
|
||||
Start()
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package awsuploader
|
||||
|
||||
// Uploader the functions required to upload to a bucket
|
||||
type Uploader interface {
|
||||
//Upload a file to the bucket
|
||||
Upload(string) error
|
||||
}
|
||||
+8
-5
@@ -12,10 +12,13 @@ import (
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
const LogFieldOriginURL = "originURL"
|
||||
|
||||
type StartOptions struct {
|
||||
OriginURL string
|
||||
Headers http.Header
|
||||
@@ -49,7 +52,7 @@ func (c *StdinoutStream) Write(p []byte) (int, error) {
|
||||
// Helper to allow defering the response close with a check that the resp is not nil
|
||||
func closeRespBody(resp *http.Response) {
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +106,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()
|
||||
remoteConn.ServeStream(options, c)
|
||||
_ = remoteConn.ServeStream(options, c)
|
||||
}
|
||||
|
||||
// IsAccessResponse checks the http Response to see if the url location
|
||||
@@ -125,13 +128,13 @@ func IsAccessResponse(resp *http.Response) bool {
|
||||
}
|
||||
|
||||
// BuildAccessRequest builds an HTTP request with the Access token set
|
||||
func BuildAccessRequest(options *StartOptions, logger logger.Service) (*http.Request, error) {
|
||||
func BuildAccessRequest(options *StartOptions, log *zerolog.Logger) (*http.Request, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := token.FetchTokenWithRedirect(req.URL, logger)
|
||||
token, err := token.FetchTokenWithRedirect(req.URL, log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
ws "github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -43,8 +43,8 @@ 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 := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
wsConn := NewWSConnection(logger, false)
|
||||
log := zerolog.Nop()
|
||||
wsConn := NewWSConnection(&log, false)
|
||||
ts := newTestWebSocketServer()
|
||||
defer ts.Close()
|
||||
|
||||
@@ -55,10 +55,10 @@ func TestStartClient(t *testing.T) {
|
||||
}
|
||||
err := StartClient(wsConn, buf, options)
|
||||
assert.NoError(t, err)
|
||||
buf.Write([]byte(message))
|
||||
_, _ = buf.Write([]byte(message))
|
||||
|
||||
readBuffer := make([]byte, len(message))
|
||||
buf.Read(readBuffer)
|
||||
_, _ = buf.Read(readBuffer)
|
||||
assert.Equal(t, message, string(readBuffer))
|
||||
}
|
||||
|
||||
@@ -68,9 +68,9 @@ 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 := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
log := zerolog.Nop()
|
||||
shutdownC := make(chan struct{})
|
||||
wsConn := NewWSConnection(logger, false)
|
||||
wsConn := NewWSConnection(&log, false)
|
||||
ts := newTestWebSocketServer()
|
||||
defer ts.Close()
|
||||
options := &StartOptions{
|
||||
@@ -86,10 +86,10 @@ func TestStartServer(t *testing.T) {
|
||||
}()
|
||||
|
||||
conn, err := net.Dial("tcp", listener.Addr().String())
|
||||
conn.Write([]byte(message))
|
||||
_, _ = conn.Write([]byte(message))
|
||||
|
||||
readBuffer := make([]byte, len(message))
|
||||
conn.Read(readBuffer)
|
||||
_, _ = conn.Read(readBuffer)
|
||||
assert.Equal(t, string(readBuffer), message)
|
||||
}
|
||||
|
||||
|
||||
+31
-18
@@ -8,16 +8,17 @@ import (
|
||||
"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/rs/zerolog"
|
||||
)
|
||||
|
||||
// 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 logger.Service
|
||||
log *zerolog.Logger
|
||||
isSocks bool
|
||||
}
|
||||
|
||||
@@ -36,9 +37,9 @@ func (d *wsdialer) Dial(address string) (io.ReadWriteCloser, *socks.AddrSpec, er
|
||||
}
|
||||
|
||||
// NewWSConnection returns a new connection object
|
||||
func NewWSConnection(logger logger.Service, isSocks bool) Connection {
|
||||
func NewWSConnection(log *zerolog.Logger, isSocks bool) Connection {
|
||||
return &Websocket{
|
||||
logger: logger,
|
||||
log: log,
|
||||
isSocks: isSocks,
|
||||
}
|
||||
}
|
||||
@@ -46,9 +47,9 @@ func NewWSConnection(logger logger.Service, 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, ws.logger)
|
||||
wsConn, err := createWebsocketStream(options, ws.log)
|
||||
if err != nil {
|
||||
ws.logger.Errorf("failed to connect to %s with error: %s", options.OriginURL, err)
|
||||
ws.log.Err(err).Str(LogFieldOriginURL, options.OriginURL).Msg("failed to connect to origin")
|
||||
return err
|
||||
}
|
||||
defer wsConn.Close()
|
||||
@@ -58,7 +59,7 @@ func (ws *Websocket) ServeStream(options *StartOptions, conn io.ReadWriter) erro
|
||||
requestHandler := socks.NewRequestHandler(dialer)
|
||||
socksServer := socks.NewConnectionHandler(requestHandler)
|
||||
|
||||
socksServer.Serve(conn)
|
||||
_ = socksServer.Serve(conn)
|
||||
} else {
|
||||
cfwebsocket.Stream(wsConn, conn)
|
||||
}
|
||||
@@ -68,13 +69,13 @@ func (ws *Websocket) ServeStream(options *StartOptions, conn io.ReadWriter) erro
|
||||
// StartServer creates a Websocket server to listen for connections.
|
||||
// This is used on the origin (tunnel) side to take data from the muxer and send it to the origin
|
||||
func (ws *Websocket) StartServer(listener net.Listener, remote string, shutdownC <-chan struct{}) error {
|
||||
return cfwebsocket.StartProxyServer(ws.logger, listener, remote, shutdownC, cfwebsocket.DefaultStreamHandler)
|
||||
return cfwebsocket.StartProxyServer(ws.log, listener, remote, shutdownC, cfwebsocket.DefaultStreamHandler)
|
||||
}
|
||||
|
||||
// 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, logger logger.Service) (*cfwebsocket.Conn, error) {
|
||||
func createWebsocketStream(options *StartOptions, log *zerolog.Logger) (*cfwebsocket.Conn, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -82,12 +83,13 @@ func createWebsocketStream(options *StartOptions, logger logger.Service) (*cfweb
|
||||
req.Header = options.Headers
|
||||
|
||||
dump, err := httputil.DumpRequest(req, false)
|
||||
logger.Debugf("Websocket request: %s", string(dump))
|
||||
log.Debug().Msgf("Websocket request: %s", string(dump))
|
||||
|
||||
wsConn, resp, err := cfwebsocket.ClientConnect(req, nil)
|
||||
defer closeRespBody(resp)
|
||||
|
||||
if err != nil && IsAccessResponse(resp) {
|
||||
wsConn, err = createAccessAuthenticatedStream(options, logger)
|
||||
wsConn, err = createAccessAuthenticatedStream(options, log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -103,8 +105,8 @@ func createWebsocketStream(options *StartOptions, logger logger.Service) (*cfweb
|
||||
// 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, logger logger.Service) (*websocket.Conn, error) {
|
||||
wsConn, resp, err := createAccessWebSocketStream(options, logger)
|
||||
func createAccessAuthenticatedStream(options *StartOptions, log *zerolog.Logger) (*websocket.Conn, error) {
|
||||
wsConn, resp, err := createAccessWebSocketStream(options, log)
|
||||
defer closeRespBody(resp)
|
||||
if err == nil {
|
||||
return wsConn, nil
|
||||
@@ -122,7 +124,7 @@ func createAccessAuthenticatedStream(options *StartOptions, logger logger.Servic
|
||||
if err := token.RemoveTokenIfExists(originReq.URL); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wsConn, resp, err = createAccessWebSocketStream(options, logger)
|
||||
wsConn, resp, err = createAccessWebSocketStream(options, log)
|
||||
defer closeRespBody(resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -132,14 +134,25 @@ func createAccessAuthenticatedStream(options *StartOptions, logger logger.Servic
|
||||
}
|
||||
|
||||
// createAccessWebSocketStream builds an Access request and makes a connection
|
||||
func createAccessWebSocketStream(options *StartOptions, logger logger.Service) (*websocket.Conn, *http.Response, error) {
|
||||
req, err := BuildAccessRequest(options, logger)
|
||||
func createAccessWebSocketStream(options *StartOptions, log *zerolog.Logger) (*websocket.Conn, *http.Response, error) {
|
||||
req, err := BuildAccessRequest(options, log)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
dump, err := httputil.DumpRequest(req, false)
|
||||
logger.Debugf("Access Websocket request: %s", string(dump))
|
||||
log.Debug().Msgf("Access Websocket request: %s", string(dump))
|
||||
|
||||
return cfwebsocket.ClientConnect(req, nil)
|
||||
conn, resp, err := cfwebsocket.ClientConnect(req, nil)
|
||||
|
||||
if resp != nil {
|
||||
r, err := httputil.DumpResponse(resp, true)
|
||||
if r != nil {
|
||||
log.Debug().Msgf("Websocket response: %q", r)
|
||||
} else if err != nil {
|
||||
log.Debug().Msgf("Websocket response error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return conn, resp, err
|
||||
}
|
||||
|
||||
+105
-64
@@ -1,17 +1,32 @@
|
||||
pinned_go: &pinned_go go=1.12.9-1
|
||||
pinned_go: &pinned_go go=1.15.7-1
|
||||
pinned_go_fips: &pinned_go_fips go-fips=1.15.5-3
|
||||
|
||||
build_dir: &build_dir /cfsetup_build
|
||||
default-flavor: stretch
|
||||
default-flavor: buster
|
||||
stretch: &stretch
|
||||
build:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- make cloudflared
|
||||
build-deb:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- make cloudflared-deb
|
||||
build-deb-arm64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
@@ -20,12 +35,12 @@ stretch: &stretch
|
||||
- rubygem-fpm
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export GOARCH=arm64
|
||||
- make cloudflared-deb
|
||||
publish-deb:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
@@ -33,27 +48,31 @@ stretch: &stretch
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- make publish-deb
|
||||
release-linux-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- make release
|
||||
github-release-linux-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
post-cache:
|
||||
pre-cache: &install_pygithub
|
||||
- pip3 install pygithub
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- make github-release
|
||||
release-linux-armv6:
|
||||
build_dir: *build_dir
|
||||
@@ -74,8 +93,8 @@ stretch: &stretch
|
||||
- gcc-arm-linux-gnueabihf
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *install_pygithub
|
||||
post-cache:
|
||||
- pip3 install pygithub
|
||||
- export GOOS=linux
|
||||
- export GOARCH=arm
|
||||
- export CC=arm-linux-gnueabihf-gcc
|
||||
@@ -96,8 +115,8 @@ stretch: &stretch
|
||||
- gcc-multilib
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *install_pygithub
|
||||
post-cache:
|
||||
- pip3 install pygithub
|
||||
- export GOOS=linux
|
||||
- export GOARCH=386
|
||||
- make github-release
|
||||
@@ -118,8 +137,8 @@ stretch: &stretch
|
||||
- gcc-mingw-w64
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *install_pygithub
|
||||
post-cache:
|
||||
- pip3 install pygithub
|
||||
- export GOOS=windows
|
||||
- export GOARCH=amd64
|
||||
- export CC=x86_64-w64-mingw32-gcc
|
||||
@@ -141,74 +160,106 @@ stretch: &stretch
|
||||
- gcc-mingw-w64
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *install_pygithub
|
||||
post-cache:
|
||||
- pip3 install pygithub
|
||||
- export GOOS=windows
|
||||
- export GOARCH=386
|
||||
- export CC=i686-w64-mingw32-gcc-win32
|
||||
- make github-release
|
||||
test:
|
||||
github-release-linux-arm64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- crossbuild-essential-armhf
|
||||
- g++-aarch64-linux-gnu
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *install_pygithub
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=arm64
|
||||
- export CC=aarch64-linux-gnu-gcc
|
||||
- make github-release
|
||||
github-release-macos-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *install_pygithub
|
||||
post-cache:
|
||||
- make github-mac-upload
|
||||
test:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
- gotest-to-teamcity
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
# cd to a non-module directory: https://github.com/golang/go/issues/24250
|
||||
- (cd / && go get github.com/BurntSushi/go-sumtype)
|
||||
- export PATH="$HOME/go/bin:$PATH"
|
||||
- make test
|
||||
- make test | gotest-to-teamcity
|
||||
update-homebrew:
|
||||
builddeps:
|
||||
- openssh-client
|
||||
- s3cmd
|
||||
post-cache:
|
||||
- .teamcity/update-homebrew.sh
|
||||
|
||||
jessie: *stretch
|
||||
github-message-release:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *install_pygithub
|
||||
post-cache:
|
||||
- make github-message
|
||||
build-junos:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- python3
|
||||
- genisoimage
|
||||
- jetez
|
||||
pre-cache:
|
||||
- ln -s /usr/bin/genisoimage /usr/bin/mkisofs
|
||||
post-cache:
|
||||
- export GOOS=freebsd
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared-junos
|
||||
publish-junos:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- python3
|
||||
- genisoimage
|
||||
- jetez
|
||||
- s4cmd
|
||||
pre-cache:
|
||||
- ln -s /usr/bin/genisoimage /usr/bin/mkisofs
|
||||
post-cache:
|
||||
- export GOOS=freebsd
|
||||
- export GOARCH=amd64
|
||||
- make publish-cloudflared-junos
|
||||
|
||||
buster: *stretch
|
||||
|
||||
centos-6:
|
||||
build-rpm:
|
||||
build_dir: *build_dir
|
||||
builddeps: &el6_builddeps
|
||||
- https://dl.fedoraproject.org/pub/epel/epel-release-latest-6.noarch.rpm
|
||||
post-cache:
|
||||
- source /usr/local/rvm/scripts/rvm
|
||||
- rvm use 1.9
|
||||
- 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 cloudflared-rpm
|
||||
publish-rpm:
|
||||
build_dir: *build_dir
|
||||
builddeps: *el6_builddeps
|
||||
post-cache:
|
||||
- source /usr/local/rvm/scripts/rvm
|
||||
- rvm use 1.9
|
||||
- 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
|
||||
|
||||
|
||||
bullseye: *stretch
|
||||
centos-7:
|
||||
publish-rpm:
|
||||
build_dir: *build_dir
|
||||
builddeps: &el7_builddeps
|
||||
- https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
|
||||
pre-cache:
|
||||
- yum install -y fakeroot
|
||||
- wget https://golang.org/dl/go1.15.7.linux-amd64.tar.gz -P /tmp/
|
||||
- tar -C /usr/local -xzf /tmp/go1.15.7.linux-amd64.tar.gz
|
||||
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
|
||||
@@ -216,22 +267,12 @@ centos-7:
|
||||
build-rpm:
|
||||
build_dir: *build_dir
|
||||
builddeps: *el7_builddeps
|
||||
pre-cache:
|
||||
- yum install -y fakeroot
|
||||
- wget https://golang.org/dl/go1.15.7.linux-amd64.tar.gz -P /tmp/
|
||||
- tar -C /usr/local -xzf /tmp/go1.15.7.linux-amd64.tar.gz
|
||||
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 cloudflared-rpm
|
||||
|
||||
# cfsetup compose
|
||||
default-stack: test_dbconnect
|
||||
test_dbconnect:
|
||||
compose:
|
||||
up-args:
|
||||
- --renew-anon-volumes
|
||||
- --abort-on-container-exit
|
||||
- --exit-code-from=cloudflared
|
||||
files:
|
||||
- dbconnect_tests/dbconnect.yaml
|
||||
|
||||
@@ -2,31 +2,30 @@ package access
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"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"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
LogFieldHost = "host"
|
||||
)
|
||||
|
||||
// StartForwarder starts a client side websocket forward
|
||||
func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, logger logger.Service) error {
|
||||
validURLString, err := validation.ValidateUrl(forwarder.Listener)
|
||||
func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, log *zerolog.Logger) error {
|
||||
validURL, err := validation.ValidateUrl(forwarder.Listener)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error validating origin URL")
|
||||
}
|
||||
|
||||
validURL, err := url.Parse(validURLString)
|
||||
if err != nil {
|
||||
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 != "" {
|
||||
@@ -47,9 +46,9 @@ func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, logger
|
||||
}
|
||||
|
||||
// we could add a cmd line variable for this bool if we want the SOCK5 server to be on the client side
|
||||
wsConn := carrier.NewWSConnection(logger, false)
|
||||
wsConn := carrier.NewWSConnection(log, false)
|
||||
|
||||
logger.Infof("Start Websocket listener on: %s", validURL.Host)
|
||||
log.Info().Str(LogFieldHost, validURL.Host).Msg("Start Websocket listener")
|
||||
return carrier.StartForwarder(wsConn, validURL.Host, shutdown, options)
|
||||
}
|
||||
|
||||
@@ -58,22 +57,7 @@ func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, logger
|
||||
// useful for proxying other protocols (like ssh) over websockets
|
||||
// (which you can put Access in front of)
|
||||
func ssh(c *cli.Context) error {
|
||||
logDirectory, logLevel := config.FindLogSettings()
|
||||
|
||||
flagLogDirectory := c.String(sshLogDirectoryFlag)
|
||||
if flagLogDirectory != "" {
|
||||
logDirectory = flagLogDirectory
|
||||
}
|
||||
|
||||
flagLogLevel := c.String(sshLogLevelFlag)
|
||||
if flagLogLevel != "" {
|
||||
logLevel = flagLogLevel
|
||||
}
|
||||
|
||||
logger, err := logger.New(logger.DefaultFile(logDirectory), logger.LogLevelString(logLevel))
|
||||
if err != nil {
|
||||
return cliutil.PrintLoggerSetupError("error setting up logger", err)
|
||||
}
|
||||
log := logger.CreateSSHLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
// get the hostname from the cmdline and error out if its not provided
|
||||
rawHostName := c.String(sshHostnameFlag)
|
||||
@@ -103,24 +87,19 @@ func ssh(c *cli.Context) error {
|
||||
}
|
||||
|
||||
// we could add a cmd line variable for this bool if we want the SOCK5 server to be on the client side
|
||||
wsConn := carrier.NewWSConnection(logger, false)
|
||||
wsConn := carrier.NewWSConnection(log, false)
|
||||
|
||||
if c.NArg() > 0 || c.IsSet(sshURLFlag) {
|
||||
localForwarder, err := config.ValidateUrl(c, true)
|
||||
forwarder, err := config.ValidateUrl(c, true)
|
||||
if err != nil {
|
||||
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.Errorf("Error validating origin URL: %s", err)
|
||||
log.Err(err).Msg("Error validating origin URL")
|
||||
return errors.Wrap(err, "error validating origin URL")
|
||||
}
|
||||
|
||||
logger.Infof("Start Websocket listener on: %s", forwarder.Host)
|
||||
log.Info().Str(LogFieldHost, forwarder.Host).Msg("Start Websocket listener")
|
||||
err = carrier.StartForwarder(wsConn, forwarder.Host, shutdownC, options)
|
||||
if err != nil {
|
||||
logger.Errorf("Error on Websocket listener: %s", err)
|
||||
log.Err(err).Msg("Error on Websocket listener")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -17,24 +17,23 @@ import (
|
||||
"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/getsentry/raven-go"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
"golang.org/x/net/idna"
|
||||
)
|
||||
|
||||
const (
|
||||
sshHostnameFlag = "hostname"
|
||||
sshDestinationFlag = "destination"
|
||||
sshURLFlag = "url"
|
||||
sshHeaderFlag = "header"
|
||||
sshTokenIDFlag = "service-token-id"
|
||||
sshTokenSecretFlag = "service-token-secret"
|
||||
sshGenCertFlag = "short-lived-cert"
|
||||
sshLogDirectoryFlag = "log-directory"
|
||||
sshLogLevelFlag = "log-level"
|
||||
sshConfigTemplate = `
|
||||
sshHostnameFlag = "hostname"
|
||||
sshDestinationFlag = "destination"
|
||||
sshURLFlag = "url"
|
||||
sshHeaderFlag = "header"
|
||||
sshTokenIDFlag = "service-token-id"
|
||||
sshTokenSecretFlag = "service-token-secret"
|
||||
sshGenCertFlag = "short-lived-cert"
|
||||
sshConfigTemplate = `
|
||||
Add to your {{.Home}}/.ssh/config:
|
||||
|
||||
Host {{.Hostname}}
|
||||
@@ -56,12 +55,11 @@ const sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b@sentry.io/189878"
|
||||
|
||||
var (
|
||||
shutdownC chan struct{}
|
||||
graceShutdownC chan struct{}
|
||||
)
|
||||
|
||||
// Init will initialize and store vars from the main program
|
||||
func Init(s, g chan struct{}) {
|
||||
shutdownC, graceShutdownC = s, g
|
||||
func Init(shutdown chan struct{}) {
|
||||
shutdownC = shutdown
|
||||
}
|
||||
|
||||
// Flags return the global flags for Access related commands (hopefully none)
|
||||
@@ -157,12 +155,12 @@ func Commands() []*cli.Command {
|
||||
Usage: "specify an Access service token secret you wish to use.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshLogDirectoryFlag,
|
||||
Name: logger.LogSSHDirectoryFlag,
|
||||
Aliases: []string{"logfile"}, //added to match the tunnel side
|
||||
Usage: "Save application log to this directory for reporting issues.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshLogLevelFlag,
|
||||
Name: logger.LogSSHLevelFlag,
|
||||
Aliases: []string{"loglevel"}, //added to match the tunnel side
|
||||
Usage: "Application logging level {fatal, error, info, debug}. ",
|
||||
},
|
||||
@@ -207,24 +205,21 @@ func login(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
args := c.Args()
|
||||
rawURL := ensureURLScheme(args.First())
|
||||
appURL, err := url.Parse(rawURL)
|
||||
if args.Len() < 1 || err != nil {
|
||||
logger.Errorf("Please provide the url of the Access application\n")
|
||||
log.Error().Msg("Please provide the url of the Access application")
|
||||
return err
|
||||
}
|
||||
if err := verifyTokenAtEdge(appURL, c, logger); err != nil {
|
||||
logger.Errorf("Could not verify token: %s", err)
|
||||
if err := verifyTokenAtEdge(appURL, c, log); err != nil {
|
||||
log.Err(err).Msg("Could not verify token")
|
||||
return err
|
||||
}
|
||||
|
||||
cfdToken, err := token.GetTokenIfExists(appURL)
|
||||
cfdToken, err := token.GetAppTokenIfExists(appURL)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Unable to find token for provided application.")
|
||||
return err
|
||||
@@ -252,32 +247,29 @@ func curl(c *cli.Context) error {
|
||||
if err := raven.SetDSN(sentryDSN); err != nil {
|
||||
return err
|
||||
}
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
args := c.Args()
|
||||
if args.Len() < 1 {
|
||||
logger.Error("Please provide the access app and command you wish to run.")
|
||||
log.Error().Msg("Please provide the access app and command you wish to run.")
|
||||
return errors.New("incorrect args")
|
||||
}
|
||||
|
||||
cmdArgs, allowRequest := parseAllowRequest(args.Slice())
|
||||
appURL, err := getAppURL(cmdArgs, logger)
|
||||
appURL, err := getAppURL(cmdArgs, log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tok, err := token.GetTokenIfExists(appURL)
|
||||
tok, err := token.GetAppTokenIfExists(appURL)
|
||||
if err != nil || tok == "" {
|
||||
if allowRequest {
|
||||
logger.Info("You don't have an Access token set. Please run access token <access application> to fetch one.")
|
||||
log.Info().Msg("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, logger)
|
||||
tok, err = token.FetchToken(appURL, log)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to refresh token: %s", err)
|
||||
log.Err(err).Msg("Failed to refresh token")
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -297,9 +289,9 @@ func generateToken(c *cli.Context) error {
|
||||
fmt.Fprintln(os.Stderr, "Please provide a url.")
|
||||
return err
|
||||
}
|
||||
tok, err := token.GetTokenIfExists(appURL)
|
||||
tok, err := token.GetAppTokenIfExists(appURL)
|
||||
if err != nil || tok == "" {
|
||||
fmt.Fprintln(os.Stderr, "Unable to find token for provided application. Please run token command to generate token.")
|
||||
fmt.Fprintln(os.Stderr, "Unable to find token for provided application. Please run login command to generate token.")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -331,10 +323,7 @@ func sshConfig(c *cli.Context) error {
|
||||
|
||||
// sshGen generates a short lived certificate for provided hostname
|
||||
func sshGen(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
// get the hostname from the cmdline and error out if its not provided
|
||||
rawHostName := c.String(sshHostnameFlag)
|
||||
@@ -351,7 +340,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.FetchTokenWithRedirect(fetchTokenURL, logger)
|
||||
cfdToken, err := token.FetchTokenWithRedirect(fetchTokenURL, log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -364,15 +353,15 @@ func sshGen(c *cli.Context) error {
|
||||
}
|
||||
|
||||
// getAppURL will pull the appURL needed for fetching a user's Access token
|
||||
func getAppURL(cmdArgs []string, logger logger.Service) (*url.URL, error) {
|
||||
func getAppURL(cmdArgs []string, log *zerolog.Logger) (*url.URL, error) {
|
||||
if len(cmdArgs) < 1 {
|
||||
logger.Error("Please provide a valid URL as the first argument to curl.")
|
||||
log.Error().Msg("Please provide a valid URL as the first argument to curl.")
|
||||
return nil, errors.New("not a valid url")
|
||||
}
|
||||
|
||||
u, err := processURL(cmdArgs[0])
|
||||
if err != nil {
|
||||
logger.Error("Please provide a valid URL as the first argument to curl.")
|
||||
log.Error().Msg("Please provide a valid URL as the first argument to curl.")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -438,7 +427,7 @@ 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, logger logger.Service) error {
|
||||
func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context, log *zerolog.Logger) error {
|
||||
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
|
||||
if c.IsSet(sshTokenIDFlag) {
|
||||
headers.Add(h2mux.CFAccessClientIDHeader, c.String(sshTokenIDFlag))
|
||||
@@ -448,7 +437,7 @@ func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context, logger logger.Service) e
|
||||
}
|
||||
options := &carrier.StartOptions{OriginURL: appUrl.String(), Headers: headers}
|
||||
|
||||
if valid, err := isTokenValid(options, logger); err != nil {
|
||||
if valid, err := isTokenValid(options, log); err != nil {
|
||||
return err
|
||||
} else if valid {
|
||||
return nil
|
||||
@@ -458,7 +447,7 @@ func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context, logger logger.Service) e
|
||||
return err
|
||||
}
|
||||
|
||||
if valid, err := isTokenValid(options, logger); err != nil {
|
||||
if valid, err := isTokenValid(options, log); err != nil {
|
||||
return err
|
||||
} else if !valid {
|
||||
return errors.New("failed to verify token")
|
||||
@@ -468,8 +457,8 @@ func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context, logger logger.Service) e
|
||||
}
|
||||
|
||||
// isTokenValid makes a request to the origin and returns true if the response was not a 302.
|
||||
func isTokenValid(options *carrier.StartOptions, logger logger.Service) (bool, error) {
|
||||
req, err := carrier.BuildAccessRequest(options, logger)
|
||||
func isTokenValid(options *carrier.StartOptions, log *zerolog.Logger) (bool, error) {
|
||||
req, err := carrier.BuildAccessRequest(options, log)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "Could not create access request")
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ package main
|
||||
import (
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// ForwardServiceType is used to identify what kind of overwatch service this is
|
||||
@@ -15,12 +16,12 @@ const ForwardServiceType = "forward"
|
||||
type ForwarderService struct {
|
||||
forwarder config.Forwarder
|
||||
shutdown chan struct{}
|
||||
logger logger.Service
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
// NewForwardService creates a new forwarder service
|
||||
func NewForwardService(f config.Forwarder, logger logger.Service) *ForwarderService {
|
||||
return &ForwarderService{forwarder: f, shutdown: make(chan struct{}, 1), logger: logger}
|
||||
func NewForwardService(f config.Forwarder, log *zerolog.Logger) *ForwarderService {
|
||||
return &ForwarderService{forwarder: f, shutdown: make(chan struct{}, 1), log: log}
|
||||
}
|
||||
|
||||
// Name is used to figure out this service is related to the others (normally the addr it binds to)
|
||||
@@ -46,5 +47,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, s.logger)
|
||||
return access.StartForwarder(s.forwarder, s.shutdown, s.log)
|
||||
}
|
||||
|
||||
@@ -2,12 +2,19 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// ResolverServiceType is used to identify what kind of overwatch service this is
|
||||
const ResolverServiceType = "resolver"
|
||||
const (
|
||||
// ResolverServiceType is used to identify what kind of overwatch service this is
|
||||
ResolverServiceType = "resolver"
|
||||
|
||||
LogFieldResolverAddress = "resolverAddress"
|
||||
LogFieldResolverPort = "resolverPort"
|
||||
LogFieldResolverMaxUpstreamConns = "resolverMaxUpstreamConns"
|
||||
)
|
||||
|
||||
// ResolverService is used to wrap the tunneldns package's DNS over HTTP
|
||||
// into a service model for the overwatch package.
|
||||
@@ -15,14 +22,14 @@ const ResolverServiceType = "resolver"
|
||||
type ResolverService struct {
|
||||
resolver config.DNSResolver
|
||||
shutdown chan struct{}
|
||||
logger logger.Service
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
// NewResolverService creates a new resolver service
|
||||
func NewResolverService(r config.DNSResolver, logger logger.Service) *ResolverService {
|
||||
func NewResolverService(r config.DNSResolver, log *zerolog.Logger) *ResolverService {
|
||||
return &ResolverService{resolver: r,
|
||||
shutdown: make(chan struct{}),
|
||||
logger: logger,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +58,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.logger)
|
||||
s.resolver.UpstreamsOrDefault(), s.resolver.BootstrapsOrDefault(), s.resolver.MaxUpstreamConnectionsOrDefault(), s.log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -60,14 +67,21 @@ func (s *ResolverService) Run() error {
|
||||
readySignal := make(chan struct{})
|
||||
err = l.Start(readySignal)
|
||||
if err != nil {
|
||||
l.Stop()
|
||||
_ = l.Stop()
|
||||
return err
|
||||
}
|
||||
<-readySignal
|
||||
s.logger.Infof("start resolver on: %s:%d", s.resolver.AddressOrDefault(), s.resolver.PortOrDefault())
|
||||
|
||||
resolverLog := s.log.With().
|
||||
Str(LogFieldResolverAddress, s.resolver.AddressOrDefault()).
|
||||
Uint16(LogFieldResolverPort, s.resolver.PortOrDefault()).
|
||||
Int(LogFieldResolverMaxUpstreamConns, s.resolver.MaxUpstreamConnectionsOrDefault()).
|
||||
Logger()
|
||||
|
||||
resolverLog.Info().Msg("Starting resolver")
|
||||
|
||||
// wait for shutdown signal
|
||||
<-s.shutdown
|
||||
s.logger.Infof("shutdown on: %s:%d", s.resolver.AddressOrDefault(), s.resolver.PortOrDefault())
|
||||
resolverLog.Info().Msg("Shutting down resolver")
|
||||
return l.Stop()
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/overwatch"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// AppService is the main service that runs when no command lines flags are passed to cloudflared
|
||||
@@ -13,17 +14,17 @@ type AppService struct {
|
||||
serviceManager overwatch.Manager
|
||||
shutdownC chan struct{}
|
||||
configUpdateChan chan config.Root
|
||||
logger logger.Service
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
// NewAppService creates a new AppService with needed supporting services
|
||||
func NewAppService(configManager config.Manager, serviceManager overwatch.Manager, shutdownC chan struct{}, logger logger.Service) *AppService {
|
||||
func NewAppService(configManager config.Manager, serviceManager overwatch.Manager, shutdownC chan struct{}, log *zerolog.Logger) *AppService {
|
||||
return &AppService{
|
||||
configManager: configManager,
|
||||
serviceManager: serviceManager,
|
||||
shutdownC: shutdownC,
|
||||
configUpdateChan: make(chan config.Root),
|
||||
logger: logger,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +38,7 @@ func (s *AppService) Run() error {
|
||||
func (s *AppService) Shutdown() error {
|
||||
s.configManager.Shutdown()
|
||||
s.shutdownC <- struct{}{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -66,14 +68,14 @@ func (s *AppService) handleConfigUpdate(c config.Root) {
|
||||
// handle the client forward listeners
|
||||
activeServices := map[string]struct{}{}
|
||||
for _, f := range c.Forwarders {
|
||||
service := NewForwardService(f, s.logger)
|
||||
service := NewForwardService(f, s.log)
|
||||
s.serviceManager.Add(service)
|
||||
activeServices[service.Name()] = struct{}{}
|
||||
}
|
||||
|
||||
// handle resolver changes
|
||||
if c.Resolver.Enabled {
|
||||
service := NewResolverService(c.Resolver, s.logger)
|
||||
service := NewResolverService(c.Resolver, s.log)
|
||||
s.serviceManager.Add(service)
|
||||
activeServices[service.Name()] = struct{}{}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package buildinfo
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
"runtime"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
type BuildInfo struct {
|
||||
@@ -22,7 +21,7 @@ func GetBuildInfo(cloudflaredVersion string) *BuildInfo {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
func (bi *BuildInfo) Log(log *zerolog.Logger) {
|
||||
log.Info().Msgf("Version %s", bi.CloudflaredVersion)
|
||||
log.Info().Msgf("GOOS: %s, GOVersion: %s, GoArch: %s", bi.GoOS, bi.GoVersion, bi.GoArch)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package cliutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func RemovedCommand(name string) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: name,
|
||||
Action: ErrorHandler(func(context *cli.Context) error {
|
||||
return cli.Exit(
|
||||
fmt.Sprintf("%s command is no longer supported by cloudflared. Consult Argo Tunnel documentation for possible alternative solutions.", name),
|
||||
-1,
|
||||
)
|
||||
}),
|
||||
Description: fmt.Sprintf("%s is deprecated", name),
|
||||
Hidden: true,
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,7 @@ package cliutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
type usageError string
|
||||
@@ -31,25 +27,11 @@ func ErrorHandler(actionFunc cli.ActionFunc) cli.ActionFunc {
|
||||
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)
|
||||
err = cli.Exit(msg, -1)
|
||||
} else if _, ok := err.(cli.ExitCoder); !ok {
|
||||
err = cli.Exit(err.Error(), 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)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
"gopkg.in/urfave/cli.v2/altsrc"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -26,10 +30,18 @@ var (
|
||||
|
||||
// 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", "/etc/cloudflared", DefaultUnixConfigLocation}
|
||||
defaultUserConfigDirs = []string{"~/.cloudflared", "~/.cloudflare-warp", "~/cloudflare-warp"}
|
||||
defaultNixConfigDirs = []string{"/etc/cloudflared", DefaultUnixConfigLocation}
|
||||
|
||||
ErrNoConfigFile = fmt.Errorf("Cannot determine default configuration path. No file %v in %v", DefaultConfigFiles, DefaultConfigSearchDirectories())
|
||||
)
|
||||
|
||||
const DefaultCredentialFile = "cert.pem"
|
||||
const (
|
||||
DefaultCredentialFile = "cert.pem"
|
||||
|
||||
// BastionFlag is to enable bastion, or jump host, operation
|
||||
BastionFlag = "bastion"
|
||||
)
|
||||
|
||||
// DefaultConfigDirectory returns the default directory of the config file
|
||||
func DefaultConfigDirectory() string {
|
||||
@@ -63,6 +75,16 @@ func DefaultConfigPath() string {
|
||||
return filepath.Join(dir, DefaultConfigFiles[0])
|
||||
}
|
||||
|
||||
// DefaultConfigSearchDirectories returns the default folder locations of the config
|
||||
func DefaultConfigSearchDirectories() []string {
|
||||
dirs := make([]string, len(defaultUserConfigDirs))
|
||||
copy(dirs, defaultUserConfigDirs)
|
||||
if runtime.GOOS != "windows" {
|
||||
dirs = append(dirs, defaultNixConfigDirs...)
|
||||
}
|
||||
return dirs
|
||||
}
|
||||
|
||||
// FileExists checks to see if a file exist at the provided path.
|
||||
func FileExists(path string) (bool, error) {
|
||||
f, err := os.Open(path)
|
||||
@@ -73,23 +95,15 @@ func FileExists(path string) (bool, error) {
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
f.Close()
|
||||
_ = f.Close()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// FindInputSourceContext pulls the input source from the config flag.
|
||||
func FindInputSourceContext(context *cli.Context) (altsrc.InputSourceContext, error) {
|
||||
if context.String("config") != "" {
|
||||
return altsrc.NewYamlSourceFromFile(context.String("config"))
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// FindDefaultConfigPath returns the first path that contains a config file.
|
||||
// If none of the combination of DefaultConfigDirs and DefaultConfigFiles
|
||||
// If none of the combination of DefaultConfigSearchDirectories() and DefaultConfigFiles
|
||||
// contains a config file, return empty string.
|
||||
func FindDefaultConfigPath() string {
|
||||
for _, configDir := range DefaultConfigDirs {
|
||||
for _, configDir := range DefaultConfigSearchDirectories() {
|
||||
for _, configFile := range DefaultConfigFiles {
|
||||
dirPath, err := homedir.Expand(configDir)
|
||||
if err != nil {
|
||||
@@ -124,7 +138,7 @@ func FindOrCreateConfigPath() string {
|
||||
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
|
||||
_ = 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,
|
||||
@@ -137,35 +151,6 @@ func FindOrCreateConfigPath() string {
|
||||
return path
|
||||
}
|
||||
|
||||
// FindLogSettings gets the log directory and level from the config file
|
||||
func FindLogSettings() (string, string) {
|
||||
configPath := FindOrCreateConfigPath()
|
||||
defaultDirectory := DefaultLogDirectory()
|
||||
defaultLevel := "info"
|
||||
|
||||
file, err := os.Open(configPath)
|
||||
if err != nil {
|
||||
return defaultDirectory, defaultLevel
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var config Root
|
||||
if err := yaml.NewDecoder(file).Decode(&config); err != nil {
|
||||
return defaultDirectory, defaultLevel
|
||||
}
|
||||
|
||||
directory := defaultDirectory
|
||||
if config.LogDirectory != "" {
|
||||
directory = config.LogDirectory
|
||||
}
|
||||
|
||||
level := defaultLevel
|
||||
if config.LogLevel != "" {
|
||||
level = config.LogLevel
|
||||
}
|
||||
return directory, level
|
||||
}
|
||||
|
||||
// ValidateUnixSocket ensures --unix-socket param is used exclusively
|
||||
// i.e. it fails if a user specifies both --url and --unix-socket
|
||||
func ValidateUnixSocket(c *cli.Context) (string, error) {
|
||||
@@ -177,14 +162,214 @@ 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, allowFromArgs bool) (string, error) {
|
||||
func ValidateUrl(c *cli.Context, allowURLFromArgs bool) (*url.URL, error) {
|
||||
var url = c.String("url")
|
||||
if allowFromArgs && c.NArg() > 0 {
|
||||
if allowURLFromArgs && 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.")
|
||||
return nil, errors.New("Specified origin urls using both --url and argument. Decide which one you want, I can only support one.")
|
||||
}
|
||||
url = c.Args().Get(0)
|
||||
}
|
||||
validUrl, err := validation.ValidateUrl(url)
|
||||
return validUrl, err
|
||||
}
|
||||
|
||||
type UnvalidatedIngressRule struct {
|
||||
Hostname string
|
||||
Path string
|
||||
Service string
|
||||
OriginRequest OriginRequestConfig `yaml:"originRequest"`
|
||||
}
|
||||
|
||||
// OriginRequestConfig is a set of optional fields that users may set to
|
||||
// customize how cloudflared sends requests to origin services. It is used to set
|
||||
// up general config that apply to all rules, and also, specific per-rule
|
||||
// config.
|
||||
// Note: To specify a time.Duration in go-yaml, use e.g. "3s" or "24h".
|
||||
type OriginRequestConfig struct {
|
||||
// HTTP proxy timeout for establishing a new connection
|
||||
ConnectTimeout *time.Duration `yaml:"connectTimeout"`
|
||||
// HTTP proxy timeout for completing a TLS handshake
|
||||
TLSTimeout *time.Duration `yaml:"tlsTimeout"`
|
||||
// HTTP proxy TCP keepalive duration
|
||||
TCPKeepAlive *time.Duration `yaml:"tcpKeepAlive"`
|
||||
// HTTP proxy should disable "happy eyeballs" for IPv4/v6 fallback
|
||||
NoHappyEyeballs *bool `yaml:"noHappyEyeballs"`
|
||||
// HTTP proxy maximum keepalive connection pool size
|
||||
KeepAliveConnections *int `yaml:"keepAliveConnections"`
|
||||
// HTTP proxy timeout for closing an idle connection
|
||||
KeepAliveTimeout *time.Duration `yaml:"keepAliveTimeout"`
|
||||
// Sets the HTTP Host header for the local webserver.
|
||||
HTTPHostHeader *string `yaml:"httpHostHeader"`
|
||||
// Hostname on the origin server certificate.
|
||||
OriginServerName *string `yaml:"originServerName"`
|
||||
// Path to the CA for the certificate of your origin.
|
||||
// This option should be used only if your certificate is not signed by Cloudflare.
|
||||
CAPool *string `yaml:"caPool"`
|
||||
// Disables TLS verification of the certificate presented by your origin.
|
||||
// Will allow any certificate from the origin to be accepted.
|
||||
// Note: The connection from your machine to Cloudflare's Edge is still encrypted.
|
||||
NoTLSVerify *bool `yaml:"noTLSVerify"`
|
||||
// Disables chunked transfer encoding.
|
||||
// Useful if you are running a WSGI server.
|
||||
DisableChunkedEncoding *bool `yaml:"disableChunkedEncoding"`
|
||||
// Runs as jump host
|
||||
BastionMode *bool `yaml:"bastionMode"`
|
||||
// Listen address for the proxy.
|
||||
ProxyAddress *string `yaml:"proxyAddress"`
|
||||
// Listen port for the proxy.
|
||||
ProxyPort *uint `yaml:"proxyPort"`
|
||||
// Valid options are 'socks' or empty.
|
||||
ProxyType *string `yaml:"proxyType"`
|
||||
}
|
||||
|
||||
type Configuration struct {
|
||||
TunnelID string `yaml:"tunnel"`
|
||||
Ingress []UnvalidatedIngressRule
|
||||
OriginRequest OriginRequestConfig `yaml:"originRequest"`
|
||||
sourceFile string
|
||||
}
|
||||
|
||||
type configFileSettings struct {
|
||||
Configuration `yaml:",inline"`
|
||||
// older settings will be aggregated into the generic map, should be read via cli.Context
|
||||
Settings map[string]interface{} `yaml:",inline"`
|
||||
}
|
||||
|
||||
func (c *Configuration) Source() string {
|
||||
return c.sourceFile
|
||||
}
|
||||
|
||||
func (c *configFileSettings) Int(name string) (int, error) {
|
||||
if raw, ok := c.Settings[name]; ok {
|
||||
if v, ok := raw.(int); ok {
|
||||
return v, nil
|
||||
}
|
||||
return 0, fmt.Errorf("expected int found %T for %s", raw, name)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (c *configFileSettings) Duration(name string) (time.Duration, error) {
|
||||
if raw, ok := c.Settings[name]; ok {
|
||||
switch v := raw.(type) {
|
||||
case time.Duration:
|
||||
return v, nil
|
||||
case string:
|
||||
return time.ParseDuration(v)
|
||||
}
|
||||
return 0, fmt.Errorf("expected duration found %T for %s", raw, name)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (c *configFileSettings) Float64(name string) (float64, error) {
|
||||
if raw, ok := c.Settings[name]; ok {
|
||||
if v, ok := raw.(float64); ok {
|
||||
return v, nil
|
||||
}
|
||||
return 0, fmt.Errorf("expected float found %T for %s", raw, name)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (c *configFileSettings) String(name string) (string, error) {
|
||||
if raw, ok := c.Settings[name]; ok {
|
||||
if v, ok := raw.(string); ok {
|
||||
return v, nil
|
||||
}
|
||||
return "", fmt.Errorf("expected string found %T for %s", raw, name)
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (c *configFileSettings) StringSlice(name string) ([]string, error) {
|
||||
if raw, ok := c.Settings[name]; ok {
|
||||
if slice, ok := raw.([]interface{}); ok {
|
||||
strSlice := make([]string, len(slice))
|
||||
for i, v := range slice {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected string, found %T for %v", i, v)
|
||||
}
|
||||
strSlice[i] = str
|
||||
}
|
||||
return strSlice, nil
|
||||
}
|
||||
return nil, fmt.Errorf("expected string slice found %T for %s", raw, name)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *configFileSettings) IntSlice(name string) ([]int, error) {
|
||||
if raw, ok := c.Settings[name]; ok {
|
||||
if slice, ok := raw.([]interface{}); ok {
|
||||
intSlice := make([]int, len(slice))
|
||||
for i, v := range slice {
|
||||
str, ok := v.(int)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected int, found %T for %v ", v, v)
|
||||
}
|
||||
intSlice[i] = str
|
||||
}
|
||||
return intSlice, nil
|
||||
}
|
||||
if v, ok := raw.([]int); ok {
|
||||
return v, nil
|
||||
}
|
||||
return nil, fmt.Errorf("expected int slice found %T for %s", raw, name)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *configFileSettings) Generic(name string) (cli.Generic, error) {
|
||||
return nil, errors.New("option type Generic not supported")
|
||||
}
|
||||
|
||||
func (c *configFileSettings) Bool(name string) (bool, error) {
|
||||
if raw, ok := c.Settings[name]; ok {
|
||||
if v, ok := raw.(bool); ok {
|
||||
return v, nil
|
||||
}
|
||||
return false, fmt.Errorf("expected boolean found %T for %s", raw, name)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var configuration configFileSettings
|
||||
|
||||
func GetConfiguration() *Configuration {
|
||||
return &configuration.Configuration
|
||||
}
|
||||
|
||||
// ReadConfigFile returns InputSourceContext initialized from the configuration file.
|
||||
// On repeat calls returns with the same file, returns without reading the file again; however,
|
||||
// if value of "config" flag changes, will read the new config file
|
||||
func ReadConfigFile(c *cli.Context, log *zerolog.Logger) (*configFileSettings, error) {
|
||||
configFile := c.String("config")
|
||||
if configuration.Source() == configFile || configFile == "" {
|
||||
if configuration.Source() == "" {
|
||||
return nil, ErrNoConfigFile
|
||||
}
|
||||
return &configuration, nil
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Loading configuration from %s", configFile)
|
||||
file, err := os.Open(configFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = ErrNoConfigFile
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
if err := yaml.NewDecoder(file).Decode(&configuration); err != nil {
|
||||
if err == io.EOF {
|
||||
log.Error().Msgf("Configuration file %s was empty", configFile)
|
||||
return &configuration, nil
|
||||
}
|
||||
return nil, errors.Wrap(err, "error parsing YAML in config file at "+configFile)
|
||||
}
|
||||
configuration.sourceFile = configFile
|
||||
return &configuration, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func TestConfigFileSettings(t *testing.T) {
|
||||
var (
|
||||
firstIngress = UnvalidatedIngressRule{
|
||||
Hostname: "tunnel1.example.com",
|
||||
Path: "/id",
|
||||
Service: "https://localhost:8000",
|
||||
}
|
||||
secondIngress = UnvalidatedIngressRule{
|
||||
Hostname: "*",
|
||||
Path: "",
|
||||
Service: "https://localhost:8001",
|
||||
}
|
||||
)
|
||||
rawYAML := `
|
||||
tunnel: config-file-test
|
||||
ingress:
|
||||
- hostname: tunnel1.example.com
|
||||
path: /id
|
||||
service: https://localhost:8000
|
||||
- hostname: "*"
|
||||
service: https://localhost:8001
|
||||
retries: 5
|
||||
grace-period: 30s
|
||||
percentage: 3.14
|
||||
hostname: example.com
|
||||
tag:
|
||||
- test
|
||||
- central-1
|
||||
counters:
|
||||
- 123
|
||||
- 456
|
||||
`
|
||||
var config configFileSettings
|
||||
err := yaml.Unmarshal([]byte(rawYAML), &config)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "config-file-test", config.TunnelID)
|
||||
assert.Equal(t, firstIngress, config.Ingress[0])
|
||||
assert.Equal(t, secondIngress, config.Ingress[1])
|
||||
|
||||
retries, err := config.Int("retries")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 5, retries)
|
||||
|
||||
gracePeriod, err := config.Duration("grace-period")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, time.Second*30, gracePeriod)
|
||||
|
||||
percentage, err := config.Float64("percentage")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3.14, percentage)
|
||||
|
||||
hostname, err := config.String("hostname")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "example.com", hostname)
|
||||
|
||||
tags, err := config.StringSlice("tag")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test", tags[0])
|
||||
assert.Equal(t, "central-1", tags[1])
|
||||
|
||||
counters, err := config.IntSlice("counters")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 123, counters[0])
|
||||
assert.Equal(t, 456, counters[1])
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/watcher"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
@@ -26,16 +28,16 @@ type FileManager struct {
|
||||
watcher watcher.Notifier
|
||||
notifier Notifier
|
||||
configPath string
|
||||
logger logger.Service
|
||||
ReadConfig func(string) (Root, error)
|
||||
log *zerolog.Logger
|
||||
ReadConfig func(string, *zerolog.Logger) (Root, error)
|
||||
}
|
||||
|
||||
// NewFileManager creates a config manager
|
||||
func NewFileManager(watcher watcher.Notifier, configPath string, logger logger.Service) (*FileManager, error) {
|
||||
func NewFileManager(watcher watcher.Notifier, configPath string, log *zerolog.Logger) (*FileManager, error) {
|
||||
m := &FileManager{
|
||||
watcher: watcher,
|
||||
configPath: configPath,
|
||||
logger: logger,
|
||||
log: log,
|
||||
ReadConfig: readConfigFromPath,
|
||||
}
|
||||
err := watcher.Add(configPath)
|
||||
@@ -59,7 +61,7 @@ func (m *FileManager) Start(notifier Notifier) error {
|
||||
|
||||
// GetConfig reads the yaml file from the disk
|
||||
func (m *FileManager) GetConfig() (Root, error) {
|
||||
return m.ReadConfig(m.configPath)
|
||||
return m.ReadConfig(m.configPath, m.log)
|
||||
}
|
||||
|
||||
// Shutdown stops the watcher
|
||||
@@ -67,7 +69,7 @@ func (m *FileManager) Shutdown() {
|
||||
m.watcher.Shutdown()
|
||||
}
|
||||
|
||||
func readConfigFromPath(configPath string) (Root, error) {
|
||||
func readConfigFromPath(configPath string, log *zerolog.Logger) (Root, error) {
|
||||
if configPath == "" {
|
||||
return Root{}, errors.New("unable to find config file")
|
||||
}
|
||||
@@ -80,7 +82,11 @@ func readConfigFromPath(configPath string) (Root, error) {
|
||||
|
||||
var config Root
|
||||
if err := yaml.NewDecoder(file).Decode(&config); err != nil {
|
||||
return Root{}, err
|
||||
if err == io.EOF {
|
||||
log.Error().Msgf("Configuration file %s was empty", configPath)
|
||||
return Root{}, nil
|
||||
}
|
||||
return Root{}, errors.Wrap(err, "error parsing YAML in config file at "+configPath)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
@@ -93,14 +99,14 @@ func readConfigFromPath(configPath string) (Root, error) {
|
||||
func (m *FileManager) WatcherItemDidChange(filepath string) {
|
||||
config, err := m.GetConfig()
|
||||
if err != nil {
|
||||
m.logger.Errorf("Failed to read new config: %s", err)
|
||||
m.log.Err(err).Msg("Failed to read new config")
|
||||
return
|
||||
}
|
||||
m.logger.Info("Config file has been updated")
|
||||
m.log.Info().Msg("Config file has been updated")
|
||||
m.notifier.ConfigDidUpdate(config)
|
||||
}
|
||||
|
||||
// WatcherDidError notifies of errors with the file watcher
|
||||
func (m *FileManager) WatcherDidError(err error) {
|
||||
m.logger.Errorf("Config watcher encountered an error: %s", err)
|
||||
m.log.Err(err).Msg("Config watcher encountered an error")
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/watcher"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -45,8 +46,8 @@ func TestConfigChanged(t *testing.T) {
|
||||
f, err := os.Create(filePath)
|
||||
assert.NoError(t, err)
|
||||
defer func() {
|
||||
f.Close()
|
||||
os.Remove(filePath)
|
||||
_ = f.Close()
|
||||
_ = os.Remove(filePath)
|
||||
}()
|
||||
c := &Root{
|
||||
Forwarders: []Forwarder{
|
||||
@@ -56,15 +57,15 @@ func TestConfigChanged(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
configRead := func(configPath string) (Root, error) {
|
||||
configRead := func(configPath string, log *zerolog.Logger) (Root, error) {
|
||||
return *c, nil
|
||||
}
|
||||
wait := make(chan struct{})
|
||||
w := &mockFileWatcher{path: filePath, ready: wait}
|
||||
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
log := zerolog.Nop()
|
||||
|
||||
service, err := NewFileManager(w, filePath, logger)
|
||||
service, err := NewFileManager(w, filePath, &log)
|
||||
service.ReadConfig = configRead
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
)
|
||||
|
||||
// Forwarder represents a client side listener to forward traffic to the edge
|
||||
@@ -25,11 +27,12 @@ type Tunnel struct {
|
||||
|
||||
// DNSResolver represents a client side DNS resolver
|
||||
type DNSResolver struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Address string `json:"address,omitempty"`
|
||||
Port uint16 `json:"port,omitempty"`
|
||||
Upstreams []string `json:"upstreams,omitempty"`
|
||||
Bootstraps []string `json:"bootstraps,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Address string `json:"address,omitempty"`
|
||||
Port uint16 `json:"port,omitempty"`
|
||||
Upstreams []string `json:"upstreams,omitempty"`
|
||||
Bootstraps []string `json:"bootstraps,omitempty"`
|
||||
MaxUpstreamConnections int `json:"max_upstream_connections,omitempty"`
|
||||
}
|
||||
|
||||
// Root is the base options to configure the service
|
||||
@@ -59,6 +62,7 @@ func (r *DNSResolver) Hash() string {
|
||||
io.WriteString(h, strings.Join(r.Bootstraps, ","))
|
||||
io.WriteString(h, strings.Join(r.Upstreams, ","))
|
||||
io.WriteString(h, fmt.Sprintf("%d", r.Port))
|
||||
io.WriteString(h, fmt.Sprintf("%d", r.MaxUpstreamConnections))
|
||||
io.WriteString(h, fmt.Sprintf("%v", r.Enabled))
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
@@ -99,3 +103,11 @@ func (r *DNSResolver) BootstrapsOrDefault() []string {
|
||||
}
|
||||
return []string{"https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"}
|
||||
}
|
||||
|
||||
// MaxUpstreamConnectionsOrDefault return the max upstream connections or returns the default if negative
|
||||
func (r *DNSResolver) MaxUpstreamConnectionsOrDefault() int {
|
||||
if r.MaxUpstreamConnections >= 0 {
|
||||
return r.MaxUpstreamConnections
|
||||
}
|
||||
return tunneldns.MaxUpstreamConnsDefault
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// +build fips
|
||||
|
||||
package main
|
||||
|
||||
import _ "crypto/tls/fipsonly"
|
||||
@@ -5,9 +5,9 @@ package main
|
||||
import (
|
||||
"os"
|
||||
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
cli "github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
func runApp(app *cli.App, graceShutdownC chan struct{}) {
|
||||
app.Run(os.Args)
|
||||
}
|
||||
|
||||
@@ -7,26 +7,35 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/pkg/errors"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
func runApp(app *cli.App, graceShutdownC chan struct{}) {
|
||||
app.Commands = append(app.Commands, &cli.Command{
|
||||
Name: "service",
|
||||
Usage: "Manages the Argo Tunnel system service",
|
||||
Subcommands: []*cli.Command{
|
||||
&cli.Command{
|
||||
{
|
||||
Name: "install",
|
||||
Usage: "Install Argo Tunnel as a system service",
|
||||
Action: installLinuxService,
|
||||
Action: cliutil.ErrorHandler(installLinuxService),
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "legacy",
|
||||
Usage: "Generate service file for non-named tunnels",
|
||||
},
|
||||
},
|
||||
},
|
||||
&cli.Command{
|
||||
{
|
||||
Name: "uninstall",
|
||||
Usage: "Uninstall the Argo Tunnel service",
|
||||
Action: uninstallLinuxService,
|
||||
Action: cliutil.ErrorHandler(uninstallLinuxService),
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -39,6 +48,7 @@ const (
|
||||
serviceConfigDir = "/etc/cloudflared"
|
||||
serviceConfigFile = "config.yml"
|
||||
serviceCredentialFile = "cert.pem"
|
||||
serviceConfigPath = serviceConfigDir + "/" + serviceConfigFile
|
||||
)
|
||||
|
||||
var systemdTemplates = []ServiceTemplate{
|
||||
@@ -51,7 +61,7 @@ After=network.target
|
||||
[Service]
|
||||
TimeoutStartSec=0
|
||||
Type=notify
|
||||
ExecStart={{ .Path }} --config /etc/cloudflared/config.yml --origincert /etc/cloudflared/cert.pem --no-autoupdate
|
||||
ExecStart={{ .Path }} --config /etc/cloudflared/config.yml --no-autoupdate{{ range .ExtraArgs }} {{ . }}{{ end }}
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
|
||||
@@ -66,7 +76,7 @@ Description=Update Argo Tunnel
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/bin/bash -c '{{ .Path }} update; code=$?; if [ $code -eq 64 ]; then systemctl restart cloudflared; exit 0; fi; exit $code'
|
||||
ExecStart=/bin/bash -c '{{ .Path }} update; code=$?; if [ $code -eq 11 ]; then systemctl restart cloudflared; exit 0; fi; exit $code'
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -101,7 +111,7 @@ var sysvTemplate = ServiceTemplate{
|
||||
# Description: Argo Tunnel agent
|
||||
### END INIT INFO
|
||||
name=$(basename $(readlink -f $0))
|
||||
cmd="{{.Path}} --config /etc/cloudflared/config.yml --origincert /etc/cloudflared/cert.pem --pidfile /var/run/$name.pid --autoupdate-freq 24h0m0s"
|
||||
cmd="{{.Path}} --config /etc/cloudflared/config.yml --pidfile /var/run/$name.pid --autoupdate-freq 24h0m0s{{ range .ExtraArgs }} {{ . }}{{ end }}"
|
||||
pid_file="/var/run/$name.pid"
|
||||
stdout_log="/var/log/$name.log"
|
||||
stderr_log="/var/log/$name.err"
|
||||
@@ -180,11 +190,7 @@ func isSystemd() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile string, logger logger.Service) error {
|
||||
if err := ensureConfigDirExists(serviceConfigDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile string, log *zerolog.Logger) error {
|
||||
srcCredentialPath := filepath.Join(userConfigDir, userCredentialFile)
|
||||
destCredentialPath := filepath.Join(serviceConfigDir, serviceCredentialFile)
|
||||
if srcCredentialPath != destCredentialPath {
|
||||
@@ -199,71 +205,108 @@ func copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile str
|
||||
if err := copyConfig(srcConfigPath, destConfigPath); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("Copied %s to %s", srcConfigPath, destConfigPath)
|
||||
log.Info().Msgf("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")
|
||||
}
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
etPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error determining executable path: %v", err)
|
||||
}
|
||||
templateArgs := ServiceTemplateArgs{Path: etPath}
|
||||
templateArgs := ServiceTemplateArgs{
|
||||
Path: etPath,
|
||||
}
|
||||
|
||||
userConfigDir := filepath.Dir(c.String("config"))
|
||||
userConfigFile := filepath.Base(c.String("config"))
|
||||
userCredentialFile := config.DefaultCredentialFile
|
||||
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)
|
||||
if err := ensureConfigDirExists(serviceConfigDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.Bool("legacy") {
|
||||
userConfigDir := filepath.Dir(c.String("config"))
|
||||
userConfigFile := filepath.Base(c.String("config"))
|
||||
userCredentialFile := config.DefaultCredentialFile
|
||||
if err = copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile, log); err != nil {
|
||||
log.Err(err).Msgf("Failed to copy user configuration. Before running the service, ensure that %s contains two files, %s and %s",
|
||||
serviceConfigDir, serviceCredentialFile, serviceConfigFile)
|
||||
return err
|
||||
}
|
||||
templateArgs.ExtraArgs = []string{
|
||||
"--origincert", serviceConfigDir + "/" + serviceCredentialFile,
|
||||
}
|
||||
} else {
|
||||
src, err := config.ReadConfigFile(c, log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// can't use context because this command doesn't define "credentials-file" flag
|
||||
configPresent := func(s string) bool {
|
||||
val, err := src.String(s)
|
||||
return err == nil && val != ""
|
||||
}
|
||||
if src.TunnelID == "" || !configPresent(tunnel.CredFileFlag) {
|
||||
return fmt.Errorf(`Configuration file %s must contain entries for the tunnel to run and its associated credentials:
|
||||
tunnel: TUNNEL-UUID
|
||||
credentials-file: CREDENTIALS-FILE
|
||||
`, src.Source())
|
||||
}
|
||||
if src.Source() != serviceConfigPath {
|
||||
if exists, err := config.FileExists(serviceConfigPath); err != nil || exists {
|
||||
return fmt.Errorf("Possible conflicting configuration in %[1]s and %[2]s. Either remove %[2]s or run `cloudflared --config %[2]s service install`", src.Source(), serviceConfigPath)
|
||||
}
|
||||
|
||||
if err := copyFile(src.Source(), serviceConfigPath); err != nil {
|
||||
return fmt.Errorf("failed to copy %s to %s: %w", src.Source(), serviceConfigPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
templateArgs.ExtraArgs = []string{
|
||||
"tunnel", "run",
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case isSystemd():
|
||||
logger.Infof("Using Systemd")
|
||||
return installSystemd(&templateArgs, logger)
|
||||
log.Info().Msgf("Using Systemd")
|
||||
return installSystemd(&templateArgs, log)
|
||||
default:
|
||||
logger.Infof("Using Sysv")
|
||||
return installSysv(&templateArgs, logger)
|
||||
log.Info().Msgf("Using SysV")
|
||||
return installSysv(&templateArgs, log)
|
||||
}
|
||||
}
|
||||
|
||||
func installSystemd(templateArgs *ServiceTemplateArgs, logger logger.Service) error {
|
||||
func installSystemd(templateArgs *ServiceTemplateArgs, log *zerolog.Logger) error {
|
||||
for _, serviceTemplate := range systemdTemplates {
|
||||
err := serviceTemplate.Generate(templateArgs)
|
||||
if err != nil {
|
||||
logger.Errorf("error generating service template: %s", err)
|
||||
log.Err(err).Msg("error generating service template")
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := runCommand("systemctl", "enable", "cloudflared.service"); err != nil {
|
||||
logger.Errorf("systemctl enable cloudflared.service error: %s", err)
|
||||
log.Err(err).Msg("systemctl enable cloudflared.service error")
|
||||
return err
|
||||
}
|
||||
if err := runCommand("systemctl", "start", "cloudflared-update.timer"); err != nil {
|
||||
logger.Errorf("systemctl start cloudflared-update.timer error: %s", err)
|
||||
log.Err(err).Msg("systemctl start cloudflared-update.timer error")
|
||||
return err
|
||||
}
|
||||
logger.Infof("systemctl daemon-reload")
|
||||
log.Info().Msg("systemctl daemon-reload")
|
||||
return runCommand("systemctl", "daemon-reload")
|
||||
}
|
||||
|
||||
func installSysv(templateArgs *ServiceTemplateArgs, logger logger.Service) error {
|
||||
func installSysv(templateArgs *ServiceTemplateArgs, log *zerolog.Logger) error {
|
||||
confPath, err := sysvTemplate.ResolvePath()
|
||||
if err != nil {
|
||||
logger.Errorf("error resolving system path: %s", err)
|
||||
log.Err(err).Msg("error resolving system path")
|
||||
return err
|
||||
}
|
||||
if err := sysvTemplate.Generate(templateArgs); err != nil {
|
||||
logger.Errorf("error generating system template: %s", err)
|
||||
log.Err(err).Msg("error generating system template")
|
||||
return err
|
||||
}
|
||||
for _, i := range [...]string{"2", "3", "4", "5"} {
|
||||
@@ -280,43 +323,40 @@ func installSysv(templateArgs *ServiceTemplateArgs, logger logger.Service) error
|
||||
}
|
||||
|
||||
func uninstallLinuxService(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
switch {
|
||||
case isSystemd():
|
||||
logger.Infof("Using Systemd")
|
||||
return uninstallSystemd(logger)
|
||||
log.Info().Msg("Using Systemd")
|
||||
return uninstallSystemd(log)
|
||||
default:
|
||||
logger.Infof("Using Sysv")
|
||||
return uninstallSysv(logger)
|
||||
log.Info().Msg("Using SysV")
|
||||
return uninstallSysv(log)
|
||||
}
|
||||
}
|
||||
|
||||
func uninstallSystemd(logger logger.Service) error {
|
||||
func uninstallSystemd(log *zerolog.Logger) error {
|
||||
if err := runCommand("systemctl", "disable", "cloudflared.service"); err != nil {
|
||||
logger.Errorf("systemctl disable cloudflared.service error: %s", err)
|
||||
log.Err(err).Msg("systemctl disable cloudflared.service error")
|
||||
return err
|
||||
}
|
||||
if err := runCommand("systemctl", "stop", "cloudflared-update.timer"); err != nil {
|
||||
logger.Errorf("systemctl stop cloudflared-update.timer error: %s", err)
|
||||
log.Err(err).Msg("systemctl stop cloudflared-update.timer error")
|
||||
return err
|
||||
}
|
||||
for _, serviceTemplate := range systemdTemplates {
|
||||
if err := serviceTemplate.Remove(); err != nil {
|
||||
logger.Errorf("error removing service template: %s", err)
|
||||
log.Err(err).Msg("error removing service template")
|
||||
return err
|
||||
}
|
||||
}
|
||||
logger.Infof("Successfully uninstall cloudflared service")
|
||||
log.Info().Msgf("Successfully uninstalled cloudflared service from systemd")
|
||||
return nil
|
||||
}
|
||||
|
||||
func uninstallSysv(logger logger.Service) error {
|
||||
func uninstallSysv(log *zerolog.Logger) error {
|
||||
if err := sysvTemplate.Remove(); err != nil {
|
||||
logger.Errorf("error removing service template: %s", err)
|
||||
log.Err(err).Msg("error removing service template")
|
||||
return err
|
||||
}
|
||||
for _, i := range [...]string{"2", "3", "4", "5"} {
|
||||
@@ -329,6 +369,6 @@ func uninstallSysv(logger logger.Service) error {
|
||||
continue
|
||||
}
|
||||
}
|
||||
logger.Infof("Successfully uninstall cloudflared service")
|
||||
log.Info().Msgf("Successfully uninstalled cloudflared service from sysv")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,17 +6,18 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
launchdIdentifier = "com.cloudflare.cloudflared"
|
||||
)
|
||||
|
||||
func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
func runApp(app *cli.App, graceShutdownC chan struct{}) {
|
||||
app.Commands = append(app.Commands, &cli.Command{
|
||||
Name: "service",
|
||||
Usage: "Manages the Argo Tunnel launch agent",
|
||||
@@ -24,16 +25,16 @@ func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
{
|
||||
Name: "install",
|
||||
Usage: "Install Argo Tunnel as an user launch agent",
|
||||
Action: installLaunchd,
|
||||
Action: cliutil.ErrorHandler(installLaunchd),
|
||||
},
|
||||
{
|
||||
Name: "uninstall",
|
||||
Usage: "Uninstall the Argo Tunnel launch agent",
|
||||
Action: uninstallLaunchd,
|
||||
Action: cliutil.ErrorHandler(uninstallLaunchd),
|
||||
},
|
||||
},
|
||||
})
|
||||
app.Run(os.Args)
|
||||
_ = app.Run(os.Args)
|
||||
}
|
||||
|
||||
func newLaunchdTemplate(installPath, stdoutPath, stderrPath string) *ServiceTemplate {
|
||||
@@ -106,71 +107,61 @@ 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")
|
||||
}
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
if isRootUser() {
|
||||
logger.Infof("Installing Argo Tunnel client as a system launch daemon. " +
|
||||
log.Info().Msg("Installing Argo Tunnel client as a system launch daemon. " +
|
||||
"Argo Tunnel client will run at boot")
|
||||
} else {
|
||||
logger.Infof("Installing Argo Tunnel client as an user launch agent. " +
|
||||
log.Info().Msg("Installing Argo Tunnel client as an user launch agent. " +
|
||||
"Note that Argo Tunnel client will only run when the user is logged in. " +
|
||||
"If you want to run Argo Tunnel client at boot, install with root permission. " +
|
||||
"For more information, visit https://developers.cloudflare.com/argo-tunnel/reference/service/")
|
||||
}
|
||||
etPath, err := os.Executable()
|
||||
if err != nil {
|
||||
logger.Errorf("Error determining executable path: %s", err)
|
||||
log.Err(err).Msg("Error determining executable path")
|
||||
return fmt.Errorf("Error determining executable path: %v", err)
|
||||
}
|
||||
installPath, err := installPath()
|
||||
if err != nil {
|
||||
logger.Errorf("Error determining install path: %s", err)
|
||||
log.Err(err).Msg("Error determining install path")
|
||||
return errors.Wrap(err, "Error determining install path")
|
||||
}
|
||||
stdoutPath, err := stdoutPath()
|
||||
if err != nil {
|
||||
logger.Errorf("error determining stdout path: %s", err)
|
||||
log.Err(err).Msg("error determining stdout path")
|
||||
return errors.Wrap(err, "error determining stdout path")
|
||||
}
|
||||
stderrPath, err := stderrPath()
|
||||
if err != nil {
|
||||
logger.Errorf("error determining stderr path: %s", err)
|
||||
log.Err(err).Msg("error determining stderr path")
|
||||
return errors.Wrap(err, "error determining stderr path")
|
||||
}
|
||||
launchdTemplate := newLaunchdTemplate(installPath, stdoutPath, stderrPath)
|
||||
if err != nil {
|
||||
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.Errorf("error generating launchd template: %s", err)
|
||||
log.Err(err).Msg("error generating launchd template")
|
||||
return err
|
||||
}
|
||||
plistPath, err := launchdTemplate.ResolvePath()
|
||||
if err != nil {
|
||||
logger.Errorf("error resolving launchd template path: %s", err)
|
||||
log.Err(err).Msg("error resolving launchd template path")
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Infof("Outputs are logged to %s and %s", stderrPath, stdoutPath)
|
||||
log.Info().Msgf("Outputs are logged to %s and %s", stderrPath, stdoutPath)
|
||||
return runCommand("launchctl", "load", plistPath)
|
||||
}
|
||||
|
||||
func uninstallLaunchd(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
if isRootUser() {
|
||||
logger.Infof("Uninstalling Argo Tunnel as a system launch daemon")
|
||||
log.Info().Msg("Uninstalling Argo Tunnel as a system launch daemon")
|
||||
} else {
|
||||
logger.Infof("Uninstalling Argo Tunnel as an user launch agent")
|
||||
log.Info().Msg("Uninstalling Argo Tunnel as an user launch agent")
|
||||
}
|
||||
installPath, err := installPath()
|
||||
if err != nil {
|
||||
@@ -185,20 +176,17 @@ func uninstallLaunchd(c *cli.Context) error {
|
||||
return errors.Wrap(err, "error determining stderr path")
|
||||
}
|
||||
launchdTemplate := newLaunchdTemplate(installPath, stdoutPath, stderrPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error creating launchd template")
|
||||
}
|
||||
plistPath, err := launchdTemplate.ResolvePath()
|
||||
if err != nil {
|
||||
logger.Errorf("error resolving launchd template path: %s", err)
|
||||
log.Err(err).Msg("error resolving launchd template path")
|
||||
return err
|
||||
}
|
||||
err = runCommand("launchctl", "unload", plistPath)
|
||||
if err != nil {
|
||||
logger.Errorf("error unloading: %s", err)
|
||||
log.Err(err).Msg("error unloading")
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Infof("Outputs are logged to %s and %s", stderrPath, stdoutPath)
|
||||
log.Info().Msgf("Outputs are logged to %s and %s", stderrPath, stdoutPath)
|
||||
return launchdTemplate.Remove()
|
||||
}
|
||||
|
||||
+60
-48
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -10,16 +11,16 @@ import (
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||
log "github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/metrics"
|
||||
"github.com/cloudflare/cloudflared/overwatch"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
"github.com/cloudflare/cloudflared/watcher"
|
||||
|
||||
raven "github.com/getsentry/raven-go"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -43,13 +44,11 @@ var (
|
||||
)
|
||||
|
||||
func main() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
metrics.RegisterBuildInfo(BuildTime, Version)
|
||||
raven.SetRelease(Version)
|
||||
|
||||
// Force shutdown channel used by the app. When closed, app must terminate.
|
||||
// Windows service manager closes this channel when it receives shutdown command.
|
||||
shutdownC := make(chan struct{})
|
||||
// Graceful shutdown channel used by the app. When closed, app must terminate.
|
||||
// Graceful shutdown channel used by the app. When closed, app must terminate gracefully.
|
||||
// Windows service manager closes this channel when it receives stop command.
|
||||
graceShutdownC := make(chan struct{})
|
||||
|
||||
@@ -73,29 +72,52 @@ func main() {
|
||||
app.Version = fmt.Sprintf("%s (built %s)", Version, BuildTime)
|
||||
app.Description = `cloudflared connects your machine or user identity to Cloudflare's global network.
|
||||
You can use it to authenticate a session to reach an API behind Access, route web traffic to this machine,
|
||||
and configure access control.`
|
||||
and configure access control.
|
||||
|
||||
See https://developers.cloudflare.com/argo-tunnel/ for more in-depth documentation.`
|
||||
app.Flags = flags()
|
||||
app.Action = action(Version, shutdownC, graceShutdownC)
|
||||
app.Before = tunnel.Before
|
||||
app.Action = action(graceShutdownC)
|
||||
app.Before = tunnel.SetFlagsFromConfigFile
|
||||
app.Commands = commands(cli.ShowVersion)
|
||||
|
||||
tunnel.Init(Version, shutdownC, graceShutdownC) // we need this to support the tunnel sub command...
|
||||
access.Init(shutdownC, graceShutdownC)
|
||||
runApp(app, shutdownC, graceShutdownC)
|
||||
tunnel.Init(Version, graceShutdownC) // we need this to support the tunnel sub command...
|
||||
access.Init(graceShutdownC)
|
||||
updater.Init(Version)
|
||||
runApp(app, graceShutdownC)
|
||||
}
|
||||
|
||||
func commands(version func(c *cli.Context)) []*cli.Command {
|
||||
cmds := []*cli.Command{
|
||||
{
|
||||
Name: "update",
|
||||
Action: updater.Update,
|
||||
Usage: "Update the agent if a new version exists",
|
||||
ArgsUsage: " ",
|
||||
Name: "update",
|
||||
Action: cliutil.ErrorHandler(updater.Update),
|
||||
Usage: "Update the agent if a new version exists",
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "beta",
|
||||
Usage: "specify if you wish to update to the latest beta version",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "force",
|
||||
Usage: "specify if you wish to force an upgrade to the latest version regardless of the current version",
|
||||
Hidden: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "staging",
|
||||
Usage: "specify if you wish to use the staging url for updating",
|
||||
Hidden: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "version",
|
||||
Usage: "specify a version you wish to upgrade or downgrade to",
|
||||
Hidden: false,
|
||||
},
|
||||
},
|
||||
Description: `Looks for a new version on the official download server.
|
||||
If a new version exists, updates the agent binary and quits.
|
||||
Otherwise, does nothing.
|
||||
|
||||
To determine if an update happened in a script, check for error code 64.`,
|
||||
To determine if an update happened in a script, check for error code 11.`,
|
||||
},
|
||||
{
|
||||
Name: "version",
|
||||
@@ -108,6 +130,7 @@ To determine if an update happened in a script, check for error code 64.`,
|
||||
},
|
||||
}
|
||||
cmds = append(cmds, tunnel.Commands()...)
|
||||
cmds = append(cmds, tunneldns.Command(false))
|
||||
cmds = append(cmds, access.Commands()...)
|
||||
return cmds
|
||||
}
|
||||
@@ -121,24 +144,20 @@ func isEmptyInvocation(c *cli.Context) bool {
|
||||
return c.NArg() == 0 && c.NumFlags() == 0
|
||||
}
|
||||
|
||||
func action(version string, shutdownC, graceShutdownC chan struct{}) cli.ActionFunc {
|
||||
return func(c *cli.Context) (err error) {
|
||||
func action(graceShutdownC chan struct{}) cli.ActionFunc {
|
||||
return cliutil.ErrorHandler(func(c *cli.Context) (err error) {
|
||||
if isEmptyInvocation(c) {
|
||||
return handleServiceMode(shutdownC)
|
||||
return handleServiceMode(c, graceShutdownC)
|
||||
}
|
||||
tags := make(map[string]string)
|
||||
tags["hostname"] = c.String("hostname")
|
||||
raven.SetTagsContext(tags)
|
||||
raven.CapturePanic(func() { err = tunnel.StartServer(c, version, shutdownC, graceShutdownC, nil) }, nil)
|
||||
exitCode := 0
|
||||
raven.CapturePanic(func() { err = tunnel.TunnelCommand(c) }, nil)
|
||||
if err != nil {
|
||||
handleError(err)
|
||||
exitCode = 1
|
||||
captureError(err)
|
||||
}
|
||||
// we already handle error printing, so we pass an empty string so we
|
||||
// don't have to print again.
|
||||
return cli.Exit("", exitCode)
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func userHomeDir() (string, error) {
|
||||
@@ -154,7 +173,7 @@ func userHomeDir() (string, error) {
|
||||
}
|
||||
|
||||
// In order to keep the amount of noise sent to Sentry low, typical network errors can be filtered out here by a substring match.
|
||||
func handleError(err error) {
|
||||
func captureError(err error) {
|
||||
errorMessage := err.Error()
|
||||
for _, ignoredErrorMessage := range ignoredErrors {
|
||||
if strings.Contains(errorMessage, ignoredErrorMessage) {
|
||||
@@ -165,41 +184,34 @@ 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)
|
||||
func handleServiceMode(c *cli.Context, shutdownC chan struct{}) error {
|
||||
log := logger.CreateLoggerFromContext(c, logger.DisableTerminalLog)
|
||||
|
||||
// start the main run loop that reads from the config file
|
||||
f, err := watcher.NewFile()
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot load config file: %s", err)
|
||||
log.Err(err).Msg("Cannot load config file")
|
||||
return err
|
||||
}
|
||||
|
||||
configPath := config.FindOrCreateConfigPath()
|
||||
configManager, err := config.NewFileManager(f, configPath, logger)
|
||||
configManager, err := config.NewFileManager(f, configPath, log)
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot setup config file for monitoring: %s", err)
|
||||
log.Err(err).Msg("Cannot setup config file for monitoring")
|
||||
return err
|
||||
}
|
||||
logger.Infof("monitoring config file at: %s", configPath)
|
||||
log.Info().Msgf("monitoring config file at: %s", configPath)
|
||||
|
||||
serviceCallback := func(t string, name string, err error) {
|
||||
if err != nil {
|
||||
logger.Errorf("%s service: %s encountered an error: %s", t, name, err)
|
||||
log.Err(err).Msgf("%s service: %s encountered an error", t, name)
|
||||
}
|
||||
}
|
||||
serviceManager := overwatch.NewAppManager(serviceCallback)
|
||||
|
||||
appService := NewAppService(configManager, serviceManager, shutdownC, logger)
|
||||
appService := NewAppService(configManager, serviceManager, shutdownC, log)
|
||||
if err := appService.Run(); err != nil {
|
||||
logger.Errorf("Failed to start app service: %s", err)
|
||||
log.Err(err).Msg("Failed to start app service")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -11,9 +11,28 @@ import (
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
// GenerateFilePathFromURL will return a filepath for given access application url
|
||||
func GenerateFilePathFromURL(url *url.URL, suffix string) (string, error) {
|
||||
configPath, err := homedir.Expand(config.DefaultConfigDirs[0])
|
||||
// GenerateAppTokenFilePathFromURL will return a filepath for given Access org token
|
||||
func GenerateAppTokenFilePathFromURL(url *url.URL, suffix string) (string, error) {
|
||||
configPath, err := getConfigPath()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
name := strings.Replace(fmt.Sprintf("%s%s-%s", url.Hostname(), url.EscapedPath(), suffix), "/", "-", -1)
|
||||
return filepath.Join(configPath, name), nil
|
||||
}
|
||||
|
||||
// GenerateOrgTokenFilePathFromURL will return a filepath for given Access application token
|
||||
func GenerateOrgTokenFilePathFromURL(authDomain string) (string, error) {
|
||||
configPath, err := getConfigPath()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
name := strings.Replace(fmt.Sprintf("%s-org-token", authDomain), "/", "-", -1)
|
||||
return filepath.Join(configPath, name), nil
|
||||
}
|
||||
|
||||
func getConfigPath() (string, error) {
|
||||
configPath, err := homedir.Expand(config.DefaultConfigSearchDirectories()[0])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -22,9 +41,5 @@ func GenerateFilePathFromURL(url *url.URL, suffix string) (string, error) {
|
||||
// create config directory if doesn't already exist
|
||||
err = os.Mkdir(configPath, 0700)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
name := strings.Replace(fmt.Sprintf("%s%s-%s", url.Hostname(), url.EscapedPath(), suffix), "/", "-", -1)
|
||||
return filepath.Join(configPath, name), nil
|
||||
return configPath, err
|
||||
}
|
||||
|
||||
@@ -10,8 +10,9 @@ import (
|
||||
"os/exec"
|
||||
"text/template"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
)
|
||||
|
||||
type ServiceTemplate struct {
|
||||
@@ -21,7 +22,8 @@ type ServiceTemplate struct {
|
||||
}
|
||||
|
||||
type ServiceTemplateArgs struct {
|
||||
Path string
|
||||
Path string
|
||||
ExtraArgs []string
|
||||
}
|
||||
|
||||
func (st *ServiceTemplate) ResolvePath() (string, error) {
|
||||
@@ -80,7 +82,7 @@ func runCommand(command string, args ...string) error {
|
||||
return fmt.Errorf("error starting %s: %v", command, err)
|
||||
}
|
||||
|
||||
ioutil.ReadAll(stderr)
|
||||
_, _ = ioutil.ReadAll(stderr)
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s returned with error: %v", command, err)
|
||||
@@ -139,6 +141,33 @@ func copyCredential(srcCredentialPath, destCredentialPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFile(src, dest string) error {
|
||||
srcFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
destFile, err := os.Create(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ok := false
|
||||
defer func() {
|
||||
destFile.Close()
|
||||
if !ok {
|
||||
_ = os.Remove(dest)
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := io.Copy(destFile, srcFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ok = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyConfig(srcConfigPath, destConfigPath string) error {
|
||||
// Copy or create config
|
||||
destFile, exists, err := openFile(destConfigPath, true)
|
||||
|
||||
+212
-41
@@ -5,22 +5,27 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"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/logger"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
const (
|
||||
keyName = "token"
|
||||
keyName = "token"
|
||||
tokenHeader = "CF_Authorization"
|
||||
)
|
||||
|
||||
type lock struct {
|
||||
@@ -34,7 +39,7 @@ type signalHandler struct {
|
||||
signals []os.Signal
|
||||
}
|
||||
|
||||
type jwtPayload struct {
|
||||
type appJWTPayload struct {
|
||||
Aud []string `json:"aud"`
|
||||
Email string `json:"email"`
|
||||
Exp int `json:"exp"`
|
||||
@@ -45,7 +50,17 @@ type jwtPayload struct {
|
||||
Subt string `json:"sub"`
|
||||
}
|
||||
|
||||
func (p jwtPayload) isExpired() bool {
|
||||
type orgJWTPayload struct {
|
||||
appJWTPayload
|
||||
Aud string `json:"aud"`
|
||||
}
|
||||
|
||||
type transferServiceResponse struct {
|
||||
AppToken string `json:"app_token"`
|
||||
OrgToken string `json:"org_token"`
|
||||
}
|
||||
|
||||
func (p appJWTPayload) isExpired() bool {
|
||||
return int(time.Now().Unix()) > p.Exp
|
||||
}
|
||||
|
||||
@@ -83,7 +98,7 @@ func newLock(path string) *lock {
|
||||
func (l *lock) Acquire() error {
|
||||
// Intercept SIGINT and SIGTERM to release lock before exiting
|
||||
l.sigHandler.register(func() {
|
||||
l.deleteLockFile()
|
||||
_ = l.deleteLockFile()
|
||||
os.Exit(0)
|
||||
})
|
||||
|
||||
@@ -128,68 +143,188 @@ func isTokenLocked(lockFilePath string) bool {
|
||||
}
|
||||
|
||||
// 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)
|
||||
// it appends the full url as the redirect URL to the access cli request if opening the browser
|
||||
func FetchTokenWithRedirect(appURL *url.URL, log *zerolog.Logger) (string, error) {
|
||||
return getToken(appURL, false, log)
|
||||
}
|
||||
|
||||
// FetchToken will either load a stored token or generate a new one
|
||||
// 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)
|
||||
// it appends the host of the appURL as the redirect URL to the access cli request if opening the browser
|
||||
func FetchToken(appURL *url.URL, log *zerolog.Logger) (string, error) {
|
||||
return getToken(appURL, true, log)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
func getToken(appURL *url.URL, useHostOnly bool, log *zerolog.Logger) (string, error) {
|
||||
if token, err := GetAppTokenIfExists(appURL); token != "" && err == nil {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
path, err := path.GenerateFilePathFromURL(appURL, keyName)
|
||||
appTokenPath, err := path.GenerateAppTokenFilePathFromURL(appURL, keyName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", errors.Wrap(err, "failed to generate app token file path")
|
||||
}
|
||||
|
||||
fileLock := newLock(path)
|
||||
|
||||
err = fileLock.Acquire()
|
||||
if err != nil {
|
||||
return "", err
|
||||
fileLockAppToken := newLock(appTokenPath)
|
||||
if err = fileLockAppToken.Acquire(); err != nil {
|
||||
return "", errors.Wrap(err, "failed to acquire app token lock")
|
||||
}
|
||||
defer fileLock.Release()
|
||||
defer fileLockAppToken.Release()
|
||||
|
||||
// check to see if another process has gotten a token while we waited for the lock
|
||||
if token, err := GetTokenIfExists(appURL); token != "" && err == nil {
|
||||
if token, err := GetAppTokenIfExists(appURL); token != "" && err == nil {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// If an app token couldnt be found on disk, check for an org token and attempt to exchange it for an app token.
|
||||
var orgTokenPath string
|
||||
// Get auth domain to format into org token file path
|
||||
if authDomain, err := getAuthDomain(appURL); err != nil {
|
||||
log.Error().Msgf("failed to get auth domain: %s", err)
|
||||
} else {
|
||||
orgToken, err := GetOrgTokenIfExists(authDomain)
|
||||
if err != nil {
|
||||
orgTokenPath, err = path.GenerateOrgTokenFilePathFromURL(authDomain)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to generate org token file path")
|
||||
}
|
||||
|
||||
fileLockOrgToken := newLock(orgTokenPath)
|
||||
if err = fileLockOrgToken.Acquire(); err != nil {
|
||||
return "", errors.Wrap(err, "failed to acquire org token lock")
|
||||
}
|
||||
defer fileLockOrgToken.Release()
|
||||
// check if an org token has been created since the lock was acquired
|
||||
orgToken, err = GetOrgTokenIfExists(authDomain)
|
||||
}
|
||||
if err == nil {
|
||||
if appToken, err := exchangeOrgToken(appURL, orgToken); err != nil {
|
||||
log.Debug().Msgf("failed to exchange org token for app token: %s", err)
|
||||
} else {
|
||||
if err := ioutil.WriteFile(appTokenPath, []byte(appToken), 0600); err != nil {
|
||||
return "", errors.Wrap(err, "failed to write app token to disk")
|
||||
}
|
||||
return appToken, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return getTokensFromEdge(appURL, appTokenPath, orgTokenPath, useHostOnly, log)
|
||||
|
||||
}
|
||||
|
||||
// getTokensFromEdge will attempt to use the transfer service to retrieve an app and org token, save them to disk,
|
||||
// and return the app token.
|
||||
func getTokensFromEdge(appURL *url.URL, appTokenPath, orgTokenPath string, useHostOnly bool, log *zerolog.Logger) (string, error) {
|
||||
// If no org token exists or if it couldnt be exchanged for an app token, then run the transfer service flow.
|
||||
|
||||
// this weird parameter is the resource name (token) and the key/value
|
||||
// we want to send to the transfer service. the key is token and the value
|
||||
// is blank (basically just the id generated in the transfer service)
|
||||
token, err := transfer.Run(appURL, keyName, keyName, "", path, true, shouldRedirect, logger)
|
||||
resourceData, err := transfer.Run(appURL, keyName, keyName, "", true, useHostOnly, log)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", errors.Wrap(err, "failed to run transfer service")
|
||||
}
|
||||
var resp transferServiceResponse
|
||||
if err = json.Unmarshal(resourceData, &resp); err != nil {
|
||||
return "", errors.Wrap(err, "failed to marshal transfer service response")
|
||||
}
|
||||
|
||||
return string(token), nil
|
||||
// If we were able to get the auth domain and generate an org token path, lets write it to disk.
|
||||
if orgTokenPath != "" {
|
||||
if err := ioutil.WriteFile(orgTokenPath, []byte(resp.OrgToken), 0600); err != nil {
|
||||
return "", errors.Wrap(err, "failed to write org token to disk")
|
||||
}
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(appTokenPath, []byte(resp.AppToken), 0600); err != nil {
|
||||
return "", errors.Wrap(err, "failed to write app token to disk")
|
||||
}
|
||||
|
||||
return resp.AppToken, nil
|
||||
|
||||
}
|
||||
|
||||
// GetTokenIfExists will return the token from local storage if it exists and not expired
|
||||
func GetTokenIfExists(url *url.URL) (string, error) {
|
||||
path, err := path.GenerateFilePathFromURL(url, keyName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
token, err := jose.ParseJWT(string(content))
|
||||
if err != nil {
|
||||
return "", err
|
||||
// getAuthDomain makes a request to the appURL and stops at the first redirect. The 302 location header will contain the
|
||||
// auth domain
|
||||
func getAuthDomain(appURL *url.URL) (string, error) {
|
||||
client := &http.Client{
|
||||
// do not follow redirects
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
Timeout: time.Second * 7,
|
||||
}
|
||||
|
||||
var payload jwtPayload
|
||||
authDomainReq, err := http.NewRequest("HEAD", appURL.String(), nil)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to create auth domain request")
|
||||
}
|
||||
resp, err := client.Do(authDomainReq)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to get auth domain")
|
||||
}
|
||||
resp.Body.Close()
|
||||
location, err := resp.Location()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get auth domain. Received status code %d from %s", resp.StatusCode, appURL.String())
|
||||
}
|
||||
return location.Hostname(), nil
|
||||
|
||||
}
|
||||
|
||||
// exchangeOrgToken attaches an org token to a request to the appURL and returns an app token. This uses the Access SSO
|
||||
// flow to automatically generate and return an app token without the login page.
|
||||
func exchangeOrgToken(appURL *url.URL, orgToken string) (string, error) {
|
||||
client := &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
// attach org token to login request
|
||||
if strings.Contains(req.URL.Path, "cdn-cgi/access/login") {
|
||||
req.AddCookie(&http.Cookie{Name: tokenHeader, Value: orgToken})
|
||||
}
|
||||
// stop after hitting authorized endpoint since it will contain the app token
|
||||
if strings.Contains(via[len(via)-1].URL.Path, "cdn-cgi/access/authorized") {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Timeout: time.Second * 7,
|
||||
}
|
||||
|
||||
appTokenRequest, err := http.NewRequest("HEAD", appURL.String(), nil)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to create app token request")
|
||||
}
|
||||
resp, err := client.Do(appTokenRequest)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to get app token")
|
||||
}
|
||||
resp.Body.Close()
|
||||
var appToken string
|
||||
for _, c := range resp.Cookies() {
|
||||
//if Org token revoked on exchange, getTokensFromEdge instead
|
||||
validAppToken := c.Name == tokenHeader && time.Now().Before(c.Expires)
|
||||
if validAppToken {
|
||||
appToken = c.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(appToken) > 0 {
|
||||
return appToken, nil
|
||||
}
|
||||
return "", fmt.Errorf("response from %s did not contain app token", resp.Request.URL.String())
|
||||
}
|
||||
|
||||
func GetOrgTokenIfExists(authDomain string) (string, error) {
|
||||
path, err := path.GenerateOrgTokenFilePathFromURL(authDomain)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
token, err := getTokenIfExists(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var payload orgJWTPayload
|
||||
err = json.Unmarshal(token.Payload, &payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -199,13 +334,49 @@ func GetTokenIfExists(url *url.URL) (string, error) {
|
||||
err := os.Remove(path)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token.Encode(), nil
|
||||
}
|
||||
|
||||
func GetAppTokenIfExists(url *url.URL) (string, error) {
|
||||
path, err := path.GenerateAppTokenFilePathFromURL(url, keyName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
token, err := getTokenIfExists(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var payload appJWTPayload
|
||||
err = json.Unmarshal(token.Payload, &payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if payload.isExpired() {
|
||||
err := os.Remove(path)
|
||||
return "", err
|
||||
}
|
||||
return token.Encode(), nil
|
||||
|
||||
}
|
||||
|
||||
// GetTokenIfExists will return the token from local storage if it exists and not expired
|
||||
func getTokenIfExists(path string) (*jose.JWT, error) {
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token, err := jose.ParseJWT(string(content))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &token, nil
|
||||
}
|
||||
|
||||
// RemoveTokenIfExists removes the a token from local storage if it exists
|
||||
func RemoveTokenIfExists(url *url.URL) error {
|
||||
path, err := path.GenerateFilePathFromURL(url, keyName)
|
||||
path, err := path.GenerateAppTokenFilePathFromURL(url, keyName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,10 +3,8 @@ package transfer
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -14,7 +12,8 @@ import (
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/encrypter"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -28,12 +27,12 @@ const (
|
||||
// The "dance" we refer to is building a HTTP request, opening that in a browser waiting for
|
||||
// the user to complete an action, while it long polls in the background waiting for an
|
||||
// action to be completed to download the resource.
|
||||
func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncrypt bool, shouldRedirect bool, logger logger.Service) ([]byte, error) {
|
||||
func Run(transferURL *url.URL, resourceName, key, value string, shouldEncrypt bool, useHostOnly bool, log *zerolog.Logger) ([]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, shouldRedirect)
|
||||
requestURL, err := buildRequestURL(transferURL, key, value+encrypterClient.PublicKey(), shouldEncrypt, useHostOnly)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -49,7 +48,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(), logger)
|
||||
buf, key, err := transferRequest(baseStoreURL+"transfer/"+encrypterClient.PublicKey(), log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -65,51 +64,49 @@ func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncr
|
||||
|
||||
resourceData = decrypted
|
||||
} else {
|
||||
buf, _, err := transferRequest(baseStoreURL+encrypterClient.PublicKey(), logger)
|
||||
buf, _, err := transferRequest(baseStoreURL+encrypterClient.PublicKey(), log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resourceData = buf
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(path, resourceData, 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resourceData, nil
|
||||
|
||||
}
|
||||
|
||||
// BuildRequestURL creates a request suitable for a resource transfer.
|
||||
// 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, shouldRedirect bool) (string, error) {
|
||||
func buildRequestURL(baseURL *url.URL, key, value string, cli, useHostOnly bool) (string, error) {
|
||||
q := baseURL.Query()
|
||||
q.Set(key, value)
|
||||
baseURL.RawQuery = q.Encode()
|
||||
if useHostOnly {
|
||||
baseURL.Path = ""
|
||||
}
|
||||
if !cli {
|
||||
return baseURL.String(), nil
|
||||
}
|
||||
|
||||
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.
|
||||
q.Set("redirect_url", baseURL.String()) // we add the token as a query param on both the redirect_url and the main url
|
||||
q.Set("send_org_token", "true") // indicates that the cli endpoint should return both the org and app token
|
||||
baseURL.RawQuery = q.Encode() // and this actual baseURL.
|
||||
baseURL.Path = "cdn-cgi/access/cli"
|
||||
return baseURL.String(), nil
|
||||
}
|
||||
|
||||
// transferRequest downloads the requested resource from the request URL
|
||||
func transferRequest(requestURL string, logger logger.Service) ([]byte, string, error) {
|
||||
func transferRequest(requestURL string, log *zerolog.Logger) ([]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, logger)
|
||||
buf, key, err := poll(client, requestURL, log)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
} else if len(buf) > 0 {
|
||||
if err := putSuccess(client, requestURL); err != nil {
|
||||
logger.Errorf("Failed to update resource success: %s", err)
|
||||
log.Err(err).Msg("Failed to update resource success")
|
||||
}
|
||||
return buf, key, nil
|
||||
}
|
||||
@@ -118,7 +115,7 @@ func transferRequest(requestURL string, logger logger.Service) ([]byte, string,
|
||||
}
|
||||
|
||||
// poll the endpoint for the request resource, waiting for the user interaction
|
||||
func poll(client *http.Client, requestURL string, logger logger.Service) ([]byte, string, error) {
|
||||
func poll(client *http.Client, requestURL string, log *zerolog.Logger) ([]byte, string, error) {
|
||||
resp, err := client.Get(requestURL)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
@@ -131,7 +128,7 @@ func poll(client *http.Client, requestURL string, logger logger.Service) ([]byte
|
||||
return nil, "", fmt.Errorf("error on request %d", resp.StatusCode)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
logger.Info("Waiting for login...")
|
||||
log.Info().Msg("Waiting for login...")
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
|
||||
+534
-600
File diff suppressed because it is too large
Load Diff
@@ -1,20 +1,19 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
@@ -23,21 +22,26 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
const LogFieldOriginCertPath = "originCertPath"
|
||||
|
||||
var (
|
||||
developerPortal = "https://developers.cloudflare.com/argo-tunnel"
|
||||
quickStartUrl = developerPortal + "/quickstart/quickstart/"
|
||||
serviceUrl = developerPortal + "/reference/service/"
|
||||
argumentsUrl = developerPortal + "/reference/arguments/"
|
||||
|
||||
LogFieldHostname = "hostname"
|
||||
)
|
||||
|
||||
// returns the first path that contains a cert.pem file. If none of the DefaultConfigDirs
|
||||
// returns the first path that contains a cert.pem file. If none of the DefaultConfigSearchDirectories
|
||||
// contains a cert.pem file, return empty string
|
||||
func findDefaultOriginCertPath() string {
|
||||
for _, defaultConfigDir := range config.DefaultConfigDirs {
|
||||
for _, defaultConfigDir := range config.DefaultConfigSearchDirectories() {
|
||||
originCertPath, _ := homedir.Expand(filepath.Join(defaultConfigDir, config.DefaultCredentialFile))
|
||||
if ok, _ := config.FileExists(originCertPath); ok {
|
||||
return originCertPath
|
||||
@@ -46,30 +50,23 @@ func findDefaultOriginCertPath() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func generateRandomClientID(logger logger.Service) (string, error) {
|
||||
func generateRandomClientID(log *zerolog.Logger) (string, error) {
|
||||
u, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
logger.Errorf("couldn't create UUID for client ID %s", err)
|
||||
log.Error().Msgf("couldn't create UUID for client ID %s", err)
|
||||
return "", err
|
||||
}
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func logClientOptions(c *cli.Context, logger logger.Service) {
|
||||
func logClientOptions(c *cli.Context, log *zerolog.Logger) {
|
||||
flags := make(map[string]interface{})
|
||||
for _, flag := range c.LocalFlagNames() {
|
||||
for _, flag := range c.FlagNames() {
|
||||
flags[flag] = c.Generic(flag)
|
||||
}
|
||||
|
||||
sliceFlags := []string{"header", "tag", "proxy-dns-upstream", "upstream", "edge"}
|
||||
for _, sliceFlag := range sliceFlags {
|
||||
if len(c.StringSlice(sliceFlag)) > 0 {
|
||||
flags[sliceFlag] = strings.Join(c.StringSlice(sliceFlag), ", ")
|
||||
}
|
||||
}
|
||||
|
||||
if len(flags) > 0 {
|
||||
logger.Infof("Environment variables %v", flags)
|
||||
log.Info().Msgf("Settings: %v", flags)
|
||||
}
|
||||
|
||||
envs := make(map[string]string)
|
||||
@@ -84,7 +81,7 @@ func logClientOptions(c *cli.Context, logger logger.Service) {
|
||||
}
|
||||
}
|
||||
if len(envs) > 0 {
|
||||
logger.Infof("Environmental variables %v", envs)
|
||||
log.Info().Msgf("Environmental variables %v", envs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,32 +89,31 @@ func dnsProxyStandAlone(c *cli.Context) bool {
|
||||
return c.IsSet("proxy-dns") && (!c.IsSet("hostname") && !c.IsSet("tag") && !c.IsSet("hello-world"))
|
||||
}
|
||||
|
||||
func findOriginCert(c *cli.Context, logger logger.Service) (string, error) {
|
||||
originCertPath := c.String("origincert")
|
||||
func findOriginCert(originCertPath string, log *zerolog.Logger) (string, error) {
|
||||
if originCertPath == "" {
|
||||
logger.Infof("Cannot determine default origin certificate path. No file %s in %v", config.DefaultCredentialFile, config.DefaultConfigDirs)
|
||||
log.Info().Msgf("Cannot determine default origin certificate path. No file %s in %v", config.DefaultCredentialFile, config.DefaultConfigSearchDirectories())
|
||||
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 "", fmt.Errorf("Client didn't specify origincert path when running from terminal")
|
||||
log.Error().Msgf("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 "", 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 "", fmt.Errorf("Client didn't specify origincert path")
|
||||
log.Error().Msgf("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 "", fmt.Errorf("client didn't specify origincert path")
|
||||
}
|
||||
}
|
||||
var err error
|
||||
originCertPath, err = homedir.Expand(originCertPath)
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot resolve path %s: %s", originCertPath, err)
|
||||
return "", fmt.Errorf("Cannot resolve path %s", originCertPath)
|
||||
log.Err(err).Msgf("Cannot resolve origin certificate path")
|
||||
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", originCertPath)
|
||||
return "", fmt.Errorf("Cannot check if origin cert exists at path %s", originCertPath)
|
||||
log.Error().Msgf("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:
|
||||
log.Error().Msgf(`Cannot find a valid certificate for your origin at the path:
|
||||
|
||||
%s
|
||||
|
||||
@@ -126,29 +122,26 @@ If you don't have a certificate signed by Cloudflare, run the command:
|
||||
|
||||
%s login
|
||||
`, originCertPath, os.Args[0])
|
||||
return "", 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)
|
||||
|
||||
func readOriginCert(originCertPath string) ([]byte, error) {
|
||||
// Easier to send the certificate as []byte via RPC than decoding it at this point
|
||||
originCert, err := ioutil.ReadFile(originCertPath)
|
||||
if err != nil {
|
||||
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 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 {
|
||||
func getOriginCert(originCertPath string, log *zerolog.Logger) ([]byte, error) {
|
||||
if originCertPath, err := findOriginCert(originCertPath, log); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return readOriginCert(originCertPath, logger)
|
||||
return readOriginCert(originCertPath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,111 +149,56 @@ func prepareTunnelConfig(
|
||||
c *cli.Context,
|
||||
buildInfo *buildinfo.BuildInfo,
|
||||
version string,
|
||||
logger logger.Service,
|
||||
transportLogger logger.Service,
|
||||
namedTunnel *origin.NamedTunnelConfig,
|
||||
) (*origin.TunnelConfig, error) {
|
||||
compatibilityMode := namedTunnel == nil
|
||||
log, logTransport *zerolog.Logger,
|
||||
observer *connection.Observer,
|
||||
namedTunnel *connection.NamedTunnelConfig,
|
||||
) (*origin.TunnelConfig, ingress.Ingress, error) {
|
||||
isNamedTunnel := namedTunnel != nil
|
||||
|
||||
hostname, err := validation.ValidateHostname(c.String("hostname"))
|
||||
configHostname := c.String("hostname")
|
||||
hostname, err := validation.ValidateHostname(configHostname)
|
||||
if err != nil {
|
||||
logger.Errorf("Invalid hostname: %s", err)
|
||||
return nil, errors.Wrap(err, "Invalid hostname")
|
||||
log.Err(err).Str(LogFieldHostname, configHostname).Msg("Invalid hostname")
|
||||
return nil, ingress.Ingress{}, errors.Wrap(err, "Invalid hostname")
|
||||
}
|
||||
isFreeTunnel := hostname == ""
|
||||
clientID := c.String("id")
|
||||
if !c.IsSet("id") {
|
||||
clientID, err = generateRandomClientID(logger)
|
||||
clientID, err = generateRandomClientID(log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, ingress.Ingress{}, err
|
||||
}
|
||||
}
|
||||
|
||||
tags, err := NewTagSliceFromCLI(c.StringSlice("tag"))
|
||||
if err != nil {
|
||||
logger.Errorf("Tag parse failure: %s", err)
|
||||
return nil, errors.Wrap(err, "Tag parse failure")
|
||||
log.Err(err).Msg("Tag parse failure")
|
||||
return nil, ingress.Ingress{}, errors.Wrap(err, "Tag parse failure")
|
||||
}
|
||||
|
||||
tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID})
|
||||
|
||||
originURL, err := config.ValidateUrl(c, compatibilityMode)
|
||||
if err != nil {
|
||||
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, logger)
|
||||
originCertPath := c.String("origincert")
|
||||
originCertLog := log.With().
|
||||
Str(LogFieldOriginCertPath, originCertPath).
|
||||
Logger()
|
||||
|
||||
originCert, err = getOriginCert(originCertPath, &originCertLog)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error getting origin cert")
|
||||
return nil, ingress.Ingress{}, errors.Wrap(err, "Error getting origin cert")
|
||||
}
|
||||
}
|
||||
|
||||
originCertPool, err := tlsconfig.LoadOriginCA(c, logger)
|
||||
if err != nil {
|
||||
logger.Errorf("Error loading cert pool: %s", err)
|
||||
return nil, errors.Wrap(err, "Error loading cert pool")
|
||||
}
|
||||
|
||||
tunnelMetrics := origin.NewTunnelMetrics()
|
||||
httpTransport := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
MaxIdleConns: c.Int("proxy-keepalive-connections"),
|
||||
MaxIdleConnsPerHost: c.Int("proxy-keepalive-connections"),
|
||||
IdleConnTimeout: c.Duration("proxy-keepalive-timeout"),
|
||||
TLSHandshakeTimeout: c.Duration("proxy-tls-timeout"),
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
TLSClientConfig: &tls.Config{RootCAs: originCertPool, InsecureSkipVerify: c.IsSet("no-tls-verify")},
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{
|
||||
Timeout: c.Duration("proxy-connect-timeout"),
|
||||
KeepAlive: c.Duration("proxy-tcp-keepalive"),
|
||||
}
|
||||
if c.Bool("proxy-no-happy-eyeballs") {
|
||||
dialer.FallbackDelay = -1 // As of Golang 1.12, a negative delay disables "happy eyeballs"
|
||||
}
|
||||
dialContext := dialer.DialContext
|
||||
|
||||
if c.IsSet("unix-socket") {
|
||||
unixSocket, err := config.ValidateUnixSocket(c)
|
||||
if err != nil {
|
||||
logger.Errorf("Error validating --unix-socket: %s", err)
|
||||
return nil, errors.Wrap(err, "Error validating --unix-socket")
|
||||
}
|
||||
|
||||
logger.Infof("Proxying tunnel requests to unix:%s", unixSocket)
|
||||
httpTransport.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
// if --unix-socket specified, enforce network type "unix"
|
||||
return dialContext(ctx, "unix", unixSocket)
|
||||
}
|
||||
} else {
|
||||
logger.Infof("Proxying tunnel requests to %s", originURL)
|
||||
httpTransport.DialContext = dialContext
|
||||
}
|
||||
|
||||
if !c.IsSet("hello-world") && c.IsSet("origin-server-name") {
|
||||
httpTransport.TLSClientConfig.ServerName = c.String("origin-server-name")
|
||||
}
|
||||
// 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.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")
|
||||
}
|
||||
|
||||
if namedTunnel != nil {
|
||||
var (
|
||||
ingressRules ingress.Ingress
|
||||
classicTunnel *connection.ClassicTunnelConfig
|
||||
)
|
||||
if isNamedTunnel {
|
||||
clientUUID, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can't generate clientUUID")
|
||||
return nil, ingress.Ingress{}, errors.Wrap(err, "can't generate clientUUID")
|
||||
}
|
||||
namedTunnel.Client = tunnelpogs.ClientInfo{
|
||||
ClientID: clientUUID[:],
|
||||
@@ -268,42 +206,84 @@ func prepareTunnelConfig(
|
||||
Version: version,
|
||||
Arch: fmt.Sprintf("%s_%s", buildInfo.GoOS, buildInfo.GoArch),
|
||||
}
|
||||
ingressRules, err = ingress.ParseIngress(config.GetConfiguration())
|
||||
if err != nil && err != ingress.ErrNoIngressRules {
|
||||
return nil, ingress.Ingress{}, err
|
||||
}
|
||||
if !ingressRules.IsEmpty() && c.IsSet("url") {
|
||||
return nil, ingress.Ingress{}, ingress.ErrURLIncompatibleWithIngress
|
||||
}
|
||||
} else {
|
||||
classicTunnel = &connection.ClassicTunnelConfig{
|
||||
Hostname: hostname,
|
||||
OriginCert: originCert,
|
||||
// turn off use of reconnect token and auth refresh when using named tunnels
|
||||
UseReconnectToken: !isNamedTunnel && c.Bool("use-reconnect-token"),
|
||||
}
|
||||
}
|
||||
|
||||
// Convert single-origin configuration into multi-origin configuration.
|
||||
if ingressRules.IsEmpty() {
|
||||
ingressRules, err = ingress.NewSingleOrigin(c, !isNamedTunnel)
|
||||
if err != nil {
|
||||
return nil, ingress.Ingress{}, err
|
||||
}
|
||||
}
|
||||
|
||||
protocolSelector, err := connection.NewProtocolSelector(c.String("protocol"), namedTunnel, edgediscovery.HTTP2Percentage, origin.ResolveTTL, log)
|
||||
if err != nil {
|
||||
return nil, ingress.Ingress{}, err
|
||||
}
|
||||
log.Info().Msgf("Initial protocol %s", protocolSelector.Current())
|
||||
|
||||
edgeTLSConfigs := make(map[connection.Protocol]*tls.Config, len(connection.ProtocolList))
|
||||
for _, p := range connection.ProtocolList {
|
||||
edgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c, p.ServerName())
|
||||
if err != nil {
|
||||
return nil, ingress.Ingress{}, errors.Wrap(err, "unable to create TLS config to connect with edge")
|
||||
}
|
||||
edgeTLSConfigs[p] = edgeTLSConfig
|
||||
}
|
||||
|
||||
originClient := origin.NewClient(ingressRules, tags, log)
|
||||
connectionConfig := &connection.Config{
|
||||
OriginClient: originClient,
|
||||
GracePeriod: c.Duration("grace-period"),
|
||||
ReplaceExisting: c.Bool("force"),
|
||||
}
|
||||
muxerConfig := &connection.MuxerConfig{
|
||||
HeartbeatInterval: c.Duration("heartbeat-interval"),
|
||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||
MaxHeartbeats: uint64(c.Int("heartbeat-count")),
|
||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||
CompressionSetting: h2mux.CompressionSetting(uint64(c.Int("compression-quality"))),
|
||||
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
|
||||
}
|
||||
|
||||
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
|
||||
ConnectionConfig: connectionConfig,
|
||||
BuildInfo: buildInfo,
|
||||
ClientID: clientID,
|
||||
EdgeAddrs: c.StringSlice("edge"),
|
||||
HAConnections: c.Int("ha-connections"),
|
||||
IncidentLookup: origin.NewIncidentLookup(),
|
||||
IsAutoupdated: c.Bool("is-autoupdated"),
|
||||
IsFreeTunnel: isFreeTunnel,
|
||||
LBPool: c.String("lb-pool"),
|
||||
Tags: tags,
|
||||
Log: log,
|
||||
LogTransport: logTransport,
|
||||
Observer: observer,
|
||||
ReportedVersion: version,
|
||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||
Retries: uint(c.Int("retries")),
|
||||
RunFromTerminal: isRunningFromTerminal(),
|
||||
NamedTunnel: namedTunnel,
|
||||
ClassicTunnel: classicTunnel,
|
||||
MuxerConfig: muxerConfig,
|
||||
ProtocolSelector: protocolSelector,
|
||||
EdgeTLSConfigs: edgeTLSConfigs,
|
||||
}, ingressRules, nil
|
||||
}
|
||||
|
||||
func isRunningFromTerminal() bool {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// CredFinder can find the tunnel credentials file.
|
||||
type CredFinder interface {
|
||||
Path() (string, error)
|
||||
}
|
||||
|
||||
// Implements CredFinder and looks for the credentials file at the given
|
||||
// filepath.
|
||||
type staticPath struct {
|
||||
filePath string
|
||||
fs fileSystem
|
||||
}
|
||||
|
||||
func newStaticPath(filePath string, fs fileSystem) CredFinder {
|
||||
return staticPath{
|
||||
filePath: filePath,
|
||||
fs: fs,
|
||||
}
|
||||
}
|
||||
|
||||
func (a staticPath) Path() (string, error) {
|
||||
if a.filePath != "" && a.fs.validFilePath(a.filePath) {
|
||||
return a.filePath, nil
|
||||
}
|
||||
return "", fmt.Errorf("Tunnel credentials file '%s' doesn't exist or is not a file", a.filePath)
|
||||
}
|
||||
|
||||
// Implements CredFinder and looks for the credentials file in several directories
|
||||
// searching for a file named <id>.json
|
||||
type searchByID struct {
|
||||
id uuid.UUID
|
||||
c *cli.Context
|
||||
log *zerolog.Logger
|
||||
fs fileSystem
|
||||
}
|
||||
|
||||
func newSearchByID(id uuid.UUID, c *cli.Context, log *zerolog.Logger, fs fileSystem) CredFinder {
|
||||
return searchByID{
|
||||
id: id,
|
||||
c: c,
|
||||
log: log,
|
||||
fs: fs,
|
||||
}
|
||||
}
|
||||
|
||||
func (s searchByID) Path() (string, error) {
|
||||
originCertPath := s.c.String("origincert")
|
||||
originCertLog := s.log.With().
|
||||
Str(LogFieldOriginCertPath, originCertPath).
|
||||
Logger()
|
||||
|
||||
// Fallback to look for tunnel credentials in the origin cert directory
|
||||
if originCertPath, err := findOriginCert(originCertPath, &originCertLog); err == nil {
|
||||
originCertDir := filepath.Dir(originCertPath)
|
||||
if filePath, err := tunnelFilePath(s.id, originCertDir); err == nil {
|
||||
if s.fs.validFilePath(filePath) {
|
||||
return filePath, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort look under default config directories
|
||||
for _, configDir := range config.DefaultConfigSearchDirectories() {
|
||||
if filePath, err := tunnelFilePath(s.id, configDir); err == nil {
|
||||
if s.fs.validFilePath(filePath) {
|
||||
return filePath, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("tunnel credentials file not found")
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Abstract away details of reading files, so that SubcommandContext can read
|
||||
// from either the real filesystem, or a mock (when running unit tests).
|
||||
type fileSystem interface {
|
||||
readFile(filePath string) ([]byte, error)
|
||||
validFilePath(path string) bool
|
||||
}
|
||||
|
||||
type realFileSystem struct{}
|
||||
|
||||
func (fs realFileSystem) validFilePath(path string) bool {
|
||||
fileStat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !fileStat.IsDir()
|
||||
}
|
||||
|
||||
func (fs realFileSystem) readFile(filePath string) ([]byte, error) {
|
||||
return ioutil.ReadFile(filePath)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func buildIngressSubcommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "ingress",
|
||||
Category: "Tunnel",
|
||||
Usage: "Validate and test cloudflared tunnel's ingress configuration",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] ingress COMMAND [arguments...]",
|
||||
Hidden: true,
|
||||
Description: ` Cloudflared lets you route traffic from the internet to multiple different addresses on your
|
||||
origin. Multiple-origin routing is configured by a set of rules. Each rule matches traffic
|
||||
by its hostname or path, and routes it to an address. These rules are configured under the
|
||||
'ingress' key of your config.yaml, for example:
|
||||
|
||||
ingress:
|
||||
- hostname: www.example.com
|
||||
service: https://localhost:8000
|
||||
- hostname: *.example.xyz
|
||||
path: /[a-zA-Z]+.html
|
||||
service: https://localhost:8001
|
||||
- hostname: *
|
||||
service: https://localhost:8002
|
||||
|
||||
To ensure cloudflared can route all incoming requests, the last rule must be a catch-all
|
||||
rule that matches all traffic. You can validate these rules with the 'ingress validate'
|
||||
command, and test which rule matches a particular URL with 'ingress rule <URL>'.
|
||||
|
||||
Multiple-origin routing is incompatible with the --url flag.`,
|
||||
Subcommands: []*cli.Command{buildValidateIngressCommand(), buildTestURLCommand()},
|
||||
}
|
||||
}
|
||||
|
||||
func buildValidateIngressCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "validate",
|
||||
Action: cliutil.ErrorHandler(validateIngressCommand),
|
||||
Usage: "Validate the ingress configuration ",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] ingress validate",
|
||||
Description: "Validates the configuration file, ensuring your ingress rules are OK.",
|
||||
}
|
||||
}
|
||||
|
||||
func buildTestURLCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "rule",
|
||||
Action: cliutil.ErrorHandler(testURLCommand),
|
||||
Usage: "Check which ingress rule matches a given request URL",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] ingress rule URL",
|
||||
ArgsUsage: "URL",
|
||||
Description: "Check which ingress rule matches a given request URL. " +
|
||||
"Ingress rules match a request's hostname and path. Hostname is " +
|
||||
"optional and is either a full hostname like `www.example.com` or a " +
|
||||
"hostname with a `*` for its subdomains, e.g. `*.example.com`. Path " +
|
||||
"is optional and matches a regular expression, like `/[a-zA-Z0-9_]+.html`",
|
||||
}
|
||||
}
|
||||
|
||||
// validateIngressCommand check the syntax of the ingress rules in the cloudflared config file
|
||||
func validateIngressCommand(c *cli.Context) error {
|
||||
conf := config.GetConfiguration()
|
||||
if conf.Source() == "" {
|
||||
fmt.Println("No configuration file was found. Please create one, or use the --config flag to specify its filepath. You can use the help command to learn more about configuration files")
|
||||
return nil
|
||||
}
|
||||
fmt.Println("Validating rules from", conf.Source())
|
||||
if _, err := ingress.ParseIngress(conf); err != nil {
|
||||
return errors.Wrap(err, "Validation failed")
|
||||
}
|
||||
if c.IsSet("url") {
|
||||
return ingress.ErrURLIncompatibleWithIngress
|
||||
}
|
||||
fmt.Println("OK")
|
||||
return nil
|
||||
}
|
||||
|
||||
// testURLCommand checks which ingress rule matches the given URL.
|
||||
func testURLCommand(c *cli.Context) error {
|
||||
requestArg := c.Args().First()
|
||||
if requestArg == "" {
|
||||
return errors.New("cloudflared tunnel rule expects a single argument, the URL to test")
|
||||
}
|
||||
|
||||
requestURL, err := url.Parse(requestArg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s is not a valid URL", requestArg)
|
||||
}
|
||||
if requestURL.Hostname() == "" && requestURL.Scheme == "" {
|
||||
return fmt.Errorf("%s doesn't have a hostname, consider adding a scheme", requestArg)
|
||||
}
|
||||
|
||||
conf := config.GetConfiguration()
|
||||
fmt.Println("Using rules from", conf.Source())
|
||||
ing, err := ingress.ParseIngress(conf)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Validation failed")
|
||||
}
|
||||
|
||||
_, i := ing.FindMatchingRule(requestURL.Hostname(), requestURL.Path)
|
||||
fmt.Printf("Matched rule #%d\n", i+1)
|
||||
fmt.Println(ing.Rules[i].MultiLineString())
|
||||
return nil
|
||||
}
|
||||
@@ -2,17 +2,20 @@ package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -20,11 +23,24 @@ const (
|
||||
callbackStoreURL = "https://login.argotunnel.com/"
|
||||
)
|
||||
|
||||
func login(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
func buildLoginSubcommand(hidden bool) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "login",
|
||||
Action: cliutil.ErrorHandler(login),
|
||||
Usage: "Generate a configuration file with your login details",
|
||||
ArgsUsage: " ",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "url",
|
||||
Hidden: true,
|
||||
},
|
||||
},
|
||||
Hidden: hidden,
|
||||
}
|
||||
}
|
||||
|
||||
func login(c *cli.Context) error {
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
path, ok, err := checkForExistingCert()
|
||||
if ok {
|
||||
@@ -40,18 +56,30 @@ func login(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = transfer.Run(loginURL, "cert", "callback", callbackStoreURL, path, false, true, logger)
|
||||
resourceData, err := transfer.Run(
|
||||
loginURL,
|
||||
"cert",
|
||||
"callback",
|
||||
callbackStoreURL,
|
||||
false,
|
||||
false,
|
||||
log,
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to write the certificate due to the following error:\n%v\n\nYour browser will download the certificate instead. You will have to manually\ncopy it to the following path:\n\n%s\n", err, path)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(path, resourceData, 0600); err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("error writing cert to %s", path))
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stdout, "You have successfully logged in.\nIf you wish to copy your credentials to a server, they have been saved to:\n%s\n", path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkForExistingCert() (string, bool, error) {
|
||||
configPath, err := homedir.Expand(config.DefaultConfigDirs[0])
|
||||
configPath, err := homedir.Expand(config.DefaultConfigSearchDirectories()[0])
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"fmt"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func runDNSProxyServer(c *cli.Context, dnsReadySignal, shutdownC chan struct{}, logger logger.Service) error {
|
||||
func runDNSProxyServer(c *cli.Context, dnsReadySignal chan struct{}, shutdownC <-chan struct{}, log *zerolog.Logger) error {
|
||||
port := c.Int("proxy-dns-port")
|
||||
if port <= 0 || port > 65535 {
|
||||
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"), logger)
|
||||
maxUpstreamConnections := c.Int("proxy-dns-max-upstream-conns")
|
||||
if maxUpstreamConnections < 0 {
|
||||
return fmt.Errorf("'%s' must be 0 or higher", "proxy-dns-max-upstream-conns")
|
||||
}
|
||||
listener, err := tunneldns.CreateListener(c.String("proxy-dns-address"), uint16(port), c.StringSlice("proxy-dns-upstream"), c.StringSlice("proxy-dns-bootstrap"), maxUpstreamConnections, log)
|
||||
if err != nil {
|
||||
close(dnsReadySignal)
|
||||
listener.Stop()
|
||||
@@ -26,6 +31,7 @@ func runDNSProxyServer(c *cli.Context, dnsReadySignal, shutdownC chan struct{},
|
||||
return errors.Wrap(err, "Cannot start the DNS over HTTPS proxy server")
|
||||
}
|
||||
<-shutdownC
|
||||
listener.Stop()
|
||||
_ = listener.Stop()
|
||||
log.Info().Msg("DNS server stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,83 +4,20 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// 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{}, logger logger.Service) error {
|
||||
// waitForSignal closes graceShutdownC to indicate that we should start graceful shutdown sequence
|
||||
func waitForSignal(graceShutdownC chan struct{}, logger *zerolog.Logger) {
|
||||
signals := make(chan os.Signal, 10)
|
||||
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
|
||||
defer signal.Stop(signals)
|
||||
|
||||
select {
|
||||
case err := <-errC:
|
||||
logger.Infof("terminating due to error: %v", err)
|
||||
close(shutdownC)
|
||||
return err
|
||||
case s := <-signals:
|
||||
logger.Infof("terminating due to signal %s", s)
|
||||
close(shutdownC)
|
||||
case <-shutdownC:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// waitForSignalWithGraceShutdown notifies all routines to shutdown immediately
|
||||
// by closing the shutdownC when one of the routines in main exits.
|
||||
// When this process recieves SIGTERM/SIGINT, it closes the graceShutdownC to
|
||||
// notify certain routines to start graceful shutdown. When grace period is over,
|
||||
// or when some routine exits, it notifies the rest of the routines to shutdown
|
||||
// immediately by closing shutdownC.
|
||||
// In the case of handling commands from Windows Service Manager, closing graceShutdownC
|
||||
// initiate graceful shutdown.
|
||||
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)
|
||||
defer signal.Stop(signals)
|
||||
|
||||
select {
|
||||
case err := <-errC:
|
||||
logger.Infof("Initiating graceful shutdown due to %v ...", err)
|
||||
logger.Info().Msgf("Initiating graceful shutdown due to signal %s ...", s)
|
||||
close(graceShutdownC)
|
||||
close(shutdownC)
|
||||
return err
|
||||
case s := <-signals:
|
||||
logger.Infof("Initiating graceful shutdown due to signal %s ...", s)
|
||||
close(graceShutdownC)
|
||||
waitForGracePeriod(signals, errC, shutdownC, gracePeriod, logger)
|
||||
case <-graceShutdownC:
|
||||
waitForGracePeriod(signals, errC, shutdownC, gracePeriod, logger)
|
||||
case <-shutdownC:
|
||||
close(graceShutdownC)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
signal.Stop(signals)
|
||||
graceTimerTick := time.Tick(gracePeriod)
|
||||
// send close signal via shutdownC when grace period expires or when an
|
||||
// error is encountered.
|
||||
select {
|
||||
case <-graceTimerTick:
|
||||
case <-errC:
|
||||
}
|
||||
close(shutdownC)
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,12 @@ package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -18,140 +19,86 @@ var (
|
||||
graceShutdownErr = fmt.Errorf("receive grace shutdown")
|
||||
)
|
||||
|
||||
func testChannelClosed(t *testing.T, c chan struct{}) {
|
||||
func channelClosed(c chan struct{}) bool {
|
||||
select {
|
||||
case <-c:
|
||||
return
|
||||
return true
|
||||
default:
|
||||
t.Fatal("Channel should be closed")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForSignal(t *testing.T) {
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
// Test handling server error
|
||||
errC := make(chan error)
|
||||
shutdownC := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
errC <- serverErr
|
||||
}()
|
||||
|
||||
// received error, shutdownC should be closed
|
||||
err := waitForSignal(errC, shutdownC, logger)
|
||||
assert.Equal(t, serverErr, err)
|
||||
testChannelClosed(t, shutdownC)
|
||||
func TestSignalShutdown(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
|
||||
// Test handling SIGTERM & SIGINT
|
||||
for _, sig := range []syscall.Signal{syscall.SIGTERM, syscall.SIGINT} {
|
||||
errC = make(chan error)
|
||||
shutdownC = make(chan struct{})
|
||||
|
||||
go func(shutdownC chan struct{}) {
|
||||
<-shutdownC
|
||||
errC <- shutdownErr
|
||||
}(shutdownC)
|
||||
graceShutdownC := make(chan struct{})
|
||||
|
||||
go func(sig syscall.Signal) {
|
||||
// sleep for a tick to prevent sending signal before calling waitForSignal
|
||||
time.Sleep(tick)
|
||||
syscall.Kill(syscall.Getpid(), sig)
|
||||
_ = syscall.Kill(syscall.Getpid(), sig)
|
||||
}(sig)
|
||||
|
||||
err = waitForSignal(errC, shutdownC, logger)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, shutdownErr, <-errC)
|
||||
testChannelClosed(t, shutdownC)
|
||||
time.AfterFunc(time.Second, func() {
|
||||
select {
|
||||
case <-graceShutdownC:
|
||||
default:
|
||||
close(graceShutdownC)
|
||||
t.Fatal("waitForSignal timed out")
|
||||
}
|
||||
})
|
||||
|
||||
waitForSignal(graceShutdownC, &log)
|
||||
assert.True(t, channelClosed(graceShutdownC))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForSignalWithGraceShutdown(t *testing.T) {
|
||||
// Test server returning error
|
||||
errC := make(chan error)
|
||||
shutdownC := make(chan struct{})
|
||||
graceshutdownC := make(chan struct{})
|
||||
func TestWaitForShutdown(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
|
||||
errC := make(chan error)
|
||||
graceShutdownC := make(chan struct{})
|
||||
const gracePeriod = 5 * time.Second
|
||||
|
||||
contextCancelled := false
|
||||
cancel := func() {
|
||||
contextCancelled = true
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// on, error stop immediately
|
||||
contextCancelled = false
|
||||
startTime := time.Now()
|
||||
go func() {
|
||||
errC <- serverErr
|
||||
}()
|
||||
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
// received error, both shutdownC and graceshutdownC should be closed
|
||||
err := waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
|
||||
err := waitToShutdown(&wg, cancel, errC, graceShutdownC, gracePeriod, &log)
|
||||
assert.Equal(t, serverErr, err)
|
||||
testChannelClosed(t, shutdownC)
|
||||
testChannelClosed(t, graceshutdownC)
|
||||
assert.True(t, contextCancelled)
|
||||
assert.False(t, channelClosed(graceShutdownC))
|
||||
assert.True(t, time.Now().Sub(startTime) < time.Second) // check that wait ended early
|
||||
|
||||
// shutdownC closed, graceshutdownC should also be closed and no error
|
||||
errC = make(chan error)
|
||||
shutdownC = make(chan struct{})
|
||||
graceshutdownC = make(chan struct{})
|
||||
close(shutdownC)
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
|
||||
assert.NoError(t, err)
|
||||
testChannelClosed(t, shutdownC)
|
||||
testChannelClosed(t, graceshutdownC)
|
||||
// on graceful shutdown, ignore error but stop as soon as an error arrives
|
||||
contextCancelled = false
|
||||
startTime = time.Now()
|
||||
go func() {
|
||||
close(graceShutdownC)
|
||||
time.Sleep(tick)
|
||||
errC <- serverErr
|
||||
}()
|
||||
err = waitToShutdown(&wg, cancel, errC, graceShutdownC, gracePeriod, &log)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, contextCancelled)
|
||||
assert.True(t, time.Now().Sub(startTime) < time.Second) // check that wait ended early
|
||||
|
||||
// graceshutdownC closed, shutdownC should also be closed and no error
|
||||
errC = make(chan error)
|
||||
shutdownC = make(chan struct{})
|
||||
graceshutdownC = make(chan struct{})
|
||||
close(graceshutdownC)
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
|
||||
assert.NoError(t, err)
|
||||
testChannelClosed(t, shutdownC)
|
||||
testChannelClosed(t, graceshutdownC)
|
||||
|
||||
// Test handling SIGTERM & SIGINT
|
||||
for _, sig := range []syscall.Signal{syscall.SIGTERM, syscall.SIGINT} {
|
||||
errC := make(chan error)
|
||||
shutdownC = make(chan struct{})
|
||||
graceshutdownC = make(chan struct{})
|
||||
|
||||
go func(shutdownC, graceshutdownC chan struct{}) {
|
||||
<-graceshutdownC
|
||||
<-shutdownC
|
||||
errC <- graceShutdownErr
|
||||
}(shutdownC, graceshutdownC)
|
||||
|
||||
go func(sig syscall.Signal) {
|
||||
// sleep for a tick to prevent sending signal before calling waitForSignalWithGraceShutdown
|
||||
time.Sleep(tick)
|
||||
syscall.Kill(syscall.Getpid(), sig)
|
||||
}(sig)
|
||||
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, graceShutdownErr, <-errC)
|
||||
testChannelClosed(t, shutdownC)
|
||||
testChannelClosed(t, graceshutdownC)
|
||||
}
|
||||
|
||||
// Test handling SIGTERM & SIGINT, server send error before end of grace period
|
||||
for _, sig := range []syscall.Signal{syscall.SIGTERM, syscall.SIGINT} {
|
||||
errC := make(chan error)
|
||||
shutdownC = make(chan struct{})
|
||||
graceshutdownC = make(chan struct{})
|
||||
|
||||
go func(shutdownC, graceshutdownC chan struct{}) {
|
||||
<-graceshutdownC
|
||||
errC <- graceShutdownErr
|
||||
<-shutdownC
|
||||
errC <- shutdownErr
|
||||
}(shutdownC, graceshutdownC)
|
||||
|
||||
go func(sig syscall.Signal) {
|
||||
// sleep for a tick to prevent sending signal before calling waitForSignalWithGraceShutdown
|
||||
time.Sleep(tick)
|
||||
syscall.Kill(syscall.Getpid(), sig)
|
||||
}(sig)
|
||||
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, shutdownErr, <-errC)
|
||||
testChannelClosed(t, shutdownC)
|
||||
testChannelClosed(t, graceshutdownC)
|
||||
}
|
||||
// with graceShutdownC closed stop right away without grace period
|
||||
contextCancelled = false
|
||||
startTime = time.Now()
|
||||
err = waitToShutdown(&wg, cancel, errC, graceShutdownC, 0, &log)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, contextCancelled)
|
||||
assert.True(t, time.Now().Sub(startTime) < time.Second) // check that wait ended early
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/certutil"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
)
|
||||
|
||||
type errInvalidJSONCredential struct {
|
||||
err error
|
||||
path string
|
||||
}
|
||||
|
||||
func (e errInvalidJSONCredential) Error() string {
|
||||
return "Invalid JSON when parsing tunnel credentials file"
|
||||
}
|
||||
|
||||
// subcommandContext carries structs shared between subcommands, to reduce number of arguments needed to
|
||||
// pass between subcommands, and make sure they are only initialized once
|
||||
type subcommandContext struct {
|
||||
c *cli.Context
|
||||
log *zerolog.Logger
|
||||
isUIEnabled bool
|
||||
fs fileSystem
|
||||
|
||||
// These fields should be accessed using their respective Getter
|
||||
tunnelstoreClient tunnelstore.Client
|
||||
userCredential *userCredential
|
||||
}
|
||||
|
||||
func newSubcommandContext(c *cli.Context) (*subcommandContext, error) {
|
||||
isUIEnabled := c.IsSet(uiFlag) && c.String("name") != ""
|
||||
|
||||
// If UI is enabled, terminal log output should be disabled -- log should be written into a UI log window instead
|
||||
log := logger.CreateLoggerFromContext(c, isUIEnabled)
|
||||
|
||||
return &subcommandContext{
|
||||
c: c,
|
||||
log: log,
|
||||
isUIEnabled: isUIEnabled,
|
||||
fs: realFileSystem{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Returns something that can find the given tunnel's credentials file.
|
||||
func (sc *subcommandContext) credentialFinder(tunnelID uuid.UUID) CredFinder {
|
||||
if path := sc.c.String(CredFileFlag); path != "" {
|
||||
return newStaticPath(path, sc.fs)
|
||||
}
|
||||
return newSearchByID(tunnelID, sc.c, sc.log, sc.fs)
|
||||
}
|
||||
|
||||
type userCredential struct {
|
||||
cert *certutil.OriginCert
|
||||
certPath string
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) client() (tunnelstore.Client, error) {
|
||||
if sc.tunnelstoreClient != nil {
|
||||
return sc.tunnelstoreClient, nil
|
||||
}
|
||||
credential, err := sc.credential()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userAgent := fmt.Sprintf("cloudflared/%s", version)
|
||||
client, err := tunnelstore.NewRESTClient(
|
||||
sc.c.String("api-url"),
|
||||
credential.cert.AccountID,
|
||||
credential.cert.ZoneID,
|
||||
credential.cert.ServiceKey,
|
||||
userAgent,
|
||||
sc.log,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sc.tunnelstoreClient = client
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) credential() (*userCredential, error) {
|
||||
if sc.userCredential == nil {
|
||||
originCertPath := sc.c.String("origincert")
|
||||
originCertLog := sc.log.With().
|
||||
Str(LogFieldOriginCertPath, originCertPath).
|
||||
Logger()
|
||||
|
||||
originCertPath, err := findOriginCert(originCertPath, &originCertLog)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error locating origin cert")
|
||||
}
|
||||
blocks, err := readOriginCert(originCertPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Can't read origin cert from %s", originCertPath)
|
||||
}
|
||||
|
||||
cert, err := certutil.DecodeOriginCert(blocks)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error decoding origin cert")
|
||||
}
|
||||
|
||||
if cert.AccountID == "" {
|
||||
return nil, errors.Errorf(`Origin certificate needs to be refreshed before creating new tunnels.\nDelete %s and run "cloudflared login" to obtain a new cert.`, originCertPath)
|
||||
}
|
||||
|
||||
sc.userCredential = &userCredential{
|
||||
cert: cert,
|
||||
certPath: originCertPath,
|
||||
}
|
||||
}
|
||||
return sc.userCredential, nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) readTunnelCredentials(credFinder CredFinder) (connection.Credentials, error) {
|
||||
filePath, err := credFinder.Path()
|
||||
if err != nil {
|
||||
return connection.Credentials{}, err
|
||||
}
|
||||
body, err := sc.fs.readFile(filePath)
|
||||
if err != nil {
|
||||
return connection.Credentials{}, errors.Wrapf(err, "couldn't read tunnel credentials from %v", filePath)
|
||||
}
|
||||
|
||||
var credentials connection.Credentials
|
||||
if err = json.Unmarshal(body, &credentials); err != nil {
|
||||
if strings.HasSuffix(filePath, ".pem") {
|
||||
return connection.Credentials{}, fmt.Errorf("The tunnel credentials file should be .json but you gave a .pem. " +
|
||||
"The tunnel credentials file was originally created by `cloudflared tunnel create`. " +
|
||||
"You may have accidentally used the filepath to cert.pem, which is generated by `cloudflared tunnel " +
|
||||
"login`.")
|
||||
}
|
||||
return connection.Credentials{}, errInvalidJSONCredential{path: filePath, err: err}
|
||||
}
|
||||
return credentials, nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) create(name string, credentialsOutputPath string) (*tunnelstore.Tunnel, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "couldn't create client to talk to Argo Tunnel backend")
|
||||
}
|
||||
|
||||
tunnelSecret, err := generateTunnelSecret()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "couldn't generate the secret for your new tunnel")
|
||||
}
|
||||
|
||||
tunnel, err := client.CreateTunnel(name, tunnelSecret)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Create Tunnel API call failed")
|
||||
}
|
||||
|
||||
credential, err := sc.credential()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tunnelCredentials := connection.Credentials{
|
||||
AccountTag: credential.cert.AccountID,
|
||||
TunnelSecret: tunnelSecret,
|
||||
TunnelID: tunnel.ID,
|
||||
TunnelName: name,
|
||||
}
|
||||
filePath, writeFileErr := writeTunnelCredentials(credential.certPath, credentialsOutputPath, &tunnelCredentials)
|
||||
if writeFileErr != 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 nil, errors.New(errorMsg)
|
||||
}
|
||||
sc.log.Info().Msgf("Tunnel credentials written to %v. cloudflared chose this file based on where your origin certificate was found. Keep this file secret. To revoke these credentials, delete the tunnel.", filePath)
|
||||
|
||||
if outputFormat := sc.c.String(outputFormatFlag.Name); outputFormat != "" {
|
||||
return nil, renderOutput(outputFormat, &tunnel)
|
||||
}
|
||||
|
||||
sc.log.Info().Msgf("Created tunnel %s with id %s", tunnel.Name, tunnel.ID)
|
||||
return tunnel, nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) list(filter *tunnelstore.Filter) ([]*tunnelstore.Tunnel, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.ListTunnels(filter)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error {
|
||||
forceFlagSet := sc.c.Bool("force")
|
||||
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, id := range tunnelIDs {
|
||||
tunnel, err := client.GetTunnel(id)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Can't get tunnel information. Please check tunnel id: %s", tunnel.ID)
|
||||
}
|
||||
|
||||
// Check if tunnel DeletedAt field has already been set
|
||||
if !tunnel.DeletedAt.IsZero() {
|
||||
return fmt.Errorf("Tunnel %s has already been deleted", tunnel.ID)
|
||||
}
|
||||
// Check if tunnel has existing connections and if force flag is set, cleanup connections
|
||||
if len(tunnel.Connections) > 0 {
|
||||
if !forceFlagSet {
|
||||
return fmt.Errorf("You can not delete tunnel %s because it has active connections. To see connections run the 'list' command. If you believe the tunnel is not active, you can use a -f / --force flag with this command.", id)
|
||||
}
|
||||
|
||||
if err := client.CleanupConnections(tunnel.ID); err != nil {
|
||||
return errors.Wrapf(err, "Error cleaning up connections for tunnel %s", tunnel.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if err := client.DeleteTunnel(tunnel.ID); err != nil {
|
||||
return errors.Wrapf(err, "Error deleting tunnel %s", tunnel.ID)
|
||||
}
|
||||
|
||||
credFinder := sc.credentialFinder(id)
|
||||
if tunnelCredentialsPath, err := credFinder.Path(); err == nil {
|
||||
if err = os.Remove(tunnelCredentialsPath); err != nil {
|
||||
sc.log.Info().Msgf("Tunnel %v was deleted, but we could not remove its credentials file %s: %s. Consider deleting this file manually.", id, tunnelCredentialsPath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// findCredentials will choose the right way to find the credentials file, find it,
|
||||
// and add the TunnelID into any old credentials (generated before TUN-3581 added the `TunnelID`
|
||||
// field to credentials files)
|
||||
func (sc *subcommandContext) findCredentials(tunnelID uuid.UUID) (connection.Credentials, error) {
|
||||
credFinder := sc.credentialFinder(tunnelID)
|
||||
credentials, err := sc.readTunnelCredentials(credFinder)
|
||||
// This line ensures backwards compatibility with credentials files generated before
|
||||
// TUN-3581. Those old credentials files don't have a TunnelID field, so we enrich the struct
|
||||
// with the ID, which we have already resolved from the user input.
|
||||
credentials.TunnelID = tunnelID
|
||||
return credentials, err
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) run(tunnelID uuid.UUID) error {
|
||||
credentials, err := sc.findCredentials(tunnelID)
|
||||
if err != nil {
|
||||
if e, ok := err.(errInvalidJSONCredential); ok {
|
||||
sc.log.Error().Msgf("The credentials file at %s contained invalid JSON. This is probably caused by passing the wrong filepath. Reminder: the credentials file is a .json file created via `cloudflared tunnel create`.", e.path)
|
||||
sc.log.Error().Msgf("Invalid JSON when parsing credentials file: %s", e.err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return StartServer(
|
||||
sc.c,
|
||||
version,
|
||||
&connection.NamedTunnelConfig{Credentials: credentials},
|
||||
sc.log,
|
||||
sc.isUIEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) cleanupConnections(tunnelIDs []uuid.UUID) error {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, tunnelID := range tunnelIDs {
|
||||
sc.log.Info().Msgf("Cleanup connection for tunnel %s", tunnelID)
|
||||
if err := client.CleanupConnections(tunnelID); err != nil {
|
||||
sc.log.Error().Msgf("Error cleaning up connections for tunnel %v, error :%v", tunnelID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) route(tunnelID uuid.UUID, r tunnelstore.Route) (tunnelstore.RouteResult, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.RouteTunnel(tunnelID, r)
|
||||
}
|
||||
|
||||
// Query Tunnelstore to find the active tunnel with the given name.
|
||||
func (sc *subcommandContext) tunnelActive(name string) (*tunnelstore.Tunnel, bool, error) {
|
||||
filter := tunnelstore.NewFilter()
|
||||
filter.NoDeleted()
|
||||
filter.ByName(name)
|
||||
tunnels, err := sc.list(filter)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if len(tunnels) == 0 {
|
||||
return nil, false, nil
|
||||
}
|
||||
// There should only be 1 active tunnel for a given name
|
||||
return tunnels[0], true, nil
|
||||
}
|
||||
|
||||
// findID parses the input. If it's a UUID, return the UUID.
|
||||
// Otherwise, assume it's a name, and look up the ID of that tunnel.
|
||||
func (sc *subcommandContext) findID(input string) (uuid.UUID, error) {
|
||||
if u, err := uuid.Parse(input); err == nil {
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// Look up name in the credentials file.
|
||||
credFinder := newStaticPath(sc.c.String(CredFileFlag), sc.fs)
|
||||
if credentials, err := sc.readTunnelCredentials(credFinder); err == nil {
|
||||
if credentials.TunnelID != uuid.Nil && input == credentials.TunnelName {
|
||||
return credentials.TunnelID, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to querying Tunnelstore.
|
||||
if tunnel, found, err := sc.tunnelActive(input); err != nil {
|
||||
return uuid.Nil, err
|
||||
} else if found {
|
||||
return tunnel.ID, nil
|
||||
}
|
||||
|
||||
return uuid.Nil, fmt.Errorf("%s is neither the ID nor the name of any of your tunnels", input)
|
||||
}
|
||||
|
||||
// findIDs is just like mapping `findID` over a slice, but it only uses
|
||||
// one Tunnelstore API call.
|
||||
func (sc *subcommandContext) findIDs(inputs []string) ([]uuid.UUID, error) {
|
||||
|
||||
// First, look up all tunnels the user has
|
||||
filter := tunnelstore.NewFilter()
|
||||
filter.NoDeleted()
|
||||
tunnels, err := sc.list(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Do the pure list-processing in its own function, so that it can be
|
||||
// unit tested easily.
|
||||
return findIDs(tunnels, inputs)
|
||||
}
|
||||
|
||||
func findIDs(tunnels []*tunnelstore.Tunnel, inputs []string) ([]uuid.UUID, error) {
|
||||
// Put them into a dictionary for faster lookups
|
||||
nameToID := make(map[string]uuid.UUID, len(tunnels))
|
||||
for _, tunnel := range tunnels {
|
||||
nameToID[tunnel.Name] = tunnel.ID
|
||||
}
|
||||
|
||||
// For each input, try to find the tunnel ID.
|
||||
tunnelIDs := make([]uuid.UUID, len(inputs))
|
||||
var badInputs []string
|
||||
for i, input := range inputs {
|
||||
if id, err := uuid.Parse(input); err == nil {
|
||||
tunnelIDs[i] = id
|
||||
} else if id, ok := nameToID[input]; ok {
|
||||
tunnelIDs[i] = id
|
||||
} else {
|
||||
badInputs = append(badInputs, input)
|
||||
}
|
||||
}
|
||||
if len(badInputs) > 0 {
|
||||
msg := "Please specify either the ID or name of a tunnel. The following inputs were neither: %s"
|
||||
return nil, fmt.Errorf(msg, strings.Join(badInputs, ", "))
|
||||
}
|
||||
return tunnelIDs, nil
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/cloudflare/cloudflared/teamnet"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const noClientMsg = "error while creating backend client"
|
||||
|
||||
func (sc *subcommandContext) listRoutes(filter *teamnet.Filter) ([]*teamnet.DetailedRoute, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, noClientMsg)
|
||||
}
|
||||
return client.ListRoutes(filter)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) addRoute(newRoute teamnet.NewRoute) (teamnet.Route, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return teamnet.Route{}, errors.Wrap(err, noClientMsg)
|
||||
}
|
||||
return client.AddRoute(newRoute)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) deleteRoute(network net.IPNet) error {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, noClientMsg)
|
||||
}
|
||||
return client.DeleteRoute(network)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) getRouteByIP(ip net.IP) (teamnet.DetailedRoute, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return teamnet.DetailedRoute{}, errors.Wrap(err, noClientMsg)
|
||||
}
|
||||
return client.GetByIP(ip)
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/rs/zerolog"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func Test_findIDs(t *testing.T) {
|
||||
type args struct {
|
||||
tunnels []*tunnelstore.Tunnel
|
||||
inputs []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []uuid.UUID
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "input not found",
|
||||
args: args{
|
||||
inputs: []string{"asdf"},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "only UUID",
|
||||
args: args{
|
||||
inputs: []string{"a8398a0b-876d-48ed-b609-3fcfd67a4950"},
|
||||
},
|
||||
want: []uuid.UUID{uuid.MustParse("a8398a0b-876d-48ed-b609-3fcfd67a4950")},
|
||||
},
|
||||
{
|
||||
name: "only name",
|
||||
args: args{
|
||||
tunnels: []*tunnelstore.Tunnel{
|
||||
{
|
||||
ID: uuid.MustParse("a8398a0b-876d-48ed-b609-3fcfd67a4950"),
|
||||
Name: "tunnel1",
|
||||
},
|
||||
},
|
||||
inputs: []string{"tunnel1"},
|
||||
},
|
||||
want: []uuid.UUID{uuid.MustParse("a8398a0b-876d-48ed-b609-3fcfd67a4950")},
|
||||
},
|
||||
{
|
||||
name: "both UUID and name",
|
||||
args: args{
|
||||
tunnels: []*tunnelstore.Tunnel{
|
||||
{
|
||||
ID: uuid.MustParse("a8398a0b-876d-48ed-b609-3fcfd67a4950"),
|
||||
Name: "tunnel1",
|
||||
},
|
||||
{
|
||||
ID: uuid.MustParse("bf028b68-744f-466e-97f8-c46161d80aa5"),
|
||||
Name: "tunnel2",
|
||||
},
|
||||
},
|
||||
inputs: []string{"tunnel1", "bf028b68-744f-466e-97f8-c46161d80aa5"},
|
||||
},
|
||||
want: []uuid.UUID{
|
||||
uuid.MustParse("a8398a0b-876d-48ed-b609-3fcfd67a4950"),
|
||||
uuid.MustParse("bf028b68-744f-466e-97f8-c46161d80aa5"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := findIDs(tt.args.tunnels, tt.args.inputs)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("findIDs() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("findIDs() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockFileSystem struct {
|
||||
rf func(string) ([]byte, error)
|
||||
vfp func(string) bool
|
||||
}
|
||||
|
||||
func (fs mockFileSystem) validFilePath(path string) bool {
|
||||
return fs.vfp(path)
|
||||
}
|
||||
|
||||
func (fs mockFileSystem) readFile(filePath string) ([]byte, error) {
|
||||
return fs.rf(filePath)
|
||||
}
|
||||
|
||||
func Test_subcommandContext_findCredentials(t *testing.T) {
|
||||
type fields struct {
|
||||
c *cli.Context
|
||||
log *zerolog.Logger
|
||||
isUIEnabled bool
|
||||
fs fileSystem
|
||||
tunnelstoreClient tunnelstore.Client
|
||||
userCredential *userCredential
|
||||
}
|
||||
type args struct {
|
||||
tunnelID uuid.UUID
|
||||
}
|
||||
oldCertPath := "old_cert.json"
|
||||
newCertPath := "new_cert.json"
|
||||
accountTag := "0000d4d14e84bd4ae5a6a02e0000ac63"
|
||||
secret := []byte{211, 79, 177, 245, 179, 194, 152, 127, 140, 71, 18, 46, 183, 209, 10, 24, 192, 150, 55, 249, 211, 16, 167, 30, 113, 51, 152, 168, 72, 100, 205, 144}
|
||||
secretB64 := base64.StdEncoding.EncodeToString(secret)
|
||||
tunnelID := uuid.MustParse("df5ed608-b8b4-4109-89f3-9f2cf199df64")
|
||||
name := "mytunnel"
|
||||
|
||||
fs := mockFileSystem{
|
||||
rf: func(filePath string) ([]byte, error) {
|
||||
if filePath == oldCertPath {
|
||||
// An old credentials file created before TUN-3581 added the new fields
|
||||
return []byte(fmt.Sprintf(`{"AccountTag":"%s","TunnelSecret":"%s"}`, accountTag, secretB64)), nil
|
||||
}
|
||||
if filePath == newCertPath {
|
||||
// A new credentials file created after TUN-3581 with its new fields.
|
||||
return []byte(fmt.Sprintf(`{"AccountTag":"%s","TunnelSecret":"%s","TunnelID":"%s","TunnelName":"%s"}`, accountTag, secretB64, tunnelID, name)), nil
|
||||
}
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
vfp: func(string) bool { return true },
|
||||
}
|
||||
log := zerolog.Nop()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want connection.Credentials
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Filepath given leads to old credentials file",
|
||||
fields: fields{
|
||||
log: &log,
|
||||
fs: fs,
|
||||
c: func() *cli.Context {
|
||||
flagSet := flag.NewFlagSet("test0", flag.PanicOnError)
|
||||
flagSet.String(CredFileFlag, oldCertPath, "")
|
||||
c := cli.NewContext(cli.NewApp(), flagSet, nil)
|
||||
_ = c.Set(CredFileFlag, oldCertPath)
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
args: args{
|
||||
tunnelID: tunnelID,
|
||||
},
|
||||
want: connection.Credentials{
|
||||
AccountTag: accountTag,
|
||||
TunnelID: tunnelID,
|
||||
TunnelSecret: secret,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Filepath given leads to new credentials file",
|
||||
fields: fields{
|
||||
log: &log,
|
||||
fs: fs,
|
||||
c: func() *cli.Context {
|
||||
flagSet := flag.NewFlagSet("test0", flag.PanicOnError)
|
||||
flagSet.String(CredFileFlag, newCertPath, "")
|
||||
c := cli.NewContext(cli.NewApp(), flagSet, nil)
|
||||
_ = c.Set(CredFileFlag, newCertPath)
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
args: args{
|
||||
tunnelID: tunnelID,
|
||||
},
|
||||
want: connection.Credentials{
|
||||
AccountTag: accountTag,
|
||||
TunnelID: tunnelID,
|
||||
TunnelSecret: secret,
|
||||
TunnelName: name,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sc := &subcommandContext{
|
||||
c: tt.fields.c,
|
||||
log: tt.fields.log,
|
||||
isUIEnabled: tt.fields.isUIEnabled,
|
||||
fs: tt.fields.fs,
|
||||
tunnelstoreClient: tt.fields.tunnelstoreClient,
|
||||
userCredential: tt.fields.userCredential,
|
||||
}
|
||||
got, err := sc.findCredentials(tt.args.tunnelID)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("subcommandContext.findCredentials() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("subcommandContext.findCredentials() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type deleteMockTunnelStore struct {
|
||||
tunnelstore.Client
|
||||
mockTunnels map[uuid.UUID]mockTunnelBehaviour
|
||||
deletedTunnelIDs []uuid.UUID
|
||||
}
|
||||
|
||||
type mockTunnelBehaviour struct {
|
||||
tunnel tunnelstore.Tunnel
|
||||
deleteErr error
|
||||
cleanupErr error
|
||||
}
|
||||
|
||||
func newDeleteMockTunnelStore(tunnels ...mockTunnelBehaviour) *deleteMockTunnelStore {
|
||||
mockTunnels := make(map[uuid.UUID]mockTunnelBehaviour)
|
||||
for _, tunnel := range tunnels {
|
||||
mockTunnels[tunnel.tunnel.ID] = tunnel
|
||||
}
|
||||
return &deleteMockTunnelStore{
|
||||
mockTunnels: mockTunnels,
|
||||
deletedTunnelIDs: make([]uuid.UUID, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *deleteMockTunnelStore) GetTunnel(tunnelID uuid.UUID) (*tunnelstore.Tunnel, error) {
|
||||
tunnel, ok := d.mockTunnels[tunnelID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
|
||||
}
|
||||
return &tunnel.tunnel, nil
|
||||
}
|
||||
|
||||
func (d *deleteMockTunnelStore) DeleteTunnel(tunnelID uuid.UUID) error {
|
||||
tunnel, ok := d.mockTunnels[tunnelID]
|
||||
if !ok {
|
||||
return fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
|
||||
}
|
||||
|
||||
if tunnel.deleteErr != nil {
|
||||
return tunnel.deleteErr
|
||||
}
|
||||
|
||||
d.deletedTunnelIDs = append(d.deletedTunnelIDs, tunnelID)
|
||||
tunnel.tunnel.DeletedAt = time.Now()
|
||||
delete(d.mockTunnels, tunnelID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *deleteMockTunnelStore) CleanupConnections(tunnelID uuid.UUID) error {
|
||||
tunnel, ok := d.mockTunnels[tunnelID]
|
||||
if !ok {
|
||||
return fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
|
||||
}
|
||||
return tunnel.cleanupErr
|
||||
}
|
||||
|
||||
func Test_subcommandContext_Delete(t *testing.T) {
|
||||
type fields struct {
|
||||
c *cli.Context
|
||||
log *zerolog.Logger
|
||||
isUIEnabled bool
|
||||
fs fileSystem
|
||||
tunnelstoreClient *deleteMockTunnelStore
|
||||
userCredential *userCredential
|
||||
}
|
||||
type args struct {
|
||||
tunnelIDs []uuid.UUID
|
||||
}
|
||||
newCertPath := "new_cert.json"
|
||||
tunnelID1 := uuid.MustParse("df5ed608-b8b4-4109-89f3-9f2cf199df64")
|
||||
tunnelID2 := uuid.MustParse("af5ed608-b8b4-4109-89f3-9f2cf199df64")
|
||||
log := zerolog.Nop()
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want []uuid.UUID
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "clean up continues if credentials are not found",
|
||||
fields: fields{
|
||||
log: &log,
|
||||
fs: mockFileSystem{
|
||||
rf: func(filePath string) ([]byte, error) {
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
vfp: func(string) bool { return true },
|
||||
},
|
||||
c: func() *cli.Context {
|
||||
flagSet := flag.NewFlagSet("test0", flag.PanicOnError)
|
||||
flagSet.String(CredFileFlag, newCertPath, "")
|
||||
c := cli.NewContext(cli.NewApp(), flagSet, nil)
|
||||
_ = c.Set(CredFileFlag, newCertPath)
|
||||
return c
|
||||
}(),
|
||||
tunnelstoreClient: newDeleteMockTunnelStore(
|
||||
mockTunnelBehaviour{
|
||||
tunnel: tunnelstore.Tunnel{ID: tunnelID1},
|
||||
},
|
||||
mockTunnelBehaviour{
|
||||
tunnel: tunnelstore.Tunnel{ID: tunnelID2},
|
||||
},
|
||||
),
|
||||
},
|
||||
|
||||
args: args{
|
||||
tunnelIDs: []uuid.UUID{tunnelID1, tunnelID2},
|
||||
},
|
||||
want: []uuid.UUID{tunnelID1, tunnelID2},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sc := &subcommandContext{
|
||||
c: tt.fields.c,
|
||||
log: tt.fields.log,
|
||||
isUIEnabled: tt.fields.isUIEnabled,
|
||||
fs: tt.fields.fs,
|
||||
tunnelstoreClient: tt.fields.tunnelstoreClient,
|
||||
userCredential: tt.fields.userCredential,
|
||||
}
|
||||
err := sc.delete(tt.args.tunnelIDs)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("subcommandContext.findCredentials() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
got := tt.fields.tunnelstoreClient.deletedTunnelIDs
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("subcommandContext.findCredentials() = %v, want %v", got, tt.want)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
@@ -15,20 +16,23 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
"golang.org/x/net/idna"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/certutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
)
|
||||
|
||||
const (
|
||||
credFileFlagAlias = "cred-file"
|
||||
allSortByOptions = "name, id, createdAt, deletedAt, numConnections"
|
||||
CredFileFlagAlias = "cred-file"
|
||||
CredFileFlag = "credentials-file"
|
||||
|
||||
LogFieldTunnelID = "tunnelID"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -37,36 +41,88 @@ var (
|
||||
Aliases: []string{"d"},
|
||||
Usage: "Include deleted tunnels in the list",
|
||||
}
|
||||
outputFormatFlag = &cli.StringFlag{
|
||||
listNameFlag = &cli.StringFlag{
|
||||
Name: "name",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "List tunnels with the given `NAME`",
|
||||
}
|
||||
listExistedAtFlag = &cli.TimestampFlag{
|
||||
Name: "when",
|
||||
Aliases: []string{"w"},
|
||||
Usage: "List tunnels that are active at the given `TIME` in RFC3339 format",
|
||||
Layout: tunnelstore.TimeLayout,
|
||||
DefaultText: fmt.Sprintf("current time, %s", time.Now().Format(tunnelstore.TimeLayout)),
|
||||
}
|
||||
listIDFlag = &cli.StringFlag{
|
||||
Name: "id",
|
||||
Aliases: []string{"i"},
|
||||
Usage: "List tunnel by `ID`",
|
||||
}
|
||||
showRecentlyDisconnected = &cli.BoolFlag{
|
||||
Name: "show-recently-disconnected",
|
||||
Aliases: []string{"rd"},
|
||||
Usage: "Include connections that have recently disconnected in the list",
|
||||
}
|
||||
outputFormatFlag = altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "output",
|
||||
Aliases: []string{"o"},
|
||||
Usage: "Render output using given `FORMAT`. Valid options are 'json' or 'yaml'",
|
||||
})
|
||||
sortByFlag = &cli.StringFlag{
|
||||
Name: "sort-by",
|
||||
Value: "name",
|
||||
Usage: fmt.Sprintf("Sorts the list of tunnels by the given field. Valid options are {%s}", allSortByOptions),
|
||||
EnvVars: []string{"TUNNEL_LIST_SORT_BY"},
|
||||
}
|
||||
forceFlag = &cli.BoolFlag{
|
||||
invertSortFlag = &cli.BoolFlag{
|
||||
Name: "invert-sort",
|
||||
Usage: "Inverts the sort order of the tunnel list.",
|
||||
EnvVars: []string{"TUNNEL_LIST_INVERT_SORT"},
|
||||
}
|
||||
forceFlag = altsrc.NewBoolFlag(&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.",
|
||||
})
|
||||
credentialsFileFlag = altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: CredFileFlag,
|
||||
Aliases: []string{CredFileFlagAlias},
|
||||
Usage: "Filepath at which to read/write the tunnel credentials",
|
||||
EnvVars: []string{"TUNNEL_CRED_FILE"},
|
||||
})
|
||||
forceDeleteFlag = &cli.BoolFlag{
|
||||
Name: "force",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "Allows you to delete a tunnel, even if it has active connections.",
|
||||
EnvVars: []string{"TUNNEL_RUN_FORCE_OVERWRITE"},
|
||||
}
|
||||
credentialsFileFlag = &cli.StringFlag{
|
||||
Name: "credentials-file",
|
||||
Aliases: []string{credFileFlagAlias},
|
||||
Usage: "File path of tunnel credentials",
|
||||
}
|
||||
selectProtocolFlag = altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "protocol",
|
||||
Value: "h2mux",
|
||||
Aliases: []string{"p"},
|
||||
Usage: fmt.Sprintf("Protocol implementation to connect with Cloudflare's edge network. %s", connection.AvailableProtocolFlagMessage),
|
||||
EnvVars: []string{"TUNNEL_TRANSPORT_PROTOCOL"},
|
||||
Hidden: true,
|
||||
})
|
||||
)
|
||||
|
||||
const hideSubcommands = true
|
||||
|
||||
func buildCreateCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "create",
|
||||
Action: cliutil.ErrorHandler(createTunnel),
|
||||
Action: cliutil.ErrorHandler(createCommand),
|
||||
Usage: "Create a new tunnel with given name",
|
||||
ArgsUsage: "TUNNEL-NAME",
|
||||
Hidden: hideSubcommands,
|
||||
Flags: []cli.Flag{outputFormatFlag},
|
||||
UsageText: "cloudflared tunnel [tunnel command options] create [subcommand options] NAME",
|
||||
Description: `Creates a tunnel, registers it with Cloudflare edge and generates credential file used to run this tunnel.
|
||||
Use "cloudflared tunnel route" subcommand to map a DNS name to this tunnel and "cloudflared tunnel run" to start the connection.
|
||||
|
||||
For example, to create a tunnel named 'my-tunnel' run:
|
||||
|
||||
$ cloudflared tunnel create my-tunnel`,
|
||||
Flags: []cli.Flag{outputFormatFlag, credentialsFileFlag},
|
||||
CustomHelpTemplate: commandHelpTemplate(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,169 +133,128 @@ func generateTunnelSecret() ([]byte, error) {
|
||||
return randomBytes, err
|
||||
}
|
||||
|
||||
func createTunnel(c *cli.Context) error {
|
||||
func createCommand(c *cli.Context) error {
|
||||
sc, err := newSubcommandContext(c)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
cert, originCertPath, err := getOriginCertFromContext(c, 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
|
||||
_, err = sc.create(name, c.String(CredFileFlag))
|
||||
return errors.Wrap(err, "failed to create tunnel")
|
||||
}
|
||||
|
||||
func tunnelFilePath(tunnelID, directory string) (string, error) {
|
||||
func tunnelFilePath(tunnelID uuid.UUID, directory string) (string, error) {
|
||||
fileName := fmt.Sprintf("%v.json", tunnelID)
|
||||
filePath := filepath.Clean(fmt.Sprintf("%s/%s", directory, fileName))
|
||||
return homedir.Expand(filePath)
|
||||
}
|
||||
|
||||
func writeTunnelCredentials(tunnelID, accountID, originCertPath string, tunnelSecret []byte, logger logger.Service) error {
|
||||
originCertDir := filepath.Dir(originCertPath)
|
||||
filePath, err := tunnelFilePath(tunnelID, originCertDir)
|
||||
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(c *cli.Context, tunnelID string, logger logger.Service) (*pogs.TunnelAuth, error) {
|
||||
filePath, err := tunnelCredentialsPath(c, tunnelID, logger)
|
||||
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 tunnelCredentialsPath(c *cli.Context, tunnelID string, logger logger.Service) (string, error) {
|
||||
if filePath := c.String("credentials-file"); filePath != "" {
|
||||
if validFilePath(filePath) {
|
||||
return filePath, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to look for tunnel credentials in the origin cert directory
|
||||
if originCertPath, err := findOriginCert(c, logger); err == nil {
|
||||
// If an `outputFile` is given, write the credentials there.
|
||||
// Otherwise, write it to the same directory as the originCert,
|
||||
// with the filename `<tunnel id>.json`.
|
||||
func writeTunnelCredentials(
|
||||
originCertPath, outputFile string,
|
||||
credentials *connection.Credentials,
|
||||
) (filePath string, err error) {
|
||||
filePath = outputFile
|
||||
if outputFile == "" {
|
||||
originCertDir := filepath.Dir(originCertPath)
|
||||
if filePath, err := tunnelFilePath(tunnelID, originCertDir); err == nil {
|
||||
if validFilePath(filePath) {
|
||||
return filePath, nil
|
||||
}
|
||||
}
|
||||
filePath, err = tunnelFilePath(credentials.TunnelID, originCertDir)
|
||||
}
|
||||
|
||||
// Last resort look under default config directories
|
||||
for _, configDir := range config.DefaultConfigDirs {
|
||||
if filePath, err := tunnelFilePath(tunnelID, configDir); err == nil {
|
||||
if validFilePath(filePath) {
|
||||
return filePath, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("Tunnel credentials file not found")
|
||||
}
|
||||
|
||||
func validFilePath(path string) bool {
|
||||
fileStat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
return "", err
|
||||
}
|
||||
return !fileStat.IsDir()
|
||||
// Write the name and ID to the file too
|
||||
body, err := json.Marshal(credentials)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "Unable to marshal tunnel credentials to JSON")
|
||||
}
|
||||
return filePath, ioutil.WriteFile(filePath, body, 400)
|
||||
}
|
||||
|
||||
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},
|
||||
Name: "list",
|
||||
Action: cliutil.ErrorHandler(listCommand),
|
||||
Usage: "List existing tunnels",
|
||||
UsageText: "cloudflared tunnel [tunnel command options] list [subcommand options]",
|
||||
Description: "cloudflared tunnel list will display all active tunnels, their created time and associated connections. Use -d flag to include deleted tunnels. See the list of options to filter the list",
|
||||
Flags: []cli.Flag{
|
||||
outputFormatFlag,
|
||||
showDeletedFlag,
|
||||
listNameFlag,
|
||||
listExistedAtFlag,
|
||||
listIDFlag,
|
||||
showRecentlyDisconnected,
|
||||
sortByFlag,
|
||||
invertSortFlag,
|
||||
},
|
||||
CustomHelpTemplate: commandHelpTemplate(),
|
||||
}
|
||||
}
|
||||
|
||||
func listTunnels(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
cert, _, err := getOriginCertFromContext(c, logger)
|
||||
func listCommand(c *cli.Context) error {
|
||||
sc, err := newSubcommandContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := newTunnelstoreClient(c, cert, logger)
|
||||
|
||||
allTunnels, err := client.ListTunnels()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error listing tunnels")
|
||||
filter := tunnelstore.NewFilter()
|
||||
if !c.Bool("show-deleted") {
|
||||
filter.NoDeleted()
|
||||
}
|
||||
if name := c.String("name"); name != "" {
|
||||
filter.ByName(name)
|
||||
}
|
||||
if existedAt := c.Timestamp("time"); existedAt != nil {
|
||||
filter.ByExistedAt(*existedAt)
|
||||
}
|
||||
if id := c.String("id"); id != "" {
|
||||
tunnelID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "%s is not a valid tunnel ID", id)
|
||||
}
|
||||
filter.ByTunnelID(tunnelID)
|
||||
}
|
||||
|
||||
var tunnels []tunnelstore.Tunnel
|
||||
if c.Bool("show-deleted") {
|
||||
tunnels = allTunnels
|
||||
} else {
|
||||
for _, tunnel := range allTunnels {
|
||||
if tunnel.DeletedAt.IsZero() {
|
||||
tunnels = append(tunnels, tunnel)
|
||||
tunnels, err := sc.list(filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Sort the tunnels
|
||||
sortBy := c.String("sort-by")
|
||||
invalidSortField := false
|
||||
sort.Slice(tunnels, func(i, j int) bool {
|
||||
cmp := func() bool {
|
||||
switch sortBy {
|
||||
case "name":
|
||||
return tunnels[i].Name < tunnels[j].Name
|
||||
case "id":
|
||||
return tunnels[i].ID.String() < tunnels[j].ID.String()
|
||||
case "createdAt":
|
||||
return tunnels[i].CreatedAt.Unix() < tunnels[j].CreatedAt.Unix()
|
||||
case "deletedAt":
|
||||
return tunnels[i].DeletedAt.Unix() < tunnels[j].DeletedAt.Unix()
|
||||
case "numConnections":
|
||||
return len(tunnels[i].Connections) < len(tunnels[j].Connections)
|
||||
default:
|
||||
invalidSortField = true
|
||||
return tunnels[i].Name < tunnels[j].Name
|
||||
}
|
||||
}()
|
||||
if c.Bool("invert-sort") {
|
||||
return !cmp
|
||||
}
|
||||
return cmp
|
||||
})
|
||||
if invalidSortField {
|
||||
sc.log.Error().Msgf("%s is not a valid sort field. Valid sort fields are %s. Defaulting to 'name'.", sortBy, allSortByOptions)
|
||||
}
|
||||
|
||||
if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" {
|
||||
@@ -247,15 +262,14 @@ func listTunnels(c *cli.Context) error {
|
||||
}
|
||||
|
||||
if len(tunnels) > 0 {
|
||||
fmtAndPrintTunnelList(tunnels)
|
||||
formatAndPrintTunnelList(tunnels, c.Bool("show-recently-disconnected"))
|
||||
} else {
|
||||
fmt.Println("You have no tunnels, use 'cloudflared tunnel create' to define a new tunnel")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fmtAndPrintTunnelList(tunnels []tunnelstore.Tunnel) {
|
||||
func formatAndPrintTunnelList(tunnels []*tunnelstore.Tunnel, showRecentlyDisconnected bool) {
|
||||
const (
|
||||
minWidth = 0
|
||||
tabWidth = 8
|
||||
@@ -265,26 +279,32 @@ func fmtAndPrintTunnelList(tunnels []tunnelstore.Tunnel) {
|
||||
)
|
||||
|
||||
writer := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, padChar, flags)
|
||||
defer writer.Flush()
|
||||
|
||||
// Print column headers with tabbed columns
|
||||
fmt.Fprintln(writer, "ID\tNAME\tCREATED\tCONNECTIONS\t")
|
||||
_, _ = fmt.Fprintln(writer, "ID\tNAME\tCREATED\tCONNECTIONS\t")
|
||||
|
||||
// Loop through tunnels, create formatted string for each, and print using tabwriter
|
||||
for _, t := range tunnels {
|
||||
formattedStr := fmt.Sprintf("%s\t%s\t%s\t%s\t", t.ID, t.Name, t.CreatedAt.Format(time.RFC3339), fmtConnections(t.Connections))
|
||||
fmt.Fprintln(writer, formattedStr)
|
||||
formattedStr := fmt.Sprintf(
|
||||
"%s\t%s\t%s\t%s\t",
|
||||
t.ID,
|
||||
t.Name,
|
||||
t.CreatedAt.Format(time.RFC3339),
|
||||
fmtConnections(t.Connections, showRecentlyDisconnected),
|
||||
)
|
||||
_, _ = fmt.Fprintln(writer, formattedStr)
|
||||
}
|
||||
|
||||
// Write data buffered in tabwriter to output
|
||||
writer.Flush()
|
||||
}
|
||||
|
||||
func fmtConnections(connections []tunnelstore.Connection) string {
|
||||
func fmtConnections(connections []tunnelstore.Connection, showRecentlyDisconnected bool) string {
|
||||
|
||||
// Count connections per colo
|
||||
numConnsPerColo := make(map[string]uint, len(connections))
|
||||
for _, connection := range connections {
|
||||
numConnsPerColo[connection.ColoName]++
|
||||
if !connection.IsPendingReconnect || showRecentlyDisconnected {
|
||||
numConnsPerColo[connection.ColoName]++
|
||||
}
|
||||
}
|
||||
|
||||
// Get sorted list of colos
|
||||
@@ -304,47 +324,32 @@ func fmtConnections(connections []tunnelstore.Connection) string {
|
||||
|
||||
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,
|
||||
Flags: []cli.Flag{credentialsFileFlag},
|
||||
Name: "delete",
|
||||
Action: cliutil.ErrorHandler(deleteCommand),
|
||||
Usage: "Delete existing tunnel by UUID or name",
|
||||
UsageText: "cloudflared tunnel [tunnel command options] delete [subcommand options] TUNNEL",
|
||||
Description: "cloudflared tunnel delete will delete tunnels with the given tunnel UUIDs or names. A tunnel cannot be deleted if it has active connections. To delete the tunnel unconditionally, use -f flag.",
|
||||
Flags: []cli.Flag{credentialsFileFlag, forceDeleteFlag},
|
||||
CustomHelpTemplate: commandHelpTemplate(),
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
cert, _, err := getOriginCertFromContext(c, logger)
|
||||
func deleteCommand(c *cli.Context) error {
|
||||
sc, err := newSubcommandContext(c)
|
||||
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)
|
||||
if c.NArg() < 1 {
|
||||
return cliutil.UsageError(`"cloudflared tunnel delete" requires at least 1 argument, the ID or name of the tunnel to delete.`)
|
||||
}
|
||||
|
||||
tunnelCredentialsPath, err := tunnelCredentialsPath(c, id, logger)
|
||||
tunnelIDs, err := sc.findIDs(c.Args().Slice())
|
||||
if err != nil {
|
||||
logger.Infof("Cannot locate tunnel credentials to delete, error: %v. Please delete the file manually", err)
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
if err = os.Remove(tunnelCredentialsPath); err != nil {
|
||||
logger.Infof("Cannot delete tunnel credentials, error: %v. Please delete the file manually", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return sc.delete(tunnelIDs)
|
||||
}
|
||||
|
||||
func renderOutput(format string, v interface{}) error {
|
||||
@@ -360,99 +365,244 @@ func renderOutput(format string, v interface{}) error {
|
||||
}
|
||||
}
|
||||
|
||||
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(c *cli.Context, logger logger.Service) (cert *certutil.OriginCert, originCertPath string, err error) {
|
||||
originCertPath, err = findOriginCert(c, logger)
|
||||
if err != nil {
|
||||
return nil, "", errors.Wrap(err, "Error locating origin cert")
|
||||
}
|
||||
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, originCertPath, nil
|
||||
}
|
||||
|
||||
func buildRunCommand() *cli.Command {
|
||||
flags := []cli.Flag{
|
||||
forceFlag,
|
||||
credentialsFileFlag,
|
||||
selectProtocolFlag,
|
||||
}
|
||||
flags = append(flags, configureProxyFlags(false)...)
|
||||
return &cli.Command{
|
||||
Name: "run",
|
||||
Action: cliutil.ErrorHandler(runTunnel),
|
||||
Action: cliutil.ErrorHandler(runCommand),
|
||||
Before: SetFlagsFromConfigFile,
|
||||
Usage: "Proxy a local web server by running the given tunnel",
|
||||
ArgsUsage: "TUNNEL-ID",
|
||||
Hidden: hideSubcommands,
|
||||
Flags: []cli.Flag{forceFlag, credentialsFileFlag},
|
||||
UsageText: "cloudflared tunnel [tunnel command options] run [subcommand options] [TUNNEL]",
|
||||
Description: `Runs the tunnel identified by name or UUUD, creating highly available connections
|
||||
between your server and the Cloudflare edge. You can provide name or UUID of tunnel to run either as the
|
||||
last command line argument or in the configuration file using "tunnel: TUNNEL".
|
||||
|
||||
This command requires the tunnel credentials file created when "cloudflared tunnel create" was run,
|
||||
however it does not need access to cert.pem from "cloudflared login" if you identify the tunnel by UUID.
|
||||
If you experience other problems running the tunnel, "cloudflared tunnel cleanup" may help by removing
|
||||
any old connection records.
|
||||
`,
|
||||
Flags: flags,
|
||||
CustomHelpTemplate: commandHelpTemplate(),
|
||||
}
|
||||
}
|
||||
|
||||
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.`)
|
||||
func runCommand(c *cli.Context) error {
|
||||
sc, err := newSubcommandContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id := c.Args().First()
|
||||
tunnelID, err := uuid.Parse(id)
|
||||
|
||||
if c.NArg() > 1 {
|
||||
return cliutil.UsageError(`"cloudflared tunnel run" accepts only one argument, the ID or name of the tunnel to run.`)
|
||||
}
|
||||
tunnelRef := c.Args().First()
|
||||
if tunnelRef == "" {
|
||||
// see if tunnel id was in the config file
|
||||
tunnelRef = config.GetConfiguration().TunnelID
|
||||
if tunnelRef == "" {
|
||||
return cliutil.UsageError(`"cloudflared tunnel run" requires the ID or name of the tunnel to run as the last command line argument or in the configuration file.`)
|
||||
}
|
||||
}
|
||||
|
||||
return runNamedTunnel(sc, tunnelRef)
|
||||
}
|
||||
|
||||
func runNamedTunnel(sc *subcommandContext, tunnelRef string) error {
|
||||
tunnelID, err := sc.findID(tunnelRef)
|
||||
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")
|
||||
}
|
||||
sc.log.Info().Str(LogFieldTunnelID, tunnelID.String()).Msg("Starting tunnel")
|
||||
|
||||
credentials, err := readTunnelCredentials(c, id, logger)
|
||||
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})
|
||||
return sc.run(tunnelID)
|
||||
}
|
||||
|
||||
func buildCleanupCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "cleanup",
|
||||
Action: cliutil.ErrorHandler(cleanupConnections),
|
||||
Usage: "Cleanup connections for the tunnel with given IDs",
|
||||
ArgsUsage: "TUNNEL-IDS",
|
||||
Hidden: hideSubcommands,
|
||||
Name: "cleanup",
|
||||
Action: cliutil.ErrorHandler(cleanupCommand),
|
||||
Usage: "Cleanup tunnel connections",
|
||||
UsageText: "cloudflared tunnel [tunnel command options] cleanup [subcommand options] TUNNEL",
|
||||
Description: "Delete connections for tunnels with the given UUIDs or names.",
|
||||
CustomHelpTemplate: commandHelpTemplate(),
|
||||
}
|
||||
}
|
||||
|
||||
func cleanupConnections(c *cli.Context) error {
|
||||
func cleanupCommand(c *cli.Context) error {
|
||||
if c.NArg() < 1 {
|
||||
return cliutil.UsageError(`"cloudflared tunnel cleanup" requires at least 1 argument, the IDs of the tunnels to cleanup connections.`)
|
||||
}
|
||||
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
cert, _, err := getOriginCertFromContext(c, logger)
|
||||
sc, err := newSubcommandContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := newTunnelstoreClient(c, cert, logger)
|
||||
|
||||
for i := 0; i < c.NArg(); i++ {
|
||||
id := c.Args().Get(i)
|
||||
logger.Infof("Cleanup connection for tunnel %s", id)
|
||||
if err := client.CleanupConnections(id); err != nil {
|
||||
logger.Errorf("Error cleaning up connections for tunnel %s, error :%v", id, err)
|
||||
}
|
||||
tunnelIDs, err := sc.findIDs(c.Args().Slice())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sc.cleanupConnections(tunnelIDs)
|
||||
}
|
||||
|
||||
func buildRouteCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "route",
|
||||
Action: cliutil.ErrorHandler(routeCommand),
|
||||
Usage: "Define what hostname or load balancer can route to this tunnel",
|
||||
UsageText: "cloudflared tunnel [tunnel command options] route [subcommand options] dns|lb TUNNEL HOSTNAME [LB-POOL]",
|
||||
Description: `The route defines what hostname or load balancer will proxy requests to this tunnel.
|
||||
|
||||
To route a hostname by creating a CNAME to tunnel's address:
|
||||
cloudflared tunnel route dns <tunnel ID> <hostname>
|
||||
To use this tunnel as a load balancer origin, creating pool and load balancer if necessary:
|
||||
cloudflared tunnel route lb <tunnel ID> <load balancer name> <load balancer pool>`,
|
||||
CustomHelpTemplate: commandHelpTemplate(),
|
||||
Subcommands: []*cli.Command{
|
||||
buildRouteIPSubcommand(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func dnsRouteFromArg(c *cli.Context) (tunnelstore.Route, error) {
|
||||
const (
|
||||
userHostnameIndex = 2
|
||||
expectedNArgs = 3
|
||||
)
|
||||
if c.NArg() != expectedNArgs {
|
||||
return nil, cliutil.UsageError("Expected %d arguments, got %d", expectedNArgs, c.NArg())
|
||||
}
|
||||
userHostname := c.Args().Get(userHostnameIndex)
|
||||
if userHostname == "" {
|
||||
return nil, cliutil.UsageError("The third argument should be the hostname")
|
||||
} else if !validateHostname(userHostname, true) {
|
||||
return nil, errors.Errorf("%s is not a valid hostname", userHostname)
|
||||
}
|
||||
return tunnelstore.NewDNSRoute(userHostname), nil
|
||||
}
|
||||
|
||||
func lbRouteFromArg(c *cli.Context) (tunnelstore.Route, error) {
|
||||
const (
|
||||
lbNameIndex = 2
|
||||
lbPoolIndex = 3
|
||||
expectedNArgs = 4
|
||||
)
|
||||
if c.NArg() != expectedNArgs {
|
||||
return nil, cliutil.UsageError("Expected %d arguments, got %d", expectedNArgs, c.NArg())
|
||||
}
|
||||
lbName := c.Args().Get(lbNameIndex)
|
||||
if lbName == "" {
|
||||
return nil, cliutil.UsageError("The third argument should be the load balancer name")
|
||||
} else if !validateHostname(lbName, true) {
|
||||
return nil, errors.Errorf("%s is not a valid load balancer name", lbName)
|
||||
}
|
||||
|
||||
lbPool := c.Args().Get(lbPoolIndex)
|
||||
if lbPool == "" {
|
||||
return nil, cliutil.UsageError("The fourth argument should be the pool name")
|
||||
} else if !validateName(lbPool, false) {
|
||||
return nil, errors.Errorf("%s is not a valid pool name", lbPool)
|
||||
}
|
||||
|
||||
return tunnelstore.NewLBRoute(lbName, lbPool), nil
|
||||
}
|
||||
|
||||
var nameRegex = regexp.MustCompile("^[_a-zA-Z0-9][-_.a-zA-Z0-9]*$")
|
||||
var hostNameRegex = regexp.MustCompile("^[*_a-zA-Z0-9][-_.a-zA-Z0-9]*$")
|
||||
|
||||
func validateName(s string, allowWildcardSubdomain bool) bool {
|
||||
if allowWildcardSubdomain {
|
||||
return hostNameRegex.MatchString(s)
|
||||
}
|
||||
return nameRegex.MatchString(s)
|
||||
}
|
||||
|
||||
func validateHostname(s string, allowWildcardSubdomain bool) bool {
|
||||
// Slightly stricter than PunyCodeProfile
|
||||
idnaProfile := idna.New(
|
||||
idna.ValidateLabels(true),
|
||||
idna.VerifyDNSLength(true))
|
||||
|
||||
puny, err := idnaProfile.ToASCII(s)
|
||||
return err == nil && validateName(puny, allowWildcardSubdomain)
|
||||
}
|
||||
|
||||
func routeCommand(c *cli.Context) error {
|
||||
if c.NArg() < 2 {
|
||||
return cliutil.UsageError(`"cloudflared tunnel route" requires the first argument to be the route type(dns or lb), followed by the ID or name of the tunnel`)
|
||||
}
|
||||
sc, err := newSubcommandContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
const tunnelIDIndex = 1
|
||||
|
||||
routeType := c.Args().First()
|
||||
var route tunnelstore.Route
|
||||
var tunnelID uuid.UUID
|
||||
switch routeType {
|
||||
case "dns":
|
||||
tunnelID, err = sc.findID(c.Args().Get(tunnelIDIndex))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
route, err = dnsRouteFromArg(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "lb":
|
||||
tunnelID, err = sc.findID(c.Args().Get(tunnelIDIndex))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
route, err = lbRouteFromArg(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return cliutil.UsageError("%s is not a recognized route type. Supported route types are dns and lb", routeType)
|
||||
}
|
||||
|
||||
res, err := sc.route(tunnelID, route)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sc.log.Info().Str(LogFieldTunnelID, tunnelID.String()).Msg(res.SuccessSummary())
|
||||
return nil
|
||||
}
|
||||
|
||||
func commandHelpTemplate() string {
|
||||
var parentFlagsHelp string
|
||||
for _, f := range configureCloudflaredFlags(false) {
|
||||
parentFlagsHelp += fmt.Sprintf(" %s\n\t", f)
|
||||
}
|
||||
for _, f := range configureLoggingFlags(false) {
|
||||
parentFlagsHelp += fmt.Sprintf(" %s\n\t", f)
|
||||
}
|
||||
const template = `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
{{.UsageText}}
|
||||
|
||||
DESCRIPTION:
|
||||
{{.Description}}
|
||||
|
||||
TUNNEL COMMAND OPTIONS:
|
||||
%s
|
||||
SUBCOMMAND OPTIONS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}
|
||||
`
|
||||
return fmt.Sprintf(template, parentFlagsHelp)
|
||||
}
|
||||
|
||||
@@ -6,9 +6,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -40,6 +39,19 @@ func Test_fmtConnections(t *testing.T) {
|
||||
},
|
||||
want: "1xDFW",
|
||||
},
|
||||
{
|
||||
name: "with a pending reconnect",
|
||||
args: args{
|
||||
connections: []tunnelstore.Connection{
|
||||
{
|
||||
ColoName: "DFW",
|
||||
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
|
||||
IsPendingReconnect: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "many colos",
|
||||
args: args{
|
||||
@@ -67,7 +79,7 @@ func Test_fmtConnections(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := fmtConnections(tt.args.connections); got != tt.want {
|
||||
if got := fmtConnections(tt.args.connections, false); got != tt.want {
|
||||
t.Errorf("fmtConnections() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
@@ -75,11 +87,93 @@ func Test_fmtConnections(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTunnelfilePath(t *testing.T) {
|
||||
tunnelID, err := uuid.Parse("f48d8918-bc23-4647-9d48-082c5b76de65")
|
||||
assert.NoError(t, err)
|
||||
originCertDir := filepath.Dir("~/.cloudflared/cert.pem")
|
||||
actual, err := tunnelFilePath("tunnel", originCertDir)
|
||||
actual, err := tunnelFilePath(tunnelID, originCertDir)
|
||||
assert.NoError(t, err)
|
||||
homeDir, err := homedir.Dir()
|
||||
assert.NoError(t, err)
|
||||
expected := fmt.Sprintf("%s/.cloudflared/tunnel.json", homeDir)
|
||||
expected := fmt.Sprintf("%s/.cloudflared/%v.json", homeDir, tunnelID)
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestValidateName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want bool
|
||||
}{
|
||||
{name: "", want: false},
|
||||
{name: "-", want: false},
|
||||
{name: ".", want: false},
|
||||
{name: "a b", want: false},
|
||||
{name: "a+b", want: false},
|
||||
{name: "-ab", want: false},
|
||||
|
||||
{name: "ab", want: true},
|
||||
{name: "ab-c", want: true},
|
||||
{name: "abc.def", want: true},
|
||||
{name: "_ab_c.-d-ef", want: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := validateName(tt.name, false); got != tt.want {
|
||||
t.Errorf("validateName() = %v, want %v", got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_validateHostname(t *testing.T) {
|
||||
type args struct {
|
||||
s string
|
||||
allowWildcardSubdomain bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "Normal",
|
||||
args: args{
|
||||
s: "example.com",
|
||||
allowWildcardSubdomain: true,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "wildcard subdomain for TUN-358",
|
||||
args: args{
|
||||
s: "*.ehrig.io",
|
||||
allowWildcardSubdomain: true,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Misplaced wildcard",
|
||||
args: args{
|
||||
s: "subdomain.*.ehrig.io",
|
||||
allowWildcardSubdomain: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Invalid domain",
|
||||
args: args{
|
||||
s: "..",
|
||||
allowWildcardSubdomain: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Invalid domain",
|
||||
args: args{
|
||||
s: "..",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := validateHostname(tt.args.s, tt.args.allowWildcardSubdomain); got != tt.want {
|
||||
t.Errorf("validateHostname() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/teamnet"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func buildRouteIPSubcommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "ip",
|
||||
Category: "Tunnel",
|
||||
Usage: "Configure and query private routes",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] route COMMAND [arguments...]",
|
||||
Hidden: true,
|
||||
Description: `cloudflared can provision private routes from your private IP space to origins
|
||||
in your corporate network. Users enrolled in your Cloudflare for Teams organization can reach
|
||||
those routes through the Cloudflare Warp client. You can also build rules to determine who
|
||||
can reach certain routes.
|
||||
`,
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "add",
|
||||
Action: cliutil.ErrorHandler(addRouteCommand),
|
||||
Usage: "Add a new Teamnet route to the table",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] route ip add [CIDR] [TUNNEL] [COMMENT?]",
|
||||
Description: `Adds a private route to a CIDR in your private IP space. Requests will
|
||||
be sent through the Cloudflare Warp client running on a user's machine, proxied
|
||||
through the specified tunnel, and reach an IP in the given CIDR.`,
|
||||
},
|
||||
{
|
||||
Name: "show",
|
||||
Action: cliutil.ErrorHandler(showRoutesCommand),
|
||||
Usage: "Show the routing table",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] route ip show [flags]",
|
||||
Description: `Shows your organization's private route table. You can use flags to filter the results.`,
|
||||
Flags: teamnet.FilterFlags,
|
||||
},
|
||||
{
|
||||
Name: "delete",
|
||||
Action: cliutil.ErrorHandler(deleteRouteCommand),
|
||||
Usage: "Delete a row from your organization's private routing table",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] route ip delete [CIDR]",
|
||||
Description: `Deletes the row for a given CIDR from your routing table`,
|
||||
},
|
||||
{
|
||||
Name: "get",
|
||||
Action: cliutil.ErrorHandler(getRouteByIPCommand),
|
||||
Usage: "Check which row of the routing table matches a given IP",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] route ip get [IP]",
|
||||
Description: `Checks which row of the routing table will be used to proxy a given IP.
|
||||
This helps check and validate your config.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func showRoutesCommand(c *cli.Context) error {
|
||||
sc, err := newSubcommandContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filter, err := teamnet.NewFromCLI(c)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid config for routing filters")
|
||||
}
|
||||
|
||||
routes, err := sc.listRoutes(filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" {
|
||||
return renderOutput(outputFormat, routes)
|
||||
}
|
||||
|
||||
if len(routes) > 0 {
|
||||
formatAndPrintRouteList(routes)
|
||||
} else {
|
||||
fmt.Println("You have no routes, use 'cloudflared tunnel route ip add' to add a route")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addRouteCommand(c *cli.Context) error {
|
||||
sc, err := newSubcommandContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.NArg() < 2 {
|
||||
return errors.New("You must supply at least 2 arguments, first the network you wish to route (in CIDR form e.g. 1.2.3.4/32) and then the tunnel ID to proxy with")
|
||||
}
|
||||
args := c.Args()
|
||||
_, network, err := net.ParseCIDR(args.Get(0))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Invalid network CIDR")
|
||||
}
|
||||
if network == nil {
|
||||
return errors.New("Invalid network CIDR")
|
||||
}
|
||||
tunnelRef := args.Get(1)
|
||||
tunnelID, err := sc.findID(tunnelRef)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Invalid tunnel")
|
||||
}
|
||||
comment := ""
|
||||
if c.NArg() >= 3 {
|
||||
comment = args.Get(2)
|
||||
}
|
||||
_, err = sc.addRoute(teamnet.NewRoute{
|
||||
Comment: comment,
|
||||
Network: *network,
|
||||
TunnelID: tunnelID,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "API error")
|
||||
}
|
||||
fmt.Printf("Successfully added route for %s over tunnel %s\n", network, tunnelID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteRouteCommand(c *cli.Context) error {
|
||||
sc, err := newSubcommandContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.NArg() != 1 {
|
||||
return errors.New("You must supply exactly one argument, the network whose route you want to delete (in CIDR form e.g. 1.2.3.4/32)")
|
||||
}
|
||||
_, network, err := net.ParseCIDR(c.Args().First())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Invalid network CIDR")
|
||||
}
|
||||
if network == nil {
|
||||
return errors.New("Invalid network CIDR")
|
||||
}
|
||||
if err := sc.deleteRoute(*network); err != nil {
|
||||
return errors.Wrap(err, "API error")
|
||||
}
|
||||
fmt.Printf("Successfully deleted route for %s\n", network)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRouteByIPCommand(c *cli.Context) error {
|
||||
sc, err := newSubcommandContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.NArg() != 1 {
|
||||
return errors.New("You must supply exactly one argument, an IP whose route will be queried (e.g. 1.2.3.4 or 2001:0db8:::7334)")
|
||||
}
|
||||
|
||||
ipInput := c.Args().First()
|
||||
ip := net.ParseIP(ipInput)
|
||||
if ip == nil {
|
||||
return fmt.Errorf("Invalid IP %s", ipInput)
|
||||
}
|
||||
route, err := sc.getRouteByIP(ip)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "API error")
|
||||
}
|
||||
if route.IsZero() {
|
||||
fmt.Printf("No route matches the IP %s\n", ip)
|
||||
} else {
|
||||
formatAndPrintRouteList([]*teamnet.DetailedRoute{&route})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatAndPrintRouteList(routes []*teamnet.DetailedRoute) {
|
||||
const (
|
||||
minWidth = 0
|
||||
tabWidth = 8
|
||||
padding = 1
|
||||
padChar = ' '
|
||||
flags = 0
|
||||
)
|
||||
|
||||
writer := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, padChar, flags)
|
||||
defer writer.Flush()
|
||||
|
||||
// Print column headers with tabbed columns
|
||||
_, _ = fmt.Fprintln(writer, "NETWORK\tCOMMENT\tTUNNEL ID\tTUNNEL NAME\tCREATED\tDELETED\t")
|
||||
|
||||
// Loop through routes, create formatted string for each, and print using tabwriter
|
||||
for _, route := range routes {
|
||||
formattedStr := route.TableString()
|
||||
_, _ = fmt.Fprintln(writer, formattedStr)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type connState struct {
|
||||
location string
|
||||
}
|
||||
|
||||
type uiModel struct {
|
||||
version string
|
||||
edgeURL string
|
||||
metricsURL string
|
||||
localServices []string
|
||||
connections []connState
|
||||
}
|
||||
|
||||
type palette struct {
|
||||
url string
|
||||
connected string
|
||||
defaultText string
|
||||
disconnected string
|
||||
reconnecting string
|
||||
unregistered string
|
||||
}
|
||||
|
||||
func NewUIModel(version, hostname, metricsURL string, ing *ingress.Ingress, haConnections int) *uiModel {
|
||||
localServices := make([]string, len(ing.Rules))
|
||||
for i, rule := range ing.Rules {
|
||||
localServices[i] = rule.Service.String()
|
||||
}
|
||||
return &uiModel{
|
||||
version: version,
|
||||
edgeURL: hostname,
|
||||
metricsURL: metricsURL,
|
||||
localServices: localServices,
|
||||
connections: make([]connState, haConnections),
|
||||
}
|
||||
}
|
||||
|
||||
func (data *uiModel) Launch(
|
||||
ctx context.Context,
|
||||
log, transportLog *zerolog.Logger,
|
||||
) connection.EventSink {
|
||||
// Configure the logger to stream logs into the textview
|
||||
|
||||
// Add TextView as a group to write output to
|
||||
logTextView := NewDynamicColorTextView()
|
||||
// TODO: Format log for UI
|
||||
//log.Add(logTextView, logger.NewUIFormatter(time.RFC3339), logLevels...)
|
||||
//transportLog.Add(logTextView, logger.NewUIFormatter(time.RFC3339), logLevels...)
|
||||
|
||||
// Construct the UI
|
||||
palette := palette{
|
||||
url: "lightblue",
|
||||
connected: "lime",
|
||||
defaultText: "white",
|
||||
disconnected: "red",
|
||||
reconnecting: "orange",
|
||||
unregistered: "orange",
|
||||
}
|
||||
|
||||
app := tview.NewApplication()
|
||||
|
||||
grid := tview.NewGrid().SetGap(1, 0)
|
||||
frame := tview.NewFrame(grid)
|
||||
header := fmt.Sprintf("cloudflared [::b]%s", data.version)
|
||||
|
||||
frame.AddText(header, true, tview.AlignLeft, tcell.ColorWhite)
|
||||
|
||||
// Create table to store connection info and status
|
||||
connTable := tview.NewTable()
|
||||
// SetColumns takes a value for each column, representing the size of the column
|
||||
// Numbers <= 0 represent proportional widths and positive numbers represent absolute widths
|
||||
grid.SetColumns(20, 0)
|
||||
|
||||
// SetRows takes a value for each row, representing the size of the row
|
||||
grid.SetRows(1, 1, len(data.connections), 1, 0)
|
||||
|
||||
// AddItem takes a primitive tview type, row, column, rowSpan, columnSpan, minGridHeight, minGridWidth, and focus
|
||||
grid.AddItem(tview.NewTextView().SetText("Tunnel:"), 0, 0, 1, 1, 0, 0, false)
|
||||
grid.AddItem(tview.NewTextView().SetText("Status:"), 1, 0, 1, 1, 0, 0, false)
|
||||
grid.AddItem(tview.NewTextView().SetText("Connections:"), 2, 0, 1, 1, 0, 0, false)
|
||||
|
||||
grid.AddItem(tview.NewTextView().SetText("Metrics:"), 3, 0, 1, 1, 0, 0, false)
|
||||
|
||||
tunnelHostText := tview.NewTextView().SetText(data.edgeURL)
|
||||
|
||||
grid.AddItem(tunnelHostText, 0, 1, 1, 1, 0, 0, false)
|
||||
status := fmt.Sprintf("[%s]\u2022[%s] Proxying to [%s::b]%s", palette.connected, palette.defaultText, palette.url, strings.Join(data.localServices, ", "))
|
||||
grid.AddItem(NewDynamicColorTextView().SetText(status), 1, 1, 1, 1, 0, 0, false)
|
||||
|
||||
grid.AddItem(connTable, 2, 1, 1, 1, 0, 0, false)
|
||||
|
||||
grid.AddItem(NewDynamicColorTextView().SetText(fmt.Sprintf("Metrics at [%s::b]http://%s/metrics", palette.url, data.metricsURL)), 3, 1, 1, 1, 0, 0, false)
|
||||
|
||||
// Add TextView to stream logs
|
||||
// Logs are displayed in a new grid so a border can be set around them
|
||||
logGrid := tview.NewGrid().SetBorders(true).AddItem(logTextView.SetChangedFunc(handleNewText(app, logTextView)), 0, 0, 5, 2, 0, 0, false)
|
||||
// LogFrame holds the Logs header as well as the grid with the textView for streamed logs
|
||||
logFrame := tview.NewFrame(logGrid).AddText("[::b]Logs:[::-]", true, tview.AlignLeft, tcell.ColorWhite).SetBorders(0, 0, 0, 0, 0, 0)
|
||||
// Footer for log frame
|
||||
logFrame.AddText("[::d]Use Ctrl+C to exit[::-]", false, tview.AlignRight, tcell.ColorWhite)
|
||||
grid.AddItem(logFrame, 4, 0, 5, 2, 0, 0, false)
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
app.Stop()
|
||||
return
|
||||
}()
|
||||
|
||||
go func() {
|
||||
if err := app.SetRoot(frame, true).Run(); err != nil {
|
||||
log.Error().Msgf("Error launching UI: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return connection.EventSinkFunc(func(event connection.Event) {
|
||||
switch event.EventType {
|
||||
case connection.Connected:
|
||||
data.setConnTableCell(event, connTable, palette)
|
||||
case connection.Disconnected, connection.Reconnecting, connection.Unregistering:
|
||||
data.changeConnStatus(event, connTable, log, palette)
|
||||
case connection.SetURL:
|
||||
tunnelHostText.SetText(event.URL)
|
||||
data.edgeURL = event.URL
|
||||
case connection.RegisteringTunnel:
|
||||
if data.edgeURL == "" {
|
||||
tunnelHostText.SetText(fmt.Sprintf("Registering tunnel connection %d...", event.Index))
|
||||
}
|
||||
}
|
||||
app.Draw()
|
||||
})
|
||||
}
|
||||
|
||||
func NewDynamicColorTextView() *tview.TextView {
|
||||
return tview.NewTextView().SetDynamicColors(true)
|
||||
}
|
||||
|
||||
// Re-draws application when new logs are streamed to UI
|
||||
func handleNewText(app *tview.Application, logTextView *tview.TextView) func() {
|
||||
return func() {
|
||||
app.Draw()
|
||||
// SetFocus to enable scrolling in textview
|
||||
app.SetFocus(logTextView)
|
||||
}
|
||||
}
|
||||
|
||||
func (data *uiModel) changeConnStatus(event connection.Event, table *tview.Table, log *zerolog.Logger, palette palette) {
|
||||
index := int(event.Index)
|
||||
// Get connection location and state
|
||||
connState := data.getConnState(index)
|
||||
// Check if connection is already displayed in UI
|
||||
if connState == nil {
|
||||
log.Info().Msg("Connection is not in the UI table")
|
||||
return
|
||||
}
|
||||
|
||||
locationState := event.Location
|
||||
|
||||
if event.EventType == connection.Reconnecting {
|
||||
locationState = "Reconnecting..."
|
||||
}
|
||||
|
||||
connectionNum := index + 1
|
||||
// Get table cell
|
||||
cell := table.GetCell(index, 0)
|
||||
// Change dot color in front of text as well as location state
|
||||
text := newCellText(palette, connectionNum, locationState, event.EventType)
|
||||
cell.SetText(text)
|
||||
}
|
||||
|
||||
// Return connection location and row in UI table
|
||||
func (data *uiModel) getConnState(connID int) *connState {
|
||||
if connID < len(data.connections) {
|
||||
return &data.connections[connID]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (data *uiModel) setConnTableCell(event connection.Event, table *tview.Table, palette palette) {
|
||||
index := int(event.Index)
|
||||
connectionNum := index + 1
|
||||
|
||||
// Update slice to keep track of connection location and state in UI table
|
||||
data.connections[index].location = event.Location
|
||||
|
||||
// Update text in table cell to show disconnected state
|
||||
text := newCellText(palette, connectionNum, event.Location, event.EventType)
|
||||
cell := tview.NewTableCell(text)
|
||||
table.SetCell(index, 0, cell)
|
||||
}
|
||||
|
||||
func newCellText(palette palette, connectionNum int, location string, connectedStatus connection.Status) string {
|
||||
// HA connection indicator formatted as: "• #<CONNECTION_INDEX>: <COLO>",
|
||||
// where the left middle dot's color depends on the status of the connection
|
||||
const connFmtString = "[%s]\u2022[%s] #%d: %s"
|
||||
|
||||
var dotColor string
|
||||
switch connectedStatus {
|
||||
case connection.Connected:
|
||||
dotColor = palette.connected
|
||||
case connection.Disconnected:
|
||||
dotColor = palette.disconnected
|
||||
case connection.Reconnecting:
|
||||
dotColor = palette.reconnecting
|
||||
case connection.Unregistering:
|
||||
dotColor = palette.unregistered
|
||||
}
|
||||
|
||||
return fmt.Sprintf(connFmtString, dotColor, palette.defaultText, connectionNum, location)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package updater
|
||||
|
||||
// Version is the functions needed to perform an update
|
||||
type Version interface {
|
||||
Apply() error
|
||||
String() string
|
||||
}
|
||||
|
||||
// Service is the functions to get check for new updates
|
||||
type Service interface {
|
||||
Check() (Version, error)
|
||||
}
|
||||
|
||||
const (
|
||||
// OSKeyName is the url parameter key to send to the checkin API for the operating system of the local cloudflared (e.g. windows, darwin, linux)
|
||||
OSKeyName = "os"
|
||||
|
||||
// ArchitectureKeyName is the url parameter key to send to the checkin API for the architecture of the local cloudflared (e.g. amd64, x86)
|
||||
ArchitectureKeyName = "arch"
|
||||
|
||||
// BetaKeyName is the url parameter key to send to the checkin API to signal if the update should be a beta version or not
|
||||
BetaKeyName = "beta"
|
||||
|
||||
// VersionKeyName is the url parameter key to send to the checkin API to specific what version to upgrade or downgrade to
|
||||
VersionKeyName = "version"
|
||||
)
|
||||
@@ -3,42 +3,38 @@ package updater
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/rs/zerolog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/equinox-io/equinox"
|
||||
"github.com/facebookgo/grace/gracenet"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultCheckUpdateFreq = time.Hour * 24
|
||||
appID = "app_idCzgxYerVD"
|
||||
noUpdateInShellMessage = "cloudflared will not automatically update when run from the shell. To enable auto-updates, run cloudflared as a service: https://developers.cloudflare.com/argo-tunnel/reference/service/"
|
||||
noUpdateOnWindowsMessage = "cloudflared will not automatically update on Windows systems."
|
||||
noUpdateManagedPackageMessage = "cloudflared will not automatically update if installed by a package manager."
|
||||
isManagedInstallFile = ".installedFromPackageManager"
|
||||
UpdateURL = "https://update.argotunnel.com"
|
||||
StagingUpdateURL = "https://staging-update.argotunnel.com"
|
||||
|
||||
LogFieldVersion = "version"
|
||||
)
|
||||
|
||||
var (
|
||||
publicKey = []byte(`
|
||||
-----BEGIN ECDSA PUBLIC KEY-----
|
||||
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4OWZocTVZ8Do/L6ScLdkV+9A0IYMHoOf
|
||||
dsCmJ/QZ6aw0w9qkkwEpne1Lmo6+0pGexZzFZOH6w5amShn+RXt7qkSid9iWlzGq
|
||||
EKx0BZogHSor9Wy5VztdFaAaVbsJiCbO
|
||||
-----END ECDSA PUBLIC KEY-----
|
||||
`)
|
||||
version string
|
||||
)
|
||||
|
||||
// BinaryUpdated implements ExitCoder interface, the app will exit with status code 11
|
||||
// https://pkg.go.dev/gopkg.in/urfave/cli.v2?tab=doc#ExitCoder
|
||||
// https://pkg.go.dev/github.com/urfave/cli/v2?tab=doc#ExitCoder
|
||||
type statusSuccess struct {
|
||||
newVersion string
|
||||
}
|
||||
@@ -64,6 +60,13 @@ func (e *statusErr) ExitCode() int {
|
||||
return 10
|
||||
}
|
||||
|
||||
type updateOptions struct {
|
||||
isBeta bool
|
||||
isStaging bool
|
||||
isForced bool
|
||||
version string
|
||||
}
|
||||
|
||||
type UpdateOutcome struct {
|
||||
Updated bool
|
||||
Version string
|
||||
@@ -74,46 +77,73 @@ func (uo *UpdateOutcome) noUpdate() bool {
|
||||
return uo.Error == nil && uo.Updated == false
|
||||
}
|
||||
|
||||
func checkForUpdateAndApply() UpdateOutcome {
|
||||
var opts equinox.Options
|
||||
if err := opts.SetPublicKeyPEM(publicKey); err != nil {
|
||||
return UpdateOutcome{Error: err}
|
||||
}
|
||||
|
||||
resp, err := equinox.Check(appID, opts)
|
||||
switch {
|
||||
case err == equinox.NotAvailableErr:
|
||||
return UpdateOutcome{}
|
||||
case err != nil:
|
||||
return UpdateOutcome{Error: err}
|
||||
}
|
||||
|
||||
err = resp.Apply()
|
||||
if err != nil {
|
||||
return UpdateOutcome{Error: err}
|
||||
}
|
||||
|
||||
return UpdateOutcome{Updated: true, Version: resp.ReleaseVersion}
|
||||
func Init(v string) {
|
||||
version = v
|
||||
}
|
||||
|
||||
func Update(_ *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
func checkForUpdateAndApply(options updateOptions) UpdateOutcome {
|
||||
cfdPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
return UpdateOutcome{Error: err}
|
||||
}
|
||||
|
||||
url := UpdateURL
|
||||
if options.isStaging {
|
||||
url = StagingUpdateURL
|
||||
}
|
||||
|
||||
s := NewWorkersService(version, url, cfdPath, Options{IsBeta: options.isBeta,
|
||||
IsForced: options.isForced, RequestedVersion: options.version})
|
||||
|
||||
v, err := s.Check()
|
||||
if err != nil {
|
||||
return UpdateOutcome{Error: err}
|
||||
}
|
||||
|
||||
//already on the latest version
|
||||
if v == nil {
|
||||
return UpdateOutcome{}
|
||||
}
|
||||
|
||||
err = v.Apply()
|
||||
if err != nil {
|
||||
return UpdateOutcome{Error: err}
|
||||
}
|
||||
|
||||
return UpdateOutcome{Updated: true, Version: v.String()}
|
||||
}
|
||||
|
||||
// Update is the handler for the update command from the command line
|
||||
func Update(c *cli.Context) error {
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
if wasInstalledFromPackageManager() {
|
||||
logger.Error("cloudflared was installed by a package manager. Please update using the same method.")
|
||||
log.Error().Msg("cloudflared was installed by a package manager. Please update using the same method.")
|
||||
return nil
|
||||
}
|
||||
|
||||
updateOutcome := loggedUpdate(logger)
|
||||
isBeta := c.Bool("beta")
|
||||
if isBeta {
|
||||
log.Info().Msg("cloudflared is set to update to the latest beta version")
|
||||
}
|
||||
|
||||
isStaging := c.Bool("staging")
|
||||
if isStaging {
|
||||
log.Info().Msg("cloudflared is set to update from staging")
|
||||
}
|
||||
|
||||
isForced := c.Bool("force")
|
||||
if isForced {
|
||||
log.Info().Msg("cloudflared is set to upgrade to the latest publish version regardless of the current version")
|
||||
}
|
||||
|
||||
updateOutcome := loggedUpdate(log, updateOptions{isBeta: isBeta, isStaging: isStaging, isForced: isForced, version: c.String("version")})
|
||||
if updateOutcome.Error != nil {
|
||||
return &statusErr{updateOutcome.Error}
|
||||
}
|
||||
|
||||
if updateOutcome.noUpdate() {
|
||||
logger.Infof("cloudflared is up to date (%s)", updateOutcome.Version)
|
||||
log.Info().Str(LogFieldVersion, updateOutcome.Version).Msg("cloudflared is up to date")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -121,13 +151,13 @@ func Update(_ *cli.Context) error {
|
||||
}
|
||||
|
||||
// Checks for an update and applies it if one is available
|
||||
func loggedUpdate(logger logger.Service) UpdateOutcome {
|
||||
updateOutcome := checkForUpdateAndApply()
|
||||
func loggedUpdate(log *zerolog.Logger, options updateOptions) UpdateOutcome {
|
||||
updateOutcome := checkForUpdateAndApply(options)
|
||||
if updateOutcome.Updated {
|
||||
logger.Infof("cloudflared has been updated to version %s", updateOutcome.Version)
|
||||
log.Info().Str(LogFieldVersion, updateOutcome.Version).Msg("cloudflared has been updated")
|
||||
}
|
||||
if updateOutcome.Error != nil {
|
||||
logger.Errorf("update check failed: %s", updateOutcome.Error)
|
||||
log.Err(updateOutcome.Error).Msg("update check failed: %s")
|
||||
}
|
||||
|
||||
return updateOutcome
|
||||
@@ -138,7 +168,7 @@ type AutoUpdater struct {
|
||||
configurable *configurable
|
||||
listeners *gracenet.Net
|
||||
updateConfigChan chan *configurable
|
||||
logger logger.Service
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
// AutoUpdaterConfigurable is the attributes of AutoUpdater that can be reconfigured during runtime
|
||||
@@ -147,7 +177,7 @@ type configurable struct {
|
||||
freq time.Duration
|
||||
}
|
||||
|
||||
func NewAutoUpdater(freq time.Duration, listeners *gracenet.Net, logger logger.Service) *AutoUpdater {
|
||||
func NewAutoUpdater(freq time.Duration, listeners *gracenet.Net, log *zerolog.Logger) *AutoUpdater {
|
||||
updaterConfigurable := &configurable{
|
||||
enabled: true,
|
||||
freq: freq,
|
||||
@@ -160,7 +190,7 @@ func NewAutoUpdater(freq time.Duration, listeners *gracenet.Net, logger logger.S
|
||||
configurable: updaterConfigurable,
|
||||
listeners: listeners,
|
||||
updateConfigChan: make(chan *configurable),
|
||||
logger: logger,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,20 +198,19 @@ func (a *AutoUpdater) Run(ctx context.Context) error {
|
||||
ticker := time.NewTicker(a.configurable.freq)
|
||||
for {
|
||||
if a.configurable.enabled {
|
||||
updateOutcome := loggedUpdate(a.logger)
|
||||
updateOutcome := loggedUpdate(a.log, updateOptions{})
|
||||
if updateOutcome.Updated {
|
||||
os.Args = append(os.Args, "--is-autoupdated=true")
|
||||
if IsSysV() {
|
||||
// SysV doesn't have a mechanism to keep service alive, we have to restart the process
|
||||
a.logger.Info("Restarting service managed by SysV...")
|
||||
a.log.Info().Msg("Restarting service managed by SysV...")
|
||||
pid, err := a.listeners.StartProcess()
|
||||
if err != nil {
|
||||
a.logger.Errorf("Unable to restart server automatically: %s", err)
|
||||
a.log.Err(err).Msg("Unable to restart server automatically")
|
||||
return &statusErr{err: err}
|
||||
}
|
||||
// stop old process after autoupdate. Otherwise we create a new process
|
||||
// after each update
|
||||
a.logger.Infof("PID of the new process is %d", pid)
|
||||
a.log.Info().Msgf("PID of the new process is %d", pid)
|
||||
}
|
||||
return &statusSuccess{newVersion: updateOutcome.Version}
|
||||
}
|
||||
@@ -213,26 +242,26 @@ func (a *AutoUpdater) Update(newFreq time.Duration) {
|
||||
a.updateConfigChan <- newConfigurable
|
||||
}
|
||||
|
||||
func IsAutoupdateEnabled(c *cli.Context, l logger.Service) bool {
|
||||
if !SupportAutoUpdate(l) {
|
||||
func IsAutoupdateEnabled(c *cli.Context, log *zerolog.Logger) bool {
|
||||
if !SupportAutoUpdate(log) {
|
||||
return false
|
||||
}
|
||||
return !c.Bool("no-autoupdate") && c.Duration("autoupdate-freq") != 0
|
||||
}
|
||||
|
||||
func SupportAutoUpdate(logger logger.Service) bool {
|
||||
func SupportAutoUpdate(log *zerolog.Logger) bool {
|
||||
if runtime.GOOS == "windows" {
|
||||
logger.Info(noUpdateOnWindowsMessage)
|
||||
log.Info().Msg(noUpdateOnWindowsMessage)
|
||||
return false
|
||||
}
|
||||
|
||||
if wasInstalledFromPackageManager() {
|
||||
logger.Info(noUpdateManagedPackageMessage)
|
||||
log.Info().Msg(noUpdateManagedPackageMessage)
|
||||
return false
|
||||
}
|
||||
|
||||
if isRunningFromTerminal() {
|
||||
logger.Info(noUpdateInShellMessage)
|
||||
log.Info().Msg(noUpdateInShellMessage)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -4,15 +4,15 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/facebookgo/grace/gracenet"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDisabledAutoUpdater(t *testing.T) {
|
||||
listeners := &gracenet.Net{}
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
autoupdater := NewAutoUpdater(0, listeners, logger)
|
||||
log := zerolog.Nop()
|
||||
autoupdater := NewAutoUpdater(0, listeners, &log)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
errC := make(chan error)
|
||||
go func() {
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Options are the update options supported by the
|
||||
type Options struct {
|
||||
// IsBeta is for beta updates to be installed if available
|
||||
IsBeta bool
|
||||
|
||||
// IsForced is to forcibly download the latest version regardless of the current version
|
||||
IsForced bool
|
||||
|
||||
// RequestedVersion is the specific version to upgrade or downgrade to
|
||||
RequestedVersion string
|
||||
}
|
||||
|
||||
// VersionResponse is the JSON response from the Workers API endpoint
|
||||
type VersionResponse struct {
|
||||
URL string `json:"url"`
|
||||
Version string `json:"version"`
|
||||
Checksum string `json:"checksum"`
|
||||
IsCompressed bool `json:"compressed"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// WorkersService implements Service.
|
||||
// It contains everything needed to check in with the WorkersAPI and download and apply the updates
|
||||
type WorkersService struct {
|
||||
currentVersion string
|
||||
url string
|
||||
targetPath string
|
||||
opts Options
|
||||
}
|
||||
|
||||
// NewWorkersService creates a new updater Service object.
|
||||
func NewWorkersService(currentVersion, url, targetPath string, opts Options) Service {
|
||||
return &WorkersService{
|
||||
currentVersion: currentVersion,
|
||||
url: url,
|
||||
targetPath: targetPath,
|
||||
opts: opts,
|
||||
}
|
||||
}
|
||||
|
||||
// Check does a check in with the Workers API to get a new version update
|
||||
func (s *WorkersService) Check() (Version, error) {
|
||||
client := &http.Client{
|
||||
Timeout: clientTimeout,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, s.url, nil)
|
||||
q := req.URL.Query()
|
||||
q.Add(OSKeyName, runtime.GOOS)
|
||||
q.Add(ArchitectureKeyName, runtime.GOARCH)
|
||||
|
||||
if s.opts.IsBeta {
|
||||
q.Add(BetaKeyName, "true")
|
||||
}
|
||||
|
||||
if s.opts.RequestedVersion != "" {
|
||||
q.Add(VersionKeyName, s.opts.RequestedVersion)
|
||||
}
|
||||
|
||||
req.URL.RawQuery = q.Encode()
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var v VersionResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v.Error != "" {
|
||||
return nil, errors.New(v.Error)
|
||||
}
|
||||
|
||||
if !s.opts.IsForced && !IsNewerVersion(s.currentVersion, v.Version) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return NewWorkersVersion(v.URL, v.Version, v.Checksum, s.targetPath, v.IsCompressed), nil
|
||||
}
|
||||
|
||||
// IsNewerVersion checks semantic versioning for the latest version
|
||||
// cloudflared tagging is more of a date than a semantic version,
|
||||
// but the same comparision logic still holds for major.minor.patch
|
||||
// e.g. 2020.8.2 is newer than 2020.8.1.
|
||||
func IsNewerVersion(current string, check string) bool {
|
||||
if strings.Contains(strings.ToLower(current), "dev") {
|
||||
return false // dev builds shouldn't update
|
||||
}
|
||||
|
||||
cMajor, cMinor, cPatch, err := SemanticParts(current)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
nMajor, nMinor, nPatch, err := SemanticParts(check)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if nMajor > cMajor {
|
||||
return true
|
||||
}
|
||||
|
||||
if nMajor == cMajor && nMinor > cMinor {
|
||||
return true
|
||||
}
|
||||
|
||||
if nMajor == cMajor && nMinor == cMinor && nPatch > cPatch {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SemanticParts gets the major, minor, and patch version of a semantic version string
|
||||
// e.g. 3.1.2 would return 3, 1, 2, nil
|
||||
func SemanticParts(version string) (major int, minor int, patch int, err error) {
|
||||
major = 0
|
||||
minor = 0
|
||||
patch = 0
|
||||
parts := strings.Split(version, ".")
|
||||
if len(parts) != 3 {
|
||||
err = errors.New("invalid version")
|
||||
return
|
||||
}
|
||||
major, err = strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
minor, err = strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
patch, err = strconv.Atoi(parts[2])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func respondWithJSON(w http.ResponseWriter, v interface{}, status int) {
|
||||
data, _ := json.Marshal(v)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
func respondWithData(w http.ResponseWriter, b []byte, status int) {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.WriteHeader(status)
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func updateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
version := "2020.08.05"
|
||||
host := fmt.Sprintf("http://%s", r.Host)
|
||||
url := host + "/download"
|
||||
|
||||
query := r.URL.Query()
|
||||
|
||||
if query.Get(BetaKeyName) == "true" {
|
||||
version = "2020.08.06"
|
||||
url = host + "/beta"
|
||||
}
|
||||
|
||||
requestedVersion := query.Get(VersionKeyName)
|
||||
if requestedVersion != "" {
|
||||
version = requestedVersion
|
||||
url = fmt.Sprintf("%s?version=%s", url, requestedVersion)
|
||||
}
|
||||
|
||||
if query.Get(ArchitectureKeyName) != runtime.GOARCH || query.Get(OSKeyName) != runtime.GOOS {
|
||||
respondWithJSON(w, VersionResponse{Error: "unsupported os and architecture"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
fmt.Fprint(h, version)
|
||||
checksum := fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
v := VersionResponse{URL: url, Version: version, Checksum: checksum}
|
||||
respondWithJSON(w, v, http.StatusOK)
|
||||
}
|
||||
|
||||
func gzipUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log.Println("got a request!")
|
||||
version := "2020.09.02"
|
||||
h := sha256.New()
|
||||
fmt.Fprint(h, version)
|
||||
checksum := fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
url := fmt.Sprintf("http://%s/gzip-download.tgz", r.Host)
|
||||
v := VersionResponse{URL: url, Version: version, Checksum: checksum}
|
||||
respondWithJSON(w, v, http.StatusOK)
|
||||
}
|
||||
|
||||
func compressedDownloadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
version := "2020.09.02"
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
gw := gzip.NewWriter(buf)
|
||||
tw := tar.NewWriter(gw)
|
||||
|
||||
header := &tar.Header{
|
||||
Size: int64(len(version)),
|
||||
Name: "download",
|
||||
}
|
||||
tw.WriteHeader(header)
|
||||
tw.Write([]byte(version))
|
||||
|
||||
tw.Close()
|
||||
gw.Close()
|
||||
|
||||
respondWithData(w, buf.Bytes(), http.StatusOK)
|
||||
}
|
||||
|
||||
func downloadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
version := "2020.08.05"
|
||||
requestedVersion := r.URL.Query().Get(VersionKeyName)
|
||||
if requestedVersion != "" {
|
||||
version = requestedVersion
|
||||
}
|
||||
respondWithData(w, []byte(version), http.StatusOK)
|
||||
}
|
||||
|
||||
func betaHandler(w http.ResponseWriter, r *http.Request) {
|
||||
respondWithData(w, []byte("2020.08.06"), http.StatusOK)
|
||||
}
|
||||
|
||||
func failureHandler(w http.ResponseWriter, r *http.Request) {
|
||||
respondWithJSON(w, VersionResponse{Error: "unsupported os and architecture"}, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func createServer() *httptest.Server {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/updater", updateHandler)
|
||||
mux.HandleFunc("/download", downloadHandler)
|
||||
mux.HandleFunc("/beta", betaHandler)
|
||||
mux.HandleFunc("/fail", failureHandler)
|
||||
mux.HandleFunc("/compressed", gzipUpdateHandler)
|
||||
mux.HandleFunc("/gzip-download.tgz", compressedDownloadHandler)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func createTestFile(t *testing.T, path string) {
|
||||
f, err := os.Create("tmpfile")
|
||||
require.NoError(t, err)
|
||||
fmt.Fprint(f, "2020.08.04")
|
||||
f.Close()
|
||||
}
|
||||
|
||||
func TestUpdateService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
testFilePath := "tmpfile"
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
log.Println("server url: ", ts.URL)
|
||||
|
||||
s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{})
|
||||
v, err := s.Check()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, v.String(), "2020.08.05")
|
||||
|
||||
require.NoError(t, v.Apply())
|
||||
dat, err := ioutil.ReadFile(testFilePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, string(dat), "2020.08.05")
|
||||
}
|
||||
|
||||
func TestBetaUpdateService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
testFilePath := "tmpfile"
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
|
||||
s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{IsBeta: true})
|
||||
v, err := s.Check()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, v.String(), "2020.08.06")
|
||||
|
||||
require.NoError(t, v.Apply())
|
||||
dat, err := ioutil.ReadFile(testFilePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, string(dat), "2020.08.06")
|
||||
}
|
||||
|
||||
func TestFailUpdateService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
testFilePath := "tmpfile"
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
|
||||
s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/fail", ts.URL), testFilePath, Options{})
|
||||
v, err := s.Check()
|
||||
require.Error(t, err)
|
||||
require.Nil(t, v)
|
||||
}
|
||||
|
||||
func TestNoUpdateService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
testFilePath := "tmpfile"
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
|
||||
s := NewWorkersService("2020.8.5", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{})
|
||||
v, err := s.Check()
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, v)
|
||||
}
|
||||
|
||||
func TestForcedUpdateService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
testFilePath := "tmpfile"
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
|
||||
s := NewWorkersService("2020.8.5", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{IsForced: true})
|
||||
v, err := s.Check()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, v.String(), "2020.08.05")
|
||||
|
||||
require.NoError(t, v.Apply())
|
||||
dat, err := ioutil.ReadFile(testFilePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, string(dat), "2020.08.05")
|
||||
}
|
||||
|
||||
func TestUpdateSpecificVersionService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
testFilePath := "tmpfile"
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
reqVersion := "2020.9.1"
|
||||
|
||||
s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{RequestedVersion: reqVersion})
|
||||
v, err := s.Check()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, reqVersion, v.String())
|
||||
|
||||
require.NoError(t, v.Apply())
|
||||
dat, err := ioutil.ReadFile(testFilePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, reqVersion, string(dat))
|
||||
}
|
||||
|
||||
func TestCompressedUpdateService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
testFilePath := "tmpfile"
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
|
||||
s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/compressed", ts.URL), testFilePath, Options{})
|
||||
v, err := s.Check()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "2020.09.02", v.String())
|
||||
|
||||
require.NoError(t, v.Apply())
|
||||
dat, err := ioutil.ReadFile(testFilePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "2020.09.02", string(dat))
|
||||
}
|
||||
|
||||
func TestVersionParsing(t *testing.T) {
|
||||
require.False(t, IsNewerVersion("2020.8.2", "2020.8.2"))
|
||||
require.True(t, IsNewerVersion("2020.8.2", "2020.8.3"))
|
||||
require.True(t, IsNewerVersion("2020.8.2", "2021.1.2"))
|
||||
require.True(t, IsNewerVersion("2020.8.2", "2020.9.1"))
|
||||
require.True(t, IsNewerVersion("2020.8.2", "2020.12.45"))
|
||||
require.False(t, IsNewerVersion("2020.8.2", "2020.6.3"))
|
||||
require.False(t, IsNewerVersion("DEV", "2020.8.5"))
|
||||
require.False(t, IsNewerVersion("2020.8.2", "asdlkfjasdf"))
|
||||
require.True(t, IsNewerVersion("3.0.1", "4.2.1"))
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
clientTimeout = time.Second * 60
|
||||
// stop the service
|
||||
// rename cloudflared.exe to cloudflared.exe.old
|
||||
// rename cloudflared.exe.new to cloudflared.exe
|
||||
// delete cloudflared.exe.old
|
||||
// start the service
|
||||
// delete the batch file
|
||||
windowsUpdateCommandTemplate = `@echo off
|
||||
sc stop cloudflared >nul 2>&1
|
||||
rename "{{.TargetPath}}" {{.OldName}}
|
||||
rename "{{.NewPath}}" {{.BinaryName}}
|
||||
del "{{.OldPath}}"
|
||||
sc start cloudflared >nul 2>&1
|
||||
del {{.BatchName}}`
|
||||
batchFileName = "cfd_update.bat"
|
||||
)
|
||||
|
||||
// Prepare some data to insert into the template.
|
||||
type batchData struct {
|
||||
TargetPath string
|
||||
OldName string
|
||||
NewPath string
|
||||
OldPath string
|
||||
BinaryName string
|
||||
BatchName string
|
||||
}
|
||||
|
||||
// WorkersVersion implements the Version interface.
|
||||
// It contains everything needed to preform a version upgrade
|
||||
type WorkersVersion struct {
|
||||
downloadURL string
|
||||
checksum string
|
||||
version string
|
||||
targetPath string
|
||||
isCompressed bool
|
||||
}
|
||||
|
||||
// NewWorkersVersion creates a new Version object. This is normally created by a WorkersService JSON checkin response
|
||||
// url is where to download the file
|
||||
// version is the version of this update
|
||||
// checksum is the expected checksum of the downloaded file
|
||||
// target path is where the file should be replace. Normally this the running cloudflared's path
|
||||
func NewWorkersVersion(url, version, checksum, targetPath string, isCompressed bool) Version {
|
||||
return &WorkersVersion{downloadURL: url, version: version, checksum: checksum, targetPath: targetPath, isCompressed: isCompressed}
|
||||
}
|
||||
|
||||
// Apply does the actual verification and update logic.
|
||||
// This includes signature and checksum validation,
|
||||
// replacing the binary, etc
|
||||
func (v *WorkersVersion) Apply() error {
|
||||
newFilePath := fmt.Sprintf("%s.new", v.targetPath)
|
||||
os.Remove(newFilePath) //remove any failed updates before download
|
||||
|
||||
// download the file
|
||||
if err := download(v.downloadURL, newFilePath, v.isCompressed); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check that the file is what is expected
|
||||
if err := isValidChecksum(v.checksum, newFilePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oldFilePath := fmt.Sprintf("%s.old", v.targetPath)
|
||||
// Windows requires more effort to self update, especially when it is running as a service:
|
||||
// you have to stop the service (if running as one) in order to move/rename the binary
|
||||
// but now the binary isn't running though, so an external process
|
||||
// has to move the old binary out and the new one in then start the service
|
||||
// the easiest way to do this is with a batch file (or with a DLL, but that gets ugly for a cross compiled binary like cloudflared)
|
||||
// a batch file isn't ideal, but it is the simplest path forward for the constraints Windows creates
|
||||
if runtime.GOOS == "windows" {
|
||||
if err := writeBatchFile(v.targetPath, newFilePath, oldFilePath); err != nil {
|
||||
return err
|
||||
}
|
||||
rootDir := filepath.Dir(v.targetPath)
|
||||
batchPath := filepath.Join(rootDir, batchFileName)
|
||||
return runWindowsBatch(batchPath)
|
||||
}
|
||||
|
||||
// now move the current file out, move the new file in and delete the old file
|
||||
if err := os.Rename(v.targetPath, oldFilePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Rename(newFilePath, v.targetPath); err != nil {
|
||||
//attempt rollback
|
||||
os.Rename(oldFilePath, v.targetPath)
|
||||
return err
|
||||
}
|
||||
os.Remove(oldFilePath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns the version number of this update/release (e.g. 2020.08.05)
|
||||
func (v *WorkersVersion) String() string {
|
||||
return v.version
|
||||
}
|
||||
|
||||
// download the file from the link in the json
|
||||
func download(url, filepath string, isCompressed bool) error {
|
||||
client := &http.Client{
|
||||
Timeout: clientTimeout,
|
||||
}
|
||||
resp, err := client.Get(url)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var r io.Reader
|
||||
r = resp.Body
|
||||
|
||||
// compressed macos binary, need to decompress
|
||||
if isCompressed || isCompressedFile(url) {
|
||||
// first the gzip reader
|
||||
gr, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer gr.Close()
|
||||
|
||||
// now the tar
|
||||
tr := tar.NewReader(gr)
|
||||
|
||||
// advance the reader pass the header, which will be the single binary file
|
||||
tr.Next()
|
||||
|
||||
r = tr
|
||||
}
|
||||
|
||||
out, err := os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, r)
|
||||
return err
|
||||
}
|
||||
|
||||
// isCompressedFile is a really simple file extension check to see if this is a macos tar and gzipped
|
||||
func isCompressedFile(urlstring string) bool {
|
||||
if strings.HasSuffix(urlstring, ".tgz") {
|
||||
return true
|
||||
}
|
||||
|
||||
u, err := url.Parse(urlstring)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.HasSuffix(u.Path, ".tgz")
|
||||
}
|
||||
|
||||
// checks if the checksum in the json response matches the checksum of the file download
|
||||
func isValidChecksum(checksum, filePath string) error {
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hash := fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
if checksum != hash {
|
||||
return errors.New("checksum validation failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeBatchFile writes a batch file out to disk
|
||||
// see the dicussion on why it has to be done this way
|
||||
func writeBatchFile(targetPath string, newPath string, oldPath string) error {
|
||||
os.Remove(batchFileName) //remove any failed updates before download
|
||||
f, err := os.Create(batchFileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
cfdName := filepath.Base(targetPath)
|
||||
oldName := filepath.Base(oldPath)
|
||||
|
||||
data := batchData{
|
||||
TargetPath: targetPath,
|
||||
OldName: oldName,
|
||||
NewPath: newPath,
|
||||
OldPath: oldPath,
|
||||
BinaryName: cfdName,
|
||||
BatchName: batchFileName,
|
||||
}
|
||||
|
||||
t, err := template.New("batch").Parse(windowsUpdateCommandTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return t.Execute(f, data)
|
||||
}
|
||||
|
||||
// run each OS command for windows
|
||||
func runWindowsBatch(batchFile string) error {
|
||||
cmd := exec.Command("cmd", "/c", batchFile)
|
||||
return cmd.Start()
|
||||
}
|
||||
@@ -13,9 +13,8 @@ import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/pkg/errors"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"golang.org/x/sys/windows/svc/eventlog"
|
||||
@@ -37,23 +36,25 @@ const (
|
||||
// ERROR_FAILED_SERVICE_CONTROLLER_CONNECT
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/debug/system-error-codes--1000-1299-
|
||||
serviceControllerConnectionFailure = 1063
|
||||
|
||||
LogFieldWindowsServiceName = "windowsServiceName"
|
||||
)
|
||||
|
||||
func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
func runApp(app *cli.App, graceShutdownC chan struct{}) {
|
||||
app.Commands = append(app.Commands, &cli.Command{
|
||||
Name: "service",
|
||||
Usage: "Manages the Argo Tunnel Windows service",
|
||||
Subcommands: []*cli.Command{
|
||||
&cli.Command{
|
||||
{
|
||||
Name: "install",
|
||||
Usage: "Install Argo Tunnel as a Windows service",
|
||||
Action: installWindowsService,
|
||||
},
|
||||
&cli.Command{
|
||||
},
|
||||
{
|
||||
Name: "uninstall",
|
||||
Usage: "Uninstall the Argo Tunnel service",
|
||||
Action: uninstallWindowsService,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -67,15 +68,11 @@ 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
|
||||
}
|
||||
log := logger.Create(nil)
|
||||
|
||||
isIntSess, err := svc.IsAnInteractiveSession()
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to determine if we are running in an interactive session: %v", err)
|
||||
log.Fatal().Err(err).Msg("failed to determine if we are running in an interactive session")
|
||||
}
|
||||
if isIntSess {
|
||||
app.Run(os.Args)
|
||||
@@ -85,7 +82,7 @@ func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
// Run executes service name by calling windowsService which is a Handler
|
||||
// interface that implements Execute method.
|
||||
// It will set service status to stop after Execute returns
|
||||
err = svc.Run(windowsServiceName, &windowsService{app: app, shutdownC: shutdownC, graceShutdownC: graceShutdownC})
|
||||
err = svc.Run(windowsServiceName, &windowsService{app: app, graceShutdownC: graceShutdownC})
|
||||
if err != nil {
|
||||
if errno, ok := err.(syscall.Errno); ok && int(errno) == serviceControllerConnectionFailure {
|
||||
// Hack: assume this is a false negative from the IsAnInteractiveSession() check above.
|
||||
@@ -93,27 +90,22 @@ func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
app.Run(os.Args)
|
||||
return
|
||||
}
|
||||
logger.Fatalf("%s service failed: %v", windowsServiceName, err)
|
||||
log.Fatal().Err(err).Msgf("%s service failed", windowsServiceName)
|
||||
}
|
||||
}
|
||||
|
||||
type windowsService struct {
|
||||
app *cli.App
|
||||
shutdownC chan struct{}
|
||||
graceShutdownC chan struct{}
|
||||
}
|
||||
|
||||
// called by the package code at the start of the service
|
||||
// Execute is called by the service manager when service starts, the state
|
||||
// of the service will be set to Stopped when this function returns.
|
||||
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
|
||||
}
|
||||
|
||||
log := logger.Create(nil)
|
||||
elog, err := eventlog.Open(windowsServiceName)
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot open event log for %s with error: %s", windowsServiceName, err)
|
||||
log.Err(err).Msgf("Cannot open event log for %s", windowsServiceName)
|
||||
return
|
||||
}
|
||||
defer elog.Close()
|
||||
@@ -136,13 +128,12 @@ func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeReques
|
||||
}
|
||||
elog.Info(1, fmt.Sprintf("%s service arguments: %v", windowsServiceName, args))
|
||||
|
||||
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
|
||||
statusChan <- svc.Status{State: svc.StartPending}
|
||||
errC := make(chan error)
|
||||
go func() {
|
||||
errC <- s.app.Run(args)
|
||||
}()
|
||||
statusChan <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
||||
statusChan <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown}
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -150,19 +141,26 @@ func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeReques
|
||||
switch c.Cmd {
|
||||
case svc.Interrogate:
|
||||
statusChan <- c.CurrentStatus
|
||||
case svc.Stop:
|
||||
close(s.graceShutdownC)
|
||||
statusChan <- svc.Status{State: svc.StopPending}
|
||||
case svc.Shutdown:
|
||||
close(s.shutdownC)
|
||||
case svc.Stop, svc.Shutdown:
|
||||
if s.graceShutdownC != nil {
|
||||
// start graceful shutdown
|
||||
elog.Info(1, "cloudflared starting graceful shutdown")
|
||||
close(s.graceShutdownC)
|
||||
s.graceShutdownC = nil
|
||||
statusChan <- svc.Status{State: svc.StopPending}
|
||||
continue
|
||||
}
|
||||
// repeated attempts at graceful shutdown forces immediate stop
|
||||
elog.Info(1, "cloudflared terminating immediately")
|
||||
statusChan <- svc.Status{State: svc.StopPending}
|
||||
return false, 0
|
||||
default:
|
||||
elog.Error(1, fmt.Sprintf("unexpected control request #%d", c))
|
||||
}
|
||||
case err := <-errC:
|
||||
ssec = true
|
||||
if err != nil {
|
||||
elog.Error(1, fmt.Sprintf("cloudflared terminated with error %v", err))
|
||||
ssec = true
|
||||
errno = 1
|
||||
} else {
|
||||
elog.Info(1, "cloudflared terminated without error")
|
||||
@@ -174,79 +172,76 @@ 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")
|
||||
}
|
||||
zeroLogger := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
logger.Infof("Installing Argo Tunnel Windows service")
|
||||
zeroLogger.Info().Msg("Installing Argo Tunnel Windows service")
|
||||
exepath, err := os.Executable()
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot find path name that start the process")
|
||||
zeroLogger.Err(err).Msg("Cannot find path name that start the process")
|
||||
return err
|
||||
}
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot establish a connection to the service control manager: %s", err)
|
||||
zeroLogger.Err(err).Msg("Cannot establish a connection to the service control manager")
|
||||
return err
|
||||
}
|
||||
defer m.Disconnect()
|
||||
s, err := m.OpenService(windowsServiceName)
|
||||
log := zeroLogger.With().Str(LogFieldWindowsServiceName, windowsServiceName).Logger()
|
||||
if err == nil {
|
||||
s.Close()
|
||||
logger.Errorf("service %s already exists", windowsServiceName)
|
||||
log.Err(err).Msg("service already exists")
|
||||
return fmt.Errorf("service %s already exists", windowsServiceName)
|
||||
}
|
||||
config := mgr.Config{StartType: mgr.StartAutomatic, DisplayName: windowsServiceDescription}
|
||||
s, err = m.CreateService(windowsServiceName, exepath, config)
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot install service %s", windowsServiceName)
|
||||
log.Error().Msg("Cannot install service")
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
logger.Infof("Argo Tunnel agent service is installed")
|
||||
log.Info().Msg("Argo Tunnel agent service is installed")
|
||||
err = eventlog.InstallAsEventCreate(windowsServiceName, eventlog.Error|eventlog.Warning|eventlog.Info)
|
||||
if err != nil {
|
||||
s.Delete()
|
||||
logger.Errorf("Cannot install event logger: %s", err)
|
||||
log.Err(err).Msg("Cannot install event logger")
|
||||
return fmt.Errorf("SetupEventLogSource() failed: %s", err)
|
||||
}
|
||||
err = configRecoveryOption(s.Handle)
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot set service recovery actions: %s", err)
|
||||
logger.Infof("See %s to manually configure service recovery actions", windowsServiceUrl)
|
||||
log.Err(err).Msg("Cannot set service recovery actions")
|
||||
log.Info().Msgf("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")
|
||||
}
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog).
|
||||
With().
|
||||
Str(LogFieldWindowsServiceName, windowsServiceName).Logger()
|
||||
|
||||
logger.Infof("Uninstalling Argo Tunnel Windows Service")
|
||||
log.Info().Msg("Uninstalling Argo Tunnel Windows Service")
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot establish a connection to the service control manager")
|
||||
log.Error().Msg("Cannot establish a connection to the service control manager")
|
||||
return err
|
||||
}
|
||||
defer m.Disconnect()
|
||||
s, err := m.OpenService(windowsServiceName)
|
||||
if err != nil {
|
||||
logger.Errorf("service %s is not installed", windowsServiceName)
|
||||
log.Error().Msg("service is not installed")
|
||||
return fmt.Errorf("service %s is not installed", windowsServiceName)
|
||||
}
|
||||
defer s.Close()
|
||||
err = s.Delete()
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot delete service %s", windowsServiceName)
|
||||
log.Error().Msg("Cannot delete service")
|
||||
return err
|
||||
}
|
||||
logger.Infof("Argo Tunnel agent service is uninstalled")
|
||||
log.Info().Msg("Argo Tunnel agent service is uninstalled")
|
||||
err = eventlog.Remove(windowsServiceName)
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot remove event logger")
|
||||
log.Error().Msg("Cannot remove event logger")
|
||||
return fmt.Errorf("RemoveEventLogSource() failed: %s", err)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const LogFieldConnIndex = "connIndex"
|
||||
|
||||
type Config struct {
|
||||
OriginClient OriginClient
|
||||
GracePeriod time.Duration
|
||||
ReplaceExisting bool
|
||||
}
|
||||
|
||||
type NamedTunnelConfig struct {
|
||||
Credentials Credentials
|
||||
Client pogs.ClientInfo
|
||||
}
|
||||
|
||||
// Credentials are stored in the credentials file and contain all info needed to run a tunnel.
|
||||
type Credentials struct {
|
||||
AccountTag string
|
||||
TunnelSecret []byte
|
||||
TunnelID uuid.UUID
|
||||
TunnelName string
|
||||
}
|
||||
|
||||
func (c *Credentials) Auth() pogs.TunnelAuth {
|
||||
return pogs.TunnelAuth{
|
||||
AccountTag: c.AccountTag,
|
||||
TunnelSecret: c.TunnelSecret,
|
||||
}
|
||||
}
|
||||
|
||||
type ClassicTunnelConfig struct {
|
||||
Hostname string
|
||||
OriginCert []byte
|
||||
// feature-flag to use new edge reconnect tokens
|
||||
UseReconnectToken bool
|
||||
}
|
||||
|
||||
func (c *ClassicTunnelConfig) IsTrialZone() bool {
|
||||
return c.Hostname == ""
|
||||
}
|
||||
|
||||
type OriginClient interface {
|
||||
Proxy(w ResponseWriter, req *http.Request, isWebsocket bool) error
|
||||
}
|
||||
|
||||
type ResponseWriter interface {
|
||||
WriteRespHeaders(*http.Response) error
|
||||
WriteErrorResponse()
|
||||
io.ReadWriter
|
||||
}
|
||||
|
||||
type ConnectedFuse interface {
|
||||
Connected()
|
||||
IsConnected() bool
|
||||
}
|
||||
|
||||
func IsServerSentEvent(headers http.Header) bool {
|
||||
if contentType := headers.Get("content-type"); contentType != "" {
|
||||
return strings.HasPrefix(strings.ToLower(contentType), "text/event-stream")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func uint8ToString(input uint8) string {
|
||||
return strconv.FormatUint(uint64(input), 10)
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
largeFileSize = 2 * 1024 * 1024
|
||||
)
|
||||
|
||||
var (
|
||||
testConfig = &Config{
|
||||
OriginClient: &mockOriginClient{},
|
||||
GracePeriod: time.Millisecond * 100,
|
||||
}
|
||||
log = zerolog.Nop()
|
||||
testOriginURL = &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "connectiontest.argotunnel.com",
|
||||
}
|
||||
testLargeResp = make([]byte, largeFileSize)
|
||||
)
|
||||
|
||||
type testRequest struct {
|
||||
name string
|
||||
endpoint string
|
||||
expectedStatus int
|
||||
expectedBody []byte
|
||||
isProxyError bool
|
||||
}
|
||||
|
||||
type mockOriginClient struct {
|
||||
}
|
||||
|
||||
func (moc *mockOriginClient) Proxy(w ResponseWriter, r *http.Request, isWebsocket bool) error {
|
||||
if isWebsocket {
|
||||
return wsEndpoint(w, r)
|
||||
}
|
||||
switch r.URL.Path {
|
||||
case "/ok":
|
||||
originRespEndpoint(w, http.StatusOK, []byte(http.StatusText(http.StatusOK)))
|
||||
case "/large_file":
|
||||
originRespEndpoint(w, http.StatusOK, testLargeResp)
|
||||
case "/400":
|
||||
originRespEndpoint(w, http.StatusBadRequest, []byte(http.StatusText(http.StatusBadRequest)))
|
||||
case "/500":
|
||||
originRespEndpoint(w, http.StatusInternalServerError, []byte(http.StatusText(http.StatusInternalServerError)))
|
||||
case "/error":
|
||||
return fmt.Errorf("Failed to proxy to origin")
|
||||
default:
|
||||
originRespEndpoint(w, http.StatusNotFound, []byte("page not found"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type nowriter struct {
|
||||
io.Reader
|
||||
}
|
||||
|
||||
func (nowriter) Write(p []byte) (int, error) {
|
||||
return 0, fmt.Errorf("Writer not implemented")
|
||||
}
|
||||
|
||||
func wsEndpoint(w ResponseWriter, r *http.Request) error {
|
||||
resp := &http.Response{
|
||||
StatusCode: http.StatusSwitchingProtocols,
|
||||
}
|
||||
_ = w.WriteRespHeaders(resp)
|
||||
clientReader := nowriter{r.Body}
|
||||
go func() {
|
||||
for {
|
||||
data, err := wsutil.ReadClientText(clientReader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := wsutil.WriteServerText(w, data); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
<-r.Context().Done()
|
||||
return nil
|
||||
}
|
||||
|
||||
func originRespEndpoint(w ResponseWriter, status int, data []byte) {
|
||||
resp := &http.Response{
|
||||
StatusCode: status,
|
||||
}
|
||||
_ = w.WriteRespHeaders(resp)
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
type mockConnectedFuse struct{}
|
||||
|
||||
func (mcf mockConnectedFuse) Connected() {}
|
||||
|
||||
func (mcf mockConnectedFuse) IsConnected() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func TestIsEventStream(t *testing.T) {
|
||||
tests := []struct {
|
||||
headers http.Header
|
||||
isEventStream bool
|
||||
}{
|
||||
{
|
||||
headers: newHeader("Content-Type", "text/event-stream"),
|
||||
isEventStream: true,
|
||||
},
|
||||
{
|
||||
headers: newHeader("content-type", "text/event-stream"),
|
||||
isEventStream: true,
|
||||
},
|
||||
{
|
||||
headers: newHeader("Content-Type", "text/event-stream; charset=utf-8"),
|
||||
isEventStream: true,
|
||||
},
|
||||
{
|
||||
headers: newHeader("Content-Type", "application/json"),
|
||||
isEventStream: false,
|
||||
},
|
||||
{
|
||||
headers: http.Header{},
|
||||
isEventStream: false,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
assert.Equal(t, test.isEventStream, IsServerSentEvent(test.headers))
|
||||
}
|
||||
}
|
||||
|
||||
func newHeader(key, value string) http.Header {
|
||||
header := http.Header{}
|
||||
header.Add(key, value)
|
||||
return header
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
const (
|
||||
DuplicateConnectionError = "EDUPCONN"
|
||||
)
|
||||
|
||||
type DupConnRegisterTunnelError struct{}
|
||||
|
||||
var errDuplicationConnection = DupConnRegisterTunnelError{}
|
||||
|
||||
func (e DupConnRegisterTunnelError) Error() string {
|
||||
return "already connected to this server, trying another address"
|
||||
}
|
||||
|
||||
// RegisterTunnel error from server
|
||||
type ServerRegisterTunnelError struct {
|
||||
Cause error
|
||||
Permanent bool
|
||||
}
|
||||
|
||||
func (e ServerRegisterTunnelError) Error() string {
|
||||
return e.Cause.Error()
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
type muxerShutdownError struct{}
|
||||
|
||||
func (e muxerShutdownError) Error() string {
|
||||
return "muxer shutdown"
|
||||
}
|
||||
|
||||
var errMuxerStopped = muxerShutdownError{}
|
||||
|
||||
func isHandshakeErrRecoverable(err error, connIndex uint8, observer *Observer) bool {
|
||||
log := observer.log.With().
|
||||
Uint8(LogFieldConnIndex, connIndex).
|
||||
Err(err).
|
||||
Logger()
|
||||
|
||||
switch err.(type) {
|
||||
case edgediscovery.DialError:
|
||||
log.Error().Msg("Connection unable to dial edge")
|
||||
case h2mux.MuxerHandshakeError:
|
||||
log.Error().Msg("Connection handshake with edge server failed")
|
||||
default:
|
||||
log.Error().Msg("Connection failed")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package connection
|
||||
|
||||
// Event is something that happened to a connection, e.g. disconnection or registration.
|
||||
type Event struct {
|
||||
Index uint8
|
||||
EventType Status
|
||||
Location string
|
||||
URL string
|
||||
}
|
||||
|
||||
// Status is the status of a connection.
|
||||
type Status int
|
||||
|
||||
const (
|
||||
// Disconnected means the connection to the edge was broken.
|
||||
Disconnected Status = iota
|
||||
// Connected means the connection to the edge was successfully established.
|
||||
Connected
|
||||
// Reconnecting means the connection to the edge is being re-established.
|
||||
Reconnecting
|
||||
// SetURL means this connection's tunnel was given a URL by the edge. Used for free tunnels.
|
||||
SetURL
|
||||
// RegisteringTunnel means the non-named tunnel is registering its connection.
|
||||
RegisteringTunnel
|
||||
// We're unregistering tunnel from the edge in preparation for a disconnect
|
||||
Unregistering
|
||||
)
|
||||
@@ -0,0 +1,255 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const (
|
||||
muxerTimeout = 5 * time.Second
|
||||
openStreamTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
type h2muxConnection struct {
|
||||
config *Config
|
||||
muxerConfig *MuxerConfig
|
||||
muxer *h2mux.Muxer
|
||||
// connectionID is only used by metrics, and prometheus requires labels to be string
|
||||
connIndexStr string
|
||||
connIndex uint8
|
||||
|
||||
observer *Observer
|
||||
gracefulShutdownC <-chan struct{}
|
||||
stoppedGracefully bool
|
||||
|
||||
// newRPCClientFunc allows us to mock RPCs during testing
|
||||
newRPCClientFunc func(context.Context, io.ReadWriteCloser, *zerolog.Logger) NamedTunnelRPCClient
|
||||
}
|
||||
|
||||
type MuxerConfig struct {
|
||||
HeartbeatInterval time.Duration
|
||||
MaxHeartbeats uint64
|
||||
CompressionSetting h2mux.CompressionSetting
|
||||
MetricsUpdateFreq time.Duration
|
||||
}
|
||||
|
||||
func (mc *MuxerConfig) H2MuxerConfig(h h2mux.MuxedStreamHandler, log *zerolog.Logger) *h2mux.MuxerConfig {
|
||||
return &h2mux.MuxerConfig{
|
||||
Timeout: muxerTimeout,
|
||||
Handler: h,
|
||||
IsClient: true,
|
||||
HeartbeatInterval: mc.HeartbeatInterval,
|
||||
MaxHeartbeats: mc.MaxHeartbeats,
|
||||
Log: log,
|
||||
CompressionQuality: mc.CompressionSetting,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTunnelHandler returns a TunnelHandler, origin LAN IP and error
|
||||
func NewH2muxConnection(
|
||||
config *Config,
|
||||
muxerConfig *MuxerConfig,
|
||||
edgeConn net.Conn,
|
||||
connIndex uint8,
|
||||
observer *Observer,
|
||||
gracefulShutdownC <-chan struct{},
|
||||
) (*h2muxConnection, error, bool) {
|
||||
h := &h2muxConnection{
|
||||
config: config,
|
||||
muxerConfig: muxerConfig,
|
||||
connIndexStr: uint8ToString(connIndex),
|
||||
connIndex: connIndex,
|
||||
observer: observer,
|
||||
gracefulShutdownC: gracefulShutdownC,
|
||||
newRPCClientFunc: newRegistrationRPCClient,
|
||||
}
|
||||
|
||||
// Establish a muxed connection with the edge
|
||||
// Client mux handshake with agent server
|
||||
muxer, err := h2mux.Handshake(edgeConn, edgeConn, *muxerConfig.H2MuxerConfig(h, observer.logTransport), h2mux.ActiveStreams)
|
||||
if err != nil {
|
||||
recoverable := isHandshakeErrRecoverable(err, connIndex, observer)
|
||||
return nil, err, recoverable
|
||||
}
|
||||
h.muxer = muxer
|
||||
return h, nil, false
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) ServeNamedTunnel(ctx context.Context, namedTunnel *NamedTunnelConfig, connOptions *tunnelpogs.ConnectionOptions, connectedFuse ConnectedFuse) error {
|
||||
errGroup, serveCtx := errgroup.WithContext(ctx)
|
||||
errGroup.Go(func() error {
|
||||
return h.serveMuxer(serveCtx)
|
||||
})
|
||||
|
||||
errGroup.Go(func() error {
|
||||
if err := h.registerNamedTunnel(serveCtx, namedTunnel, connOptions); err != nil {
|
||||
return err
|
||||
}
|
||||
connectedFuse.Connected()
|
||||
return nil
|
||||
})
|
||||
|
||||
errGroup.Go(func() error {
|
||||
h.controlLoop(serveCtx, connectedFuse, true)
|
||||
return nil
|
||||
})
|
||||
|
||||
err := errGroup.Wait()
|
||||
if err == errMuxerStopped {
|
||||
if h.stoppedGracefully {
|
||||
return nil
|
||||
}
|
||||
h.observer.log.Info().Uint8(LogFieldConnIndex, h.connIndex).Msg("Unexpected muxer shutdown")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) ServeClassicTunnel(ctx context.Context, classicTunnel *ClassicTunnelConfig, credentialManager CredentialManager, registrationOptions *tunnelpogs.RegistrationOptions, connectedFuse ConnectedFuse) error {
|
||||
errGroup, serveCtx := errgroup.WithContext(ctx)
|
||||
errGroup.Go(func() error {
|
||||
return h.serveMuxer(serveCtx)
|
||||
})
|
||||
|
||||
errGroup.Go(func() (err error) {
|
||||
defer func() {
|
||||
if err == nil {
|
||||
connectedFuse.Connected()
|
||||
}
|
||||
}()
|
||||
if classicTunnel.UseReconnectToken && connectedFuse.IsConnected() {
|
||||
err := h.reconnectTunnel(ctx, credentialManager, classicTunnel, registrationOptions)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
// log errors and proceed to RegisterTunnel
|
||||
h.observer.log.Err(err).
|
||||
Uint8(LogFieldConnIndex, h.connIndex).
|
||||
Msg("Couldn't reconnect connection. Re-registering it instead.")
|
||||
}
|
||||
return h.registerTunnel(ctx, credentialManager, classicTunnel, registrationOptions)
|
||||
})
|
||||
|
||||
errGroup.Go(func() error {
|
||||
h.controlLoop(serveCtx, connectedFuse, false)
|
||||
return nil
|
||||
})
|
||||
|
||||
err := errGroup.Wait()
|
||||
if err == errMuxerStopped {
|
||||
if h.stoppedGracefully {
|
||||
return nil
|
||||
}
|
||||
h.observer.log.Info().Uint8(LogFieldConnIndex, h.connIndex).Msg("Unexpected muxer shutdown")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) serveMuxer(ctx context.Context) error {
|
||||
// All routines should stop when muxer finish serving. When muxer is shutdown
|
||||
// gracefully, it doesn't return an error, so we need to return errMuxerShutdown
|
||||
// here to notify other routines to stop
|
||||
err := h.muxer.Serve(ctx)
|
||||
if err == nil {
|
||||
return errMuxerStopped
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) controlLoop(ctx context.Context, connectedFuse ConnectedFuse, isNamedTunnel bool) {
|
||||
updateMetricsTickC := time.Tick(h.muxerConfig.MetricsUpdateFreq)
|
||||
var shutdownCompleted <-chan struct{}
|
||||
for {
|
||||
select {
|
||||
case <-h.gracefulShutdownC:
|
||||
if connectedFuse.IsConnected() {
|
||||
h.unregister(isNamedTunnel)
|
||||
}
|
||||
h.stoppedGracefully = true
|
||||
h.gracefulShutdownC = nil
|
||||
shutdownCompleted = h.muxer.Shutdown()
|
||||
|
||||
case <-shutdownCompleted:
|
||||
return
|
||||
|
||||
case <-ctx.Done():
|
||||
// UnregisterTunnel blocks until the RPC call returns
|
||||
if !h.stoppedGracefully && connectedFuse.IsConnected() {
|
||||
h.unregister(isNamedTunnel)
|
||||
}
|
||||
h.muxer.Shutdown()
|
||||
// don't wait for shutdown to finish when context is closed, this is the hard termination path
|
||||
return
|
||||
|
||||
case <-updateMetricsTickC:
|
||||
h.observer.metrics.updateMuxerMetrics(h.connIndexStr, h.muxer.Metrics())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) newRPCStream(ctx context.Context, rpcName rpcName) (*h2mux.MuxedStream, error) {
|
||||
openStreamCtx, openStreamCancel := context.WithTimeout(ctx, openStreamTimeout)
|
||||
defer openStreamCancel()
|
||||
stream, err := h.muxer.OpenRPCStream(openStreamCtx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) ServeStream(stream *h2mux.MuxedStream) error {
|
||||
respWriter := &h2muxRespWriter{stream}
|
||||
|
||||
req, reqErr := h.newRequest(stream)
|
||||
if reqErr != nil {
|
||||
respWriter.WriteErrorResponse()
|
||||
return reqErr
|
||||
}
|
||||
|
||||
err := h.config.OriginClient.Proxy(respWriter, req, websocket.IsWebSocketUpgrade(req))
|
||||
if err != nil {
|
||||
respWriter.WriteErrorResponse()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) newRequest(stream *h2mux.MuxedStream) (*http.Request, error) {
|
||||
req, err := http.NewRequest("GET", "http://localhost:8080", 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
|
||||
}
|
||||
|
||||
type h2muxRespWriter struct {
|
||||
*h2mux.MuxedStream
|
||||
}
|
||||
|
||||
func (rp *h2muxRespWriter) WriteRespHeaders(resp *http.Response) error {
|
||||
headers := h2mux.H1ResponseToH2ResponseHeaders(resp)
|
||||
headers = append(headers, h2mux.Header{Name: ResponseMetaHeaderField, Value: responseMetaHeaderOrigin})
|
||||
return rp.WriteHeaders(headers)
|
||||
}
|
||||
|
||||
func (rp *h2muxRespWriter) WriteErrorResponse() {
|
||||
_ = rp.WriteHeaders([]h2mux.Header{
|
||||
{Name: ":status", Value: "502"},
|
||||
{Name: ResponseMetaHeaderField, Value: responseMetaHeaderCfd},
|
||||
})
|
||||
_, _ = rp.Write([]byte("502 Bad Gateway"))
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
)
|
||||
|
||||
var (
|
||||
testMuxerConfig = &MuxerConfig{
|
||||
HeartbeatInterval: time.Second * 5,
|
||||
MaxHeartbeats: 5,
|
||||
CompressionSetting: 0,
|
||||
MetricsUpdateFreq: time.Second * 5,
|
||||
}
|
||||
)
|
||||
|
||||
func newH2MuxConnection(t require.TestingT) (*h2muxConnection, *h2mux.Muxer) {
|
||||
edgeConn, originConn := net.Pipe()
|
||||
edgeMuxChan := make(chan *h2mux.Muxer)
|
||||
go func() {
|
||||
edgeMuxConfig := h2mux.MuxerConfig{
|
||||
Log: &log,
|
||||
Handler: h2mux.MuxedStreamFunc(func(stream *h2mux.MuxedStream) error {
|
||||
// we only expect RPC traffic in client->edge direction, provide minimal support for mocking
|
||||
require.True(t, stream.IsRPCStream())
|
||||
return stream.WriteHeaders([]h2mux.Header{
|
||||
{Name: ":status", Value: "200"},
|
||||
})
|
||||
}),
|
||||
}
|
||||
edgeMux, err := h2mux.Handshake(edgeConn, edgeConn, edgeMuxConfig, h2mux.ActiveStreams)
|
||||
require.NoError(t, err)
|
||||
edgeMuxChan <- edgeMux
|
||||
}()
|
||||
var connIndex = uint8(0)
|
||||
testObserver := NewObserver(&log, &log, false)
|
||||
h2muxConn, err, _ := NewH2muxConnection(testConfig, testMuxerConfig, originConn, connIndex, testObserver, nil)
|
||||
require.NoError(t, err)
|
||||
return h2muxConn, <-edgeMuxChan
|
||||
}
|
||||
|
||||
func TestServeStreamHTTP(t *testing.T) {
|
||||
tests := []testRequest{
|
||||
{
|
||||
name: "ok",
|
||||
endpoint: "/ok",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: []byte(http.StatusText(http.StatusOK)),
|
||||
},
|
||||
{
|
||||
name: "large_file",
|
||||
endpoint: "/large_file",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: testLargeResp,
|
||||
},
|
||||
{
|
||||
name: "Bad request",
|
||||
endpoint: "/400",
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedBody: []byte(http.StatusText(http.StatusBadRequest)),
|
||||
},
|
||||
{
|
||||
name: "Internal server error",
|
||||
endpoint: "/500",
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
expectedBody: []byte(http.StatusText(http.StatusInternalServerError)),
|
||||
},
|
||||
{
|
||||
name: "Proxy error",
|
||||
endpoint: "/error",
|
||||
expectedStatus: http.StatusBadGateway,
|
||||
expectedBody: nil,
|
||||
isProxyError: true,
|
||||
},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
h2muxConn, edgeMux := newH2MuxConnection(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_ = edgeMux.Serve(ctx)
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := h2muxConn.serveMuxer(ctx)
|
||||
require.Error(t, err)
|
||||
}()
|
||||
|
||||
for _, test := range tests {
|
||||
headers := []h2mux.Header{
|
||||
{
|
||||
Name: ":path",
|
||||
Value: test.endpoint,
|
||||
},
|
||||
}
|
||||
stream, err := edgeMux.OpenStream(ctx, headers, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, hasHeader(stream, ":status", strconv.Itoa(test.expectedStatus)))
|
||||
|
||||
if test.isProxyError {
|
||||
assert.True(t, hasHeader(stream, ResponseMetaHeaderField, responseMetaHeaderCfd))
|
||||
} else {
|
||||
assert.True(t, hasHeader(stream, ResponseMetaHeaderField, responseMetaHeaderOrigin))
|
||||
body := make([]byte, len(test.expectedBody))
|
||||
_, err = stream.Read(body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expectedBody, body)
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestServeStreamWS(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
h2muxConn, edgeMux := newH2MuxConnection(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
edgeMux.Serve(ctx)
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := h2muxConn.serveMuxer(ctx)
|
||||
require.Error(t, err)
|
||||
}()
|
||||
|
||||
headers := []h2mux.Header{
|
||||
{
|
||||
Name: ":path",
|
||||
Value: "/ws",
|
||||
},
|
||||
{
|
||||
Name: "connection",
|
||||
Value: "upgrade",
|
||||
},
|
||||
{
|
||||
Name: "upgrade",
|
||||
Value: "websocket",
|
||||
},
|
||||
}
|
||||
|
||||
readPipe, writePipe := io.Pipe()
|
||||
stream, err := edgeMux.OpenStream(ctx, headers, readPipe)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, hasHeader(stream, ":status", strconv.Itoa(http.StatusSwitchingProtocols)))
|
||||
assert.True(t, hasHeader(stream, ResponseMetaHeaderField, responseMetaHeaderOrigin))
|
||||
|
||||
data := []byte("test websocket")
|
||||
err = wsutil.WriteClientText(writePipe, data)
|
||||
require.NoError(t, err)
|
||||
|
||||
respBody, err := wsutil.ReadServerText(stream)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, data, respBody, fmt.Sprintf("Expect %s, got %s", string(data), string(respBody)))
|
||||
|
||||
cancel()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestGracefulShutdownH2Mux(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
h2muxConn, edgeMux := newH2MuxConnection(t)
|
||||
|
||||
shutdownC := make(chan struct{})
|
||||
unregisteredC := make(chan struct{})
|
||||
h2muxConn.gracefulShutdownC = shutdownC
|
||||
h2muxConn.newRPCClientFunc = func(_ context.Context, _ io.ReadWriteCloser, _ *zerolog.Logger) NamedTunnelRPCClient {
|
||||
return &mockNamedTunnelRPCClient{
|
||||
registered: nil,
|
||||
unregistered: unregisteredC,
|
||||
}
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(3)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_ = edgeMux.Serve(ctx)
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_ = h2muxConn.serveMuxer(ctx)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
h2muxConn.controlLoop(ctx, &mockConnectedFuse{}, true)
|
||||
}()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
close(shutdownC)
|
||||
|
||||
select {
|
||||
case <-unregisteredC:
|
||||
break // ok
|
||||
case <-time.Tick(time.Second):
|
||||
assert.Fail(t, "timed out waiting for control loop to unregister")
|
||||
}
|
||||
|
||||
cancel()
|
||||
wg.Wait()
|
||||
|
||||
assert.True(t, h2muxConn.stoppedGracefully)
|
||||
assert.Nil(t, h2muxConn.gracefulShutdownC)
|
||||
}
|
||||
|
||||
func hasHeader(stream *h2mux.MuxedStream, name, val string) bool {
|
||||
for _, header := range stream.Headers {
|
||||
if header.Name == name && header.Value == val {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func benchmarkServeStreamHTTPSimple(b *testing.B, test testRequest) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
h2muxConn, edgeMux := newH2MuxConnection(b)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
edgeMux.Serve(ctx)
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := h2muxConn.serveMuxer(ctx)
|
||||
require.Error(b, err)
|
||||
}()
|
||||
|
||||
headers := []h2mux.Header{
|
||||
{
|
||||
Name: ":path",
|
||||
Value: test.endpoint,
|
||||
},
|
||||
}
|
||||
|
||||
body := make([]byte, len(test.expectedBody))
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
b.StartTimer()
|
||||
stream, openstreamErr := edgeMux.OpenStream(ctx, headers, nil)
|
||||
_, readBodyErr := stream.Read(body)
|
||||
b.StopTimer()
|
||||
|
||||
require.NoError(b, openstreamErr)
|
||||
assert.True(b, hasHeader(stream, ResponseMetaHeaderField, responseMetaHeaderOrigin))
|
||||
require.True(b, hasHeader(stream, ":status", strconv.Itoa(http.StatusOK)))
|
||||
require.NoError(b, readBodyErr)
|
||||
require.Equal(b, test.expectedBody, body)
|
||||
}
|
||||
|
||||
cancel()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func BenchmarkServeStreamHTTPSimple(b *testing.B) {
|
||||
test := testRequest{
|
||||
name: "ok",
|
||||
endpoint: "/ok",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: []byte(http.StatusText(http.StatusOK)),
|
||||
}
|
||||
|
||||
benchmarkServeStreamHTTPSimple(b, test)
|
||||
}
|
||||
|
||||
func BenchmarkServeStreamHTTPLargeFile(b *testing.B) {
|
||||
test := testRequest{
|
||||
name: "large_file",
|
||||
endpoint: "/large_file",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: testLargeResp,
|
||||
}
|
||||
|
||||
benchmarkServeStreamHTTPSimple(b, test)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
)
|
||||
|
||||
const (
|
||||
ResponseMetaHeaderField = "cf-cloudflared-response-meta"
|
||||
)
|
||||
|
||||
var (
|
||||
canonicalResponseUserHeadersField = http.CanonicalHeaderKey(h2mux.ResponseUserHeadersField)
|
||||
canonicalResponseMetaHeaderField = http.CanonicalHeaderKey(ResponseMetaHeaderField)
|
||||
responseMetaHeaderCfd = mustInitRespMetaHeader("cloudflared")
|
||||
responseMetaHeaderOrigin = mustInitRespMetaHeader("origin")
|
||||
)
|
||||
|
||||
type responseMetaHeader struct {
|
||||
Source string `json:"src"`
|
||||
}
|
||||
|
||||
func mustInitRespMetaHeader(src string) string {
|
||||
header, err := json.Marshal(responseMetaHeader{Source: src})
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to serialize response meta header = %s, err: %v", src, err))
|
||||
}
|
||||
return string(header)
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
const (
|
||||
internalUpgradeHeader = "Cf-Cloudflared-Proxy-Connection-Upgrade"
|
||||
websocketUpgrade = "websocket"
|
||||
controlStreamUpgrade = "control-stream"
|
||||
)
|
||||
|
||||
var errEdgeConnectionClosed = fmt.Errorf("connection with edge closed")
|
||||
|
||||
type http2Connection struct {
|
||||
conn net.Conn
|
||||
server *http2.Server
|
||||
config *Config
|
||||
namedTunnel *NamedTunnelConfig
|
||||
connOptions *tunnelpogs.ConnectionOptions
|
||||
observer *Observer
|
||||
connIndexStr string
|
||||
connIndex uint8
|
||||
// newRPCClientFunc allows us to mock RPCs during testing
|
||||
newRPCClientFunc func(context.Context, io.ReadWriteCloser, *zerolog.Logger) NamedTunnelRPCClient
|
||||
|
||||
activeRequestsWG sync.WaitGroup
|
||||
connectedFuse ConnectedFuse
|
||||
gracefulShutdownC <-chan struct{}
|
||||
stoppedGracefully bool
|
||||
controlStreamErr error // result of running control stream handler
|
||||
}
|
||||
|
||||
func NewHTTP2Connection(
|
||||
conn net.Conn,
|
||||
config *Config,
|
||||
namedTunnelConfig *NamedTunnelConfig,
|
||||
connOptions *tunnelpogs.ConnectionOptions,
|
||||
observer *Observer,
|
||||
connIndex uint8,
|
||||
connectedFuse ConnectedFuse,
|
||||
gracefulShutdownC <-chan struct{},
|
||||
) *http2Connection {
|
||||
return &http2Connection{
|
||||
conn: conn,
|
||||
server: &http2.Server{
|
||||
MaxConcurrentStreams: math.MaxUint32,
|
||||
},
|
||||
config: config,
|
||||
namedTunnel: namedTunnelConfig,
|
||||
connOptions: connOptions,
|
||||
observer: observer,
|
||||
connIndexStr: uint8ToString(connIndex),
|
||||
connIndex: connIndex,
|
||||
newRPCClientFunc: newRegistrationRPCClient,
|
||||
connectedFuse: connectedFuse,
|
||||
gracefulShutdownC: gracefulShutdownC,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *http2Connection) Serve(ctx context.Context) error {
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
c.close()
|
||||
}()
|
||||
c.server.ServeConn(c.conn, &http2.ServeConnOpts{
|
||||
Context: ctx,
|
||||
Handler: c,
|
||||
})
|
||||
|
||||
switch {
|
||||
case c.stoppedGracefully:
|
||||
return nil
|
||||
case c.controlStreamErr != nil:
|
||||
return c.controlStreamErr
|
||||
default:
|
||||
c.observer.log.Info().Uint8(LogFieldConnIndex, c.connIndex).Msg("Lost connection with the edge")
|
||||
return errEdgeConnectionClosed
|
||||
}
|
||||
}
|
||||
|
||||
func (c *http2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
c.activeRequestsWG.Add(1)
|
||||
defer c.activeRequestsWG.Done()
|
||||
|
||||
respWriter := &http2RespWriter{
|
||||
r: r.Body,
|
||||
w: w,
|
||||
}
|
||||
flusher, isFlusher := w.(http.Flusher)
|
||||
if !isFlusher {
|
||||
c.observer.log.Error().Msgf("%T doesn't implement http.Flusher", w)
|
||||
respWriter.WriteErrorResponse()
|
||||
return
|
||||
}
|
||||
respWriter.flusher = flusher
|
||||
var err error
|
||||
if isControlStreamUpgrade(r) {
|
||||
respWriter.shouldFlush = true
|
||||
err = c.serveControlStream(r.Context(), respWriter)
|
||||
c.controlStreamErr = err
|
||||
} else if isWebsocketUpgrade(r) {
|
||||
respWriter.shouldFlush = true
|
||||
stripWebsocketUpgradeHeader(r)
|
||||
err = c.config.OriginClient.Proxy(respWriter, r, true)
|
||||
} else {
|
||||
err = c.config.OriginClient.Proxy(respWriter, r, false)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
respWriter.WriteErrorResponse()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *http2Connection) serveControlStream(ctx context.Context, respWriter *http2RespWriter) error {
|
||||
rpcClient := c.newRPCClientFunc(ctx, respWriter, c.observer.log)
|
||||
defer rpcClient.Close()
|
||||
|
||||
if err := rpcClient.RegisterConnection(ctx, c.namedTunnel, c.connOptions, c.connIndex, c.observer); err != nil {
|
||||
return err
|
||||
}
|
||||
c.connectedFuse.Connected()
|
||||
|
||||
// wait for connection termination or start of graceful shutdown
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break
|
||||
case <-c.gracefulShutdownC:
|
||||
c.stoppedGracefully = true
|
||||
}
|
||||
|
||||
c.observer.sendUnregisteringEvent(c.connIndex)
|
||||
rpcClient.GracefulShutdown(ctx, c.config.GracePeriod)
|
||||
c.observer.log.Info().Uint8(LogFieldConnIndex, c.connIndex).Msg("Unregistered tunnel connection")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *http2Connection) close() {
|
||||
// Wait for all serve HTTP handlers to return
|
||||
c.activeRequestsWG.Wait()
|
||||
c.conn.Close()
|
||||
}
|
||||
|
||||
type http2RespWriter struct {
|
||||
r io.Reader
|
||||
w http.ResponseWriter
|
||||
flusher http.Flusher
|
||||
shouldFlush bool
|
||||
}
|
||||
|
||||
func (rp *http2RespWriter) WriteRespHeaders(resp *http.Response) error {
|
||||
dest := rp.w.Header()
|
||||
userHeaders := make(http.Header, len(resp.Header))
|
||||
for header, values := range resp.Header {
|
||||
// Since these are http2 headers, they're required to be lowercase
|
||||
h2name := strings.ToLower(header)
|
||||
for _, v := range values {
|
||||
if h2name == "content-length" {
|
||||
// This header has meaning in HTTP/2 and will be used by the edge,
|
||||
// so it should be sent as an HTTP/2 response header.
|
||||
dest.Add(h2name, v)
|
||||
// Since these are http2 headers, they're required to be lowercase
|
||||
} else if !h2mux.IsControlHeader(h2name) || h2mux.IsWebsocketClientHeader(h2name) {
|
||||
// User headers, on the other hand, must all be serialized so that
|
||||
// HTTP/2 header validation won't be applied to HTTP/1 header values
|
||||
userHeaders.Add(h2name, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Perform user header serialization and set them in the single header
|
||||
dest.Set(canonicalResponseUserHeadersField, h2mux.SerializeHeaders(userHeaders))
|
||||
rp.setResponseMetaHeader(responseMetaHeaderOrigin)
|
||||
status := resp.StatusCode
|
||||
// HTTP2 removes support for 101 Switching Protocols https://tools.ietf.org/html/rfc7540#section-8.1.1
|
||||
if status == http.StatusSwitchingProtocols {
|
||||
status = http.StatusOK
|
||||
}
|
||||
rp.w.WriteHeader(status)
|
||||
if IsServerSentEvent(resp.Header) {
|
||||
rp.shouldFlush = true
|
||||
}
|
||||
if rp.shouldFlush {
|
||||
rp.flusher.Flush()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rp *http2RespWriter) WriteErrorResponse() {
|
||||
rp.setResponseMetaHeader(responseMetaHeaderCfd)
|
||||
rp.w.WriteHeader(http.StatusBadGateway)
|
||||
}
|
||||
|
||||
func (rp *http2RespWriter) setResponseMetaHeader(value string) {
|
||||
rp.w.Header().Set(canonicalResponseMetaHeaderField, value)
|
||||
}
|
||||
|
||||
func (rp *http2RespWriter) Read(p []byte) (n int, err error) {
|
||||
return rp.r.Read(p)
|
||||
}
|
||||
|
||||
func (rp *http2RespWriter) Write(p []byte) (n int, err error) {
|
||||
defer func() {
|
||||
// Implementer of OriginClient should make sure it doesn't write to the connection after Proxy returns
|
||||
// Register a recover routine just in case.
|
||||
if r := recover(); r != nil {
|
||||
println("Recover from http2 response writer panic, error", r)
|
||||
}
|
||||
}()
|
||||
n, err = rp.w.Write(p)
|
||||
if err == nil && rp.shouldFlush {
|
||||
rp.flusher.Flush()
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (rp *http2RespWriter) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func isControlStreamUpgrade(r *http.Request) bool {
|
||||
return strings.ToLower(r.Header.Get(internalUpgradeHeader)) == controlStreamUpgrade
|
||||
}
|
||||
|
||||
func isWebsocketUpgrade(r *http.Request) bool {
|
||||
return strings.ToLower(r.Header.Get(internalUpgradeHeader)) == websocketUpgrade
|
||||
}
|
||||
|
||||
func stripWebsocketUpgradeHeader(r *http.Request) {
|
||||
r.Header.Del(internalUpgradeHeader)
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
var (
|
||||
testTransport = http2.Transport{}
|
||||
)
|
||||
|
||||
func newTestHTTP2Connection() (*http2Connection, net.Conn) {
|
||||
edgeConn, originConn := net.Pipe()
|
||||
var connIndex = uint8(0)
|
||||
return NewHTTP2Connection(
|
||||
originConn,
|
||||
testConfig,
|
||||
&NamedTunnelConfig{},
|
||||
&pogs.ConnectionOptions{},
|
||||
NewObserver(&log, &log, false),
|
||||
connIndex,
|
||||
mockConnectedFuse{},
|
||||
nil,
|
||||
), edgeConn
|
||||
}
|
||||
|
||||
func TestServeHTTP(t *testing.T) {
|
||||
tests := []testRequest{
|
||||
{
|
||||
name: "ok",
|
||||
endpoint: "ok",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: []byte(http.StatusText(http.StatusOK)),
|
||||
},
|
||||
{
|
||||
name: "large_file",
|
||||
endpoint: "large_file",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: testLargeResp,
|
||||
},
|
||||
{
|
||||
name: "Bad request",
|
||||
endpoint: "400",
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedBody: []byte(http.StatusText(http.StatusBadRequest)),
|
||||
},
|
||||
{
|
||||
name: "Internal server error",
|
||||
endpoint: "500",
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
expectedBody: []byte(http.StatusText(http.StatusInternalServerError)),
|
||||
},
|
||||
{
|
||||
name: "Proxy error",
|
||||
endpoint: "error",
|
||||
expectedStatus: http.StatusBadGateway,
|
||||
expectedBody: nil,
|
||||
isProxyError: true,
|
||||
},
|
||||
}
|
||||
|
||||
http2Conn, edgeConn := newTestHTTP2Connection()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
http2Conn.Serve(ctx)
|
||||
}()
|
||||
|
||||
edgeHTTP2Conn, err := testTransport.NewClientConn(edgeConn)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, test := range tests {
|
||||
endpoint := fmt.Sprintf("http://localhost:8080/%s", test.endpoint)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := edgeHTTP2Conn.RoundTrip(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expectedStatus, resp.StatusCode)
|
||||
if test.expectedBody != nil {
|
||||
respBody, err := ioutil.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expectedBody, respBody)
|
||||
}
|
||||
if test.isProxyError {
|
||||
require.Equal(t, responseMetaHeaderCfd, resp.Header.Get(ResponseMetaHeaderField))
|
||||
} else {
|
||||
require.Equal(t, responseMetaHeaderOrigin, resp.Header.Get(ResponseMetaHeaderField))
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
type mockNamedTunnelRPCClient struct {
|
||||
registered chan struct{}
|
||||
unregistered chan struct{}
|
||||
}
|
||||
|
||||
func (mc mockNamedTunnelRPCClient) RegisterConnection(
|
||||
c context.Context,
|
||||
config *NamedTunnelConfig,
|
||||
options *tunnelpogs.ConnectionOptions,
|
||||
connIndex uint8,
|
||||
observer *Observer,
|
||||
) error {
|
||||
close(mc.registered)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mc mockNamedTunnelRPCClient) GracefulShutdown(ctx context.Context, gracePeriod time.Duration) {
|
||||
close(mc.unregistered)
|
||||
}
|
||||
|
||||
func (mockNamedTunnelRPCClient) Close() {}
|
||||
|
||||
type mockRPCClientFactory struct {
|
||||
registered chan struct{}
|
||||
unregistered chan struct{}
|
||||
}
|
||||
|
||||
func (mf *mockRPCClientFactory) newMockRPCClient(context.Context, io.ReadWriteCloser, *zerolog.Logger) NamedTunnelRPCClient {
|
||||
return mockNamedTunnelRPCClient{
|
||||
registered: mf.registered,
|
||||
unregistered: mf.unregistered,
|
||||
}
|
||||
}
|
||||
|
||||
type wsRespWriter struct {
|
||||
*httptest.ResponseRecorder
|
||||
readPipe *io.PipeReader
|
||||
writePipe *io.PipeWriter
|
||||
}
|
||||
|
||||
func newWSRespWriter() *wsRespWriter {
|
||||
readPipe, writePipe := io.Pipe()
|
||||
return &wsRespWriter{
|
||||
httptest.NewRecorder(),
|
||||
readPipe,
|
||||
writePipe,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *wsRespWriter) RespBody() io.ReadWriter {
|
||||
return nowriter{w.readPipe}
|
||||
}
|
||||
|
||||
func (w *wsRespWriter) Write(data []byte) (n int, err error) {
|
||||
return w.writePipe.Write(data)
|
||||
}
|
||||
|
||||
func TestServeWS(t *testing.T) {
|
||||
http2Conn, _ := newTestHTTP2Connection()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
http2Conn.Serve(ctx)
|
||||
}()
|
||||
|
||||
respWriter := newWSRespWriter()
|
||||
readPipe, writePipe := io.Pipe()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/ws", readPipe)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set(internalUpgradeHeader, websocketUpgrade)
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
http2Conn.ServeHTTP(respWriter, req)
|
||||
}()
|
||||
|
||||
data := []byte("test websocket")
|
||||
err = wsutil.WriteClientText(writePipe, data)
|
||||
require.NoError(t, err)
|
||||
|
||||
respBody, err := wsutil.ReadServerText(respWriter.RespBody())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, data, respBody, fmt.Sprintf("Expect %s, got %s", string(data), string(respBody)))
|
||||
|
||||
cancel()
|
||||
resp := respWriter.Result()
|
||||
// http2RespWriter should rewrite status 101 to 200
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.Equal(t, responseMetaHeaderOrigin, resp.Header.Get(ResponseMetaHeaderField))
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestServeControlStream(t *testing.T) {
|
||||
http2Conn, edgeConn := newTestHTTP2Connection()
|
||||
|
||||
rpcClientFactory := mockRPCClientFactory{
|
||||
registered: make(chan struct{}),
|
||||
unregistered: make(chan struct{}),
|
||||
}
|
||||
http2Conn.newRPCClientFunc = rpcClientFactory.newMockRPCClient
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
http2Conn.Serve(ctx)
|
||||
}()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/", nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set(internalUpgradeHeader, controlStreamUpgrade)
|
||||
|
||||
edgeHTTP2Conn, err := testTransport.NewClientConn(edgeConn)
|
||||
require.NoError(t, err)
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
edgeHTTP2Conn.RoundTrip(req)
|
||||
}()
|
||||
|
||||
<-rpcClientFactory.registered
|
||||
cancel()
|
||||
<-rpcClientFactory.unregistered
|
||||
assert.False(t, http2Conn.stoppedGracefully)
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestGracefulShutdownHTTP2(t *testing.T) {
|
||||
http2Conn, edgeConn := newTestHTTP2Connection()
|
||||
|
||||
rpcClientFactory := mockRPCClientFactory{
|
||||
registered: make(chan struct{}),
|
||||
unregistered: make(chan struct{}),
|
||||
}
|
||||
events := &eventCollectorSink{}
|
||||
http2Conn.newRPCClientFunc = rpcClientFactory.newMockRPCClient
|
||||
http2Conn.observer.RegisterSink(events)
|
||||
shutdownC := make(chan struct{})
|
||||
http2Conn.gracefulShutdownC = shutdownC
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
http2Conn.Serve(ctx)
|
||||
}()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/", nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set(internalUpgradeHeader, controlStreamUpgrade)
|
||||
|
||||
edgeHTTP2Conn, err := testTransport.NewClientConn(edgeConn)
|
||||
require.NoError(t, err)
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_, _ = edgeHTTP2Conn.RoundTrip(req)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-rpcClientFactory.registered:
|
||||
break //ok
|
||||
case <-time.Tick(time.Second):
|
||||
t.Fatal("timeout out waiting for registration")
|
||||
}
|
||||
|
||||
// signal graceful shutdown
|
||||
close(shutdownC)
|
||||
|
||||
select {
|
||||
case <-rpcClientFactory.unregistered:
|
||||
break //ok
|
||||
case <-time.Tick(time.Second):
|
||||
t.Fatal("timeout out waiting for unregistered signal")
|
||||
}
|
||||
assert.True(t, http2Conn.stoppedGracefully)
|
||||
|
||||
cancel()
|
||||
wg.Wait()
|
||||
|
||||
events.assertSawEvent(t, Event{
|
||||
Index: http2Conn.connIndex,
|
||||
EventType: Unregistering,
|
||||
})
|
||||
}
|
||||
|
||||
func benchmarkServeHTTP(b *testing.B, test testRequest) {
|
||||
http2Conn, edgeConn := newTestHTTP2Connection()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
http2Conn.Serve(ctx)
|
||||
}()
|
||||
|
||||
endpoint := fmt.Sprintf("http://localhost:8080/%s", test.endpoint)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
require.NoError(b, err)
|
||||
|
||||
edgeHTTP2Conn, err := testTransport.NewClientConn(edgeConn)
|
||||
require.NoError(b, err)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
b.StartTimer()
|
||||
resp, err := edgeHTTP2Conn.RoundTrip(req)
|
||||
b.StopTimer()
|
||||
require.NoError(b, err)
|
||||
require.Equal(b, test.expectedStatus, resp.StatusCode)
|
||||
if test.expectedBody != nil {
|
||||
respBody, err := ioutil.ReadAll(resp.Body)
|
||||
require.NoError(b, err)
|
||||
require.Equal(b, test.expectedBody, respBody)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
cancel()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func BenchmarkServeHTTPSimple(b *testing.B) {
|
||||
test := testRequest{
|
||||
name: "ok",
|
||||
endpoint: "ok",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: []byte(http.StatusText(http.StatusOK)),
|
||||
}
|
||||
|
||||
benchmarkServeHTTP(b, test)
|
||||
}
|
||||
|
||||
func BenchmarkServeHTTPLargeFile(b *testing.B) {
|
||||
test := testRequest{
|
||||
name: "large_file",
|
||||
endpoint: "large_file",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: testLargeResp,
|
||||
}
|
||||
|
||||
benchmarkServeHTTP(b, test)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
var json = jsoniter.ConfigFastest
|
||||
@@ -0,0 +1,417 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
MetricsNamespace = "cloudflared"
|
||||
TunnelSubsystem = "tunnel"
|
||||
muxerSubsystem = "muxer"
|
||||
)
|
||||
|
||||
type muxerMetrics struct {
|
||||
rtt *prometheus.GaugeVec
|
||||
rttMin *prometheus.GaugeVec
|
||||
rttMax *prometheus.GaugeVec
|
||||
receiveWindowAve *prometheus.GaugeVec
|
||||
sendWindowAve *prometheus.GaugeVec
|
||||
receiveWindowMin *prometheus.GaugeVec
|
||||
receiveWindowMax *prometheus.GaugeVec
|
||||
sendWindowMin *prometheus.GaugeVec
|
||||
sendWindowMax *prometheus.GaugeVec
|
||||
inBoundRateCurr *prometheus.GaugeVec
|
||||
inBoundRateMin *prometheus.GaugeVec
|
||||
inBoundRateMax *prometheus.GaugeVec
|
||||
outBoundRateCurr *prometheus.GaugeVec
|
||||
outBoundRateMin *prometheus.GaugeVec
|
||||
outBoundRateMax *prometheus.GaugeVec
|
||||
compBytesBefore *prometheus.GaugeVec
|
||||
compBytesAfter *prometheus.GaugeVec
|
||||
compRateAve *prometheus.GaugeVec
|
||||
}
|
||||
|
||||
type tunnelMetrics struct {
|
||||
timerRetries prometheus.Gauge
|
||||
serverLocations *prometheus.GaugeVec
|
||||
// locationLock is a mutex for oldServerLocations
|
||||
locationLock sync.Mutex
|
||||
// oldServerLocations stores the last server the tunnel was connected to
|
||||
oldServerLocations map[string]string
|
||||
|
||||
regSuccess *prometheus.CounterVec
|
||||
regFail *prometheus.CounterVec
|
||||
rpcFail *prometheus.CounterVec
|
||||
|
||||
muxerMetrics *muxerMetrics
|
||||
tunnelsHA tunnelsForHA
|
||||
userHostnamesCounts *prometheus.CounterVec
|
||||
}
|
||||
|
||||
func newMuxerMetrics() *muxerMetrics {
|
||||
rtt := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "rtt",
|
||||
Help: "Round-trip time in millisecond",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(rtt)
|
||||
|
||||
rttMin := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "rtt_min",
|
||||
Help: "Shortest round-trip time in millisecond",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(rttMin)
|
||||
|
||||
rttMax := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "rtt_max",
|
||||
Help: "Longest round-trip time in millisecond",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(rttMax)
|
||||
|
||||
receiveWindowAve := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "receive_window_ave",
|
||||
Help: "Average receive window size in bytes",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(receiveWindowAve)
|
||||
|
||||
sendWindowAve := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "send_window_ave",
|
||||
Help: "Average send window size in bytes",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(sendWindowAve)
|
||||
|
||||
receiveWindowMin := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "receive_window_min",
|
||||
Help: "Smallest receive window size in bytes",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(receiveWindowMin)
|
||||
|
||||
receiveWindowMax := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "receive_window_max",
|
||||
Help: "Largest receive window size in bytes",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(receiveWindowMax)
|
||||
|
||||
sendWindowMin := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "send_window_min",
|
||||
Help: "Smallest send window size in bytes",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(sendWindowMin)
|
||||
|
||||
sendWindowMax := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "send_window_max",
|
||||
Help: "Largest send window size in bytes",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(sendWindowMax)
|
||||
|
||||
inBoundRateCurr := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "inbound_bytes_per_sec_curr",
|
||||
Help: "Current inbounding bytes per second, 0 if there is no incoming connection",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(inBoundRateCurr)
|
||||
|
||||
inBoundRateMin := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "inbound_bytes_per_sec_min",
|
||||
Help: "Minimum non-zero inbounding bytes per second",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(inBoundRateMin)
|
||||
|
||||
inBoundRateMax := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "inbound_bytes_per_sec_max",
|
||||
Help: "Maximum inbounding bytes per second",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(inBoundRateMax)
|
||||
|
||||
outBoundRateCurr := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "outbound_bytes_per_sec_curr",
|
||||
Help: "Current outbounding bytes per second, 0 if there is no outgoing traffic",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(outBoundRateCurr)
|
||||
|
||||
outBoundRateMin := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "outbound_bytes_per_sec_min",
|
||||
Help: "Minimum non-zero outbounding bytes per second",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(outBoundRateMin)
|
||||
|
||||
outBoundRateMax := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "outbound_bytes_per_sec_max",
|
||||
Help: "Maximum outbounding bytes per second",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(outBoundRateMax)
|
||||
|
||||
compBytesBefore := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "comp_bytes_before",
|
||||
Help: "Bytes sent via cross-stream compression, pre compression",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(compBytesBefore)
|
||||
|
||||
compBytesAfter := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "comp_bytes_after",
|
||||
Help: "Bytes sent via cross-stream compression, post compression",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(compBytesAfter)
|
||||
|
||||
compRateAve := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "comp_rate_ave",
|
||||
Help: "Average outbound cross-stream compression ratio",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(compRateAve)
|
||||
|
||||
return &muxerMetrics{
|
||||
rtt: rtt,
|
||||
rttMin: rttMin,
|
||||
rttMax: rttMax,
|
||||
receiveWindowAve: receiveWindowAve,
|
||||
sendWindowAve: sendWindowAve,
|
||||
receiveWindowMin: receiveWindowMin,
|
||||
receiveWindowMax: receiveWindowMax,
|
||||
sendWindowMin: sendWindowMin,
|
||||
sendWindowMax: sendWindowMax,
|
||||
inBoundRateCurr: inBoundRateCurr,
|
||||
inBoundRateMin: inBoundRateMin,
|
||||
inBoundRateMax: inBoundRateMax,
|
||||
outBoundRateCurr: outBoundRateCurr,
|
||||
outBoundRateMin: outBoundRateMin,
|
||||
outBoundRateMax: outBoundRateMax,
|
||||
compBytesBefore: compBytesBefore,
|
||||
compBytesAfter: compBytesAfter,
|
||||
compRateAve: compRateAve,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *muxerMetrics) update(connectionID string, metrics *h2mux.MuxerMetrics) {
|
||||
m.rtt.WithLabelValues(connectionID).Set(convertRTTMilliSec(metrics.RTT))
|
||||
m.rttMin.WithLabelValues(connectionID).Set(convertRTTMilliSec(metrics.RTTMin))
|
||||
m.rttMax.WithLabelValues(connectionID).Set(convertRTTMilliSec(metrics.RTTMax))
|
||||
m.receiveWindowAve.WithLabelValues(connectionID).Set(metrics.ReceiveWindowAve)
|
||||
m.sendWindowAve.WithLabelValues(connectionID).Set(metrics.SendWindowAve)
|
||||
m.receiveWindowMin.WithLabelValues(connectionID).Set(float64(metrics.ReceiveWindowMin))
|
||||
m.receiveWindowMax.WithLabelValues(connectionID).Set(float64(metrics.ReceiveWindowMax))
|
||||
m.sendWindowMin.WithLabelValues(connectionID).Set(float64(metrics.SendWindowMin))
|
||||
m.sendWindowMax.WithLabelValues(connectionID).Set(float64(metrics.SendWindowMax))
|
||||
m.inBoundRateCurr.WithLabelValues(connectionID).Set(float64(metrics.InBoundRateCurr))
|
||||
m.inBoundRateMin.WithLabelValues(connectionID).Set(float64(metrics.InBoundRateMin))
|
||||
m.inBoundRateMax.WithLabelValues(connectionID).Set(float64(metrics.InBoundRateMax))
|
||||
m.outBoundRateCurr.WithLabelValues(connectionID).Set(float64(metrics.OutBoundRateCurr))
|
||||
m.outBoundRateMin.WithLabelValues(connectionID).Set(float64(metrics.OutBoundRateMin))
|
||||
m.outBoundRateMax.WithLabelValues(connectionID).Set(float64(metrics.OutBoundRateMax))
|
||||
m.compBytesBefore.WithLabelValues(connectionID).Set(float64(metrics.CompBytesBefore.Value()))
|
||||
m.compBytesAfter.WithLabelValues(connectionID).Set(float64(metrics.CompBytesAfter.Value()))
|
||||
m.compRateAve.WithLabelValues(connectionID).Set(float64(metrics.CompRateAve()))
|
||||
}
|
||||
|
||||
func convertRTTMilliSec(t time.Duration) float64 {
|
||||
return float64(t / time.Millisecond)
|
||||
}
|
||||
|
||||
// Metrics that can be collected without asking the edge
|
||||
func initTunnelMetrics() *tunnelMetrics {
|
||||
maxConcurrentRequestsPerTunnel := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: TunnelSubsystem,
|
||||
Name: "max_concurrent_requests_per_tunnel",
|
||||
Help: "Largest number of concurrent requests proxied through each tunnel so far",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(maxConcurrentRequestsPerTunnel)
|
||||
|
||||
timerRetries := prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: TunnelSubsystem,
|
||||
Name: "timer_retries",
|
||||
Help: "Unacknowledged heart beats count",
|
||||
})
|
||||
prometheus.MustRegister(timerRetries)
|
||||
|
||||
serverLocations := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: TunnelSubsystem,
|
||||
Name: "server_locations",
|
||||
Help: "Where each tunnel is connected to. 1 means current location, 0 means previous locations.",
|
||||
},
|
||||
[]string{"connection_id", "location"},
|
||||
)
|
||||
prometheus.MustRegister(serverLocations)
|
||||
|
||||
rpcFail := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: TunnelSubsystem,
|
||||
Name: "tunnel_rpc_fail",
|
||||
Help: "Count of RPC connection errors by type",
|
||||
},
|
||||
[]string{"error", "rpcName"},
|
||||
)
|
||||
prometheus.MustRegister(rpcFail)
|
||||
|
||||
registerFail := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: TunnelSubsystem,
|
||||
Name: "tunnel_register_fail",
|
||||
Help: "Count of tunnel registration errors by type",
|
||||
},
|
||||
[]string{"error", "rpcName"},
|
||||
)
|
||||
prometheus.MustRegister(registerFail)
|
||||
|
||||
userHostnamesCounts := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: TunnelSubsystem,
|
||||
Name: "user_hostnames_counts",
|
||||
Help: "Which user hostnames cloudflared is serving",
|
||||
},
|
||||
[]string{"userHostname"},
|
||||
)
|
||||
prometheus.MustRegister(userHostnamesCounts)
|
||||
|
||||
registerSuccess := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: TunnelSubsystem,
|
||||
Name: "tunnel_register_success",
|
||||
Help: "Count of successful tunnel registrations",
|
||||
},
|
||||
[]string{"rpcName"},
|
||||
)
|
||||
prometheus.MustRegister(registerSuccess)
|
||||
|
||||
return &tunnelMetrics{
|
||||
timerRetries: timerRetries,
|
||||
serverLocations: serverLocations,
|
||||
oldServerLocations: make(map[string]string),
|
||||
muxerMetrics: newMuxerMetrics(),
|
||||
tunnelsHA: newTunnelsForHA(),
|
||||
regSuccess: registerSuccess,
|
||||
regFail: registerFail,
|
||||
rpcFail: rpcFail,
|
||||
userHostnamesCounts: userHostnamesCounts,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tunnelMetrics) updateMuxerMetrics(connectionID string, metrics *h2mux.MuxerMetrics) {
|
||||
t.muxerMetrics.update(connectionID, metrics)
|
||||
}
|
||||
|
||||
func (t *tunnelMetrics) registerServerLocation(connectionID, loc string) {
|
||||
t.locationLock.Lock()
|
||||
defer t.locationLock.Unlock()
|
||||
if oldLoc, ok := t.oldServerLocations[connectionID]; ok && oldLoc == loc {
|
||||
return
|
||||
} else if ok {
|
||||
t.serverLocations.WithLabelValues(connectionID, oldLoc).Dec()
|
||||
}
|
||||
t.serverLocations.WithLabelValues(connectionID, loc).Inc()
|
||||
t.oldServerLocations[connectionID] = loc
|
||||
}
|
||||
|
||||
var tunnelMetricsInternal struct {
|
||||
sync.Once
|
||||
metrics *tunnelMetrics
|
||||
}
|
||||
|
||||
func newTunnelMetrics() *tunnelMetrics {
|
||||
tunnelMetricsInternal.Do(func() {
|
||||
tunnelMetricsInternal.metrics = initTunnelMetrics()
|
||||
})
|
||||
return tunnelMetricsInternal.metrics
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
const (
|
||||
LogFieldLocation = "location"
|
||||
observerChannelBufferSize = 16
|
||||
)
|
||||
|
||||
type Observer struct {
|
||||
log *zerolog.Logger
|
||||
logTransport *zerolog.Logger
|
||||
metrics *tunnelMetrics
|
||||
tunnelEventChan chan Event
|
||||
uiEnabled bool
|
||||
addSinkChan chan EventSink
|
||||
}
|
||||
|
||||
type EventSink interface {
|
||||
OnTunnelEvent(event Event)
|
||||
}
|
||||
|
||||
func NewObserver(log, logTransport *zerolog.Logger, uiEnabled bool) *Observer {
|
||||
o := &Observer{
|
||||
log: log,
|
||||
logTransport: logTransport,
|
||||
metrics: newTunnelMetrics(),
|
||||
uiEnabled: uiEnabled,
|
||||
tunnelEventChan: make(chan Event, observerChannelBufferSize),
|
||||
addSinkChan: make(chan EventSink, observerChannelBufferSize),
|
||||
}
|
||||
go o.dispatchEvents()
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *Observer) RegisterSink(sink EventSink) {
|
||||
o.addSinkChan <- sink
|
||||
}
|
||||
|
||||
func (o *Observer) logServerInfo(connIndex uint8, location, msg string) {
|
||||
o.sendEvent(Event{Index: connIndex, EventType: Connected, Location: location})
|
||||
o.log.Info().
|
||||
Uint8(LogFieldConnIndex, connIndex).
|
||||
Str(LogFieldLocation, location).
|
||||
Msg(msg)
|
||||
o.metrics.registerServerLocation(uint8ToString(connIndex), location)
|
||||
}
|
||||
|
||||
func (o *Observer) logTrialHostname(registration *tunnelpogs.TunnelRegistration) error {
|
||||
// Print out the user's trial zone URL in a nice box (if they requested and got one and UI flag is not set)
|
||||
if !o.uiEnabled {
|
||||
if registrationURL, err := url.Parse(registration.Url); err == nil {
|
||||
for _, line := range asciiBox(trialZoneMsg(registrationURL.String()), 2) {
|
||||
o.log.Info().Msg(line)
|
||||
}
|
||||
} else {
|
||||
o.log.Error().Msg("Failed to connect tunnel, please try again.")
|
||||
return fmt.Errorf("empty URL in response from Cloudflare edge")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Print out the given lines in a nice ASCII box.
|
||||
func asciiBox(lines []string, padding int) (box []string) {
|
||||
maxLen := maxLen(lines)
|
||||
spacer := strings.Repeat(" ", padding)
|
||||
|
||||
border := "+" + strings.Repeat("-", maxLen+(padding*2)) + "+"
|
||||
|
||||
box = append(box, border)
|
||||
for _, line := range lines {
|
||||
box = append(box, "|"+spacer+line+strings.Repeat(" ", maxLen-len(line))+spacer+"|")
|
||||
}
|
||||
box = append(box, border)
|
||||
return
|
||||
}
|
||||
|
||||
func maxLen(lines []string) int {
|
||||
max := 0
|
||||
for _, line := range lines {
|
||||
if len(line) > max {
|
||||
max = len(line)
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
func trialZoneMsg(url string) []string {
|
||||
return []string{
|
||||
"Your free tunnel has started! Visit it:",
|
||||
" " + url,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Observer) sendRegisteringEvent(connIndex uint8) {
|
||||
o.sendEvent(Event{Index: connIndex, EventType: RegisteringTunnel})
|
||||
}
|
||||
|
||||
func (o *Observer) sendConnectedEvent(connIndex uint8, location string) {
|
||||
o.sendEvent(Event{Index: connIndex, EventType: Connected, Location: location})
|
||||
}
|
||||
|
||||
func (o *Observer) sendURL(url string) {
|
||||
o.sendEvent(Event{EventType: SetURL, URL: url})
|
||||
}
|
||||
|
||||
func (o *Observer) SendReconnect(connIndex uint8) {
|
||||
o.sendEvent(Event{Index: connIndex, EventType: Reconnecting})
|
||||
}
|
||||
|
||||
func (o *Observer) sendUnregisteringEvent(connIndex uint8) {
|
||||
o.sendEvent(Event{Index: connIndex, EventType: Unregistering})
|
||||
}
|
||||
|
||||
func (o *Observer) SendDisconnect(connIndex uint8) {
|
||||
o.sendEvent(Event{Index: connIndex, EventType: Disconnected})
|
||||
}
|
||||
|
||||
func (o *Observer) sendEvent(e Event) {
|
||||
select {
|
||||
case o.tunnelEventChan <- e:
|
||||
break
|
||||
default:
|
||||
o.log.Warn().Msg("observer channel buffer is full")
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Observer) dispatchEvents() {
|
||||
var sinks []EventSink
|
||||
for {
|
||||
select {
|
||||
case sink := <-o.addSinkChan:
|
||||
sinks = append(sinks, sink)
|
||||
case evt := <-o.tunnelEventChan:
|
||||
for _, sink := range sinks {
|
||||
sink.OnTunnelEvent(evt)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type EventSinkFunc func(event Event)
|
||||
|
||||
func (f EventSinkFunc) OnTunnelEvent(event Event) {
|
||||
f(event)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRegisterServerLocation(t *testing.T) {
|
||||
m := newTunnelMetrics()
|
||||
tunnels := 20
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(tunnels)
|
||||
for i := 0; i < tunnels; i++ {
|
||||
go func(i int) {
|
||||
id := strconv.Itoa(i)
|
||||
m.registerServerLocation(id, "LHR")
|
||||
wg.Done()
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
for i := 0; i < tunnels; i++ {
|
||||
id := strconv.Itoa(i)
|
||||
assert.Equal(t, "LHR", m.oldServerLocations[id])
|
||||
}
|
||||
|
||||
wg.Add(tunnels)
|
||||
for i := 0; i < tunnels; i++ {
|
||||
go func(i int) {
|
||||
id := strconv.Itoa(i)
|
||||
m.registerServerLocation(id, "AUS")
|
||||
wg.Done()
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
for i := 0; i < tunnels; i++ {
|
||||
id := strconv.Itoa(i)
|
||||
assert.Equal(t, "AUS", m.oldServerLocations[id])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestObserverEventsDontBlock(t *testing.T) {
|
||||
observer := NewObserver(&log, &log, false)
|
||||
var mu sync.Mutex
|
||||
observer.RegisterSink(EventSinkFunc(func(_ Event) {
|
||||
// callback will block if lock is already held
|
||||
mu.Lock()
|
||||
mu.Unlock()
|
||||
}))
|
||||
|
||||
timeout := time.AfterFunc(5*time.Second, func() {
|
||||
mu.Unlock() // release the callback on timer expiration
|
||||
t.Fatal("observer is blocked")
|
||||
})
|
||||
|
||||
mu.Lock() // block the callback
|
||||
for i := 0; i < 2*observerChannelBufferSize; i++ {
|
||||
observer.sendRegisteringEvent(0)
|
||||
}
|
||||
if pending := timeout.Stop(); pending {
|
||||
// release the callback if timer hasn't expired yet
|
||||
mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type eventCollectorSink struct {
|
||||
observedEvents []Event
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (s *eventCollectorSink) OnTunnelEvent(event Event) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.observedEvents = append(s.observedEvents, event)
|
||||
}
|
||||
|
||||
func (s *eventCollectorSink) assertSawEvent(t *testing.T, event Event) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
assert.Contains(t, s.observedEvents, event)
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
const (
|
||||
AvailableProtocolFlagMessage = "Available protocols: http2 - Go's implementation, h2mux - Cloudflare's implementation of HTTP/2, and auto - automatically select between http2 and h2mux"
|
||||
// edgeH2muxTLSServerName is the server name to establish h2mux connection with edge
|
||||
edgeH2muxTLSServerName = "cftunnel.com"
|
||||
// edgeH2TLSServerName is the server name to establish http2 connection with edge
|
||||
edgeH2TLSServerName = "h2.cftunnel.com"
|
||||
// threshold to switch back to h2mux when the user intentionally pick --protocol http2
|
||||
explicitHTTP2FallbackThreshold = -1
|
||||
autoSelectFlag = "auto"
|
||||
)
|
||||
|
||||
var (
|
||||
ProtocolList = []Protocol{H2mux, HTTP2}
|
||||
)
|
||||
|
||||
type Protocol int64
|
||||
|
||||
const (
|
||||
H2mux Protocol = iota
|
||||
HTTP2
|
||||
)
|
||||
|
||||
func (p Protocol) ServerName() string {
|
||||
switch p {
|
||||
case H2mux:
|
||||
return edgeH2muxTLSServerName
|
||||
case HTTP2:
|
||||
return edgeH2TLSServerName
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback returns the fallback protocol and whether the protocol has a fallback
|
||||
func (p Protocol) fallback() (Protocol, bool) {
|
||||
switch p {
|
||||
case H2mux:
|
||||
return 0, false
|
||||
case HTTP2:
|
||||
return H2mux, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func (p Protocol) String() string {
|
||||
switch p {
|
||||
case H2mux:
|
||||
return "h2mux"
|
||||
case HTTP2:
|
||||
return "http2"
|
||||
default:
|
||||
return fmt.Sprintf("unknown protocol")
|
||||
}
|
||||
}
|
||||
|
||||
type ProtocolSelector interface {
|
||||
Current() Protocol
|
||||
Fallback() (Protocol, bool)
|
||||
}
|
||||
|
||||
type staticProtocolSelector struct {
|
||||
current Protocol
|
||||
}
|
||||
|
||||
func (s *staticProtocolSelector) Current() Protocol {
|
||||
return s.current
|
||||
}
|
||||
|
||||
func (s *staticProtocolSelector) Fallback() (Protocol, bool) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
type autoProtocolSelector struct {
|
||||
lock sync.RWMutex
|
||||
current Protocol
|
||||
switchThrehold int32
|
||||
fetchFunc PercentageFetcher
|
||||
refreshAfter time.Time
|
||||
ttl time.Duration
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
func newAutoProtocolSelector(
|
||||
current Protocol,
|
||||
switchThrehold int32,
|
||||
fetchFunc PercentageFetcher,
|
||||
ttl time.Duration,
|
||||
log *zerolog.Logger,
|
||||
) *autoProtocolSelector {
|
||||
return &autoProtocolSelector{
|
||||
current: current,
|
||||
switchThrehold: switchThrehold,
|
||||
fetchFunc: fetchFunc,
|
||||
refreshAfter: time.Now().Add(ttl),
|
||||
ttl: ttl,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *autoProtocolSelector) Current() Protocol {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
if time.Now().Before(s.refreshAfter) {
|
||||
return s.current
|
||||
}
|
||||
|
||||
percentage, err := s.fetchFunc()
|
||||
if err != nil {
|
||||
s.log.Err(err).Msg("Failed to refresh protocol")
|
||||
return s.current
|
||||
}
|
||||
|
||||
if s.switchThrehold < percentage {
|
||||
s.current = HTTP2
|
||||
} else {
|
||||
s.current = H2mux
|
||||
}
|
||||
s.refreshAfter = time.Now().Add(s.ttl)
|
||||
return s.current
|
||||
}
|
||||
|
||||
func (s *autoProtocolSelector) Fallback() (Protocol, bool) {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
return s.current.fallback()
|
||||
}
|
||||
|
||||
type PercentageFetcher func() (int32, error)
|
||||
|
||||
func NewProtocolSelector(
|
||||
protocolFlag string,
|
||||
namedTunnel *NamedTunnelConfig,
|
||||
fetchFunc PercentageFetcher,
|
||||
ttl time.Duration,
|
||||
log *zerolog.Logger,
|
||||
) (ProtocolSelector, error) {
|
||||
if namedTunnel == nil {
|
||||
return &staticProtocolSelector{
|
||||
current: H2mux,
|
||||
}, nil
|
||||
}
|
||||
if protocolFlag == H2mux.String() {
|
||||
return &staticProtocolSelector{
|
||||
current: H2mux,
|
||||
}, nil
|
||||
}
|
||||
|
||||
http2Percentage, err := fetchFunc()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if protocolFlag == HTTP2.String() {
|
||||
if http2Percentage < 0 {
|
||||
return newAutoProtocolSelector(H2mux, explicitHTTP2FallbackThreshold, fetchFunc, ttl, log), nil
|
||||
}
|
||||
return newAutoProtocolSelector(HTTP2, explicitHTTP2FallbackThreshold, fetchFunc, ttl, log), nil
|
||||
}
|
||||
|
||||
if protocolFlag != autoSelectFlag {
|
||||
return nil, fmt.Errorf("Unknown protocol %s, %s", protocolFlag, AvailableProtocolFlagMessage)
|
||||
}
|
||||
threshold := switchThreshold(namedTunnel.Credentials.AccountTag)
|
||||
if threshold < http2Percentage {
|
||||
return newAutoProtocolSelector(HTTP2, threshold, fetchFunc, ttl, log), nil
|
||||
}
|
||||
return newAutoProtocolSelector(H2mux, threshold, fetchFunc, ttl, log), nil
|
||||
}
|
||||
|
||||
func switchThreshold(accountTag string) int32 {
|
||||
h := fnv.New32a()
|
||||
_, _ = h.Write([]byte(accountTag))
|
||||
return int32(h.Sum32() % 100)
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
testNoTTL = 0
|
||||
)
|
||||
|
||||
var (
|
||||
testNamedTunnelConfig = &NamedTunnelConfig{
|
||||
Credentials: Credentials{
|
||||
AccountTag: "testAccountTag",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func mockFetcher(percentage int32) PercentageFetcher {
|
||||
return func() (int32, error) {
|
||||
return percentage, nil
|
||||
}
|
||||
}
|
||||
|
||||
func mockFetcherWithError() PercentageFetcher {
|
||||
return func() (int32, error) {
|
||||
return 0, fmt.Errorf("failed to fetch precentage")
|
||||
}
|
||||
}
|
||||
|
||||
type dynamicMockFetcher struct {
|
||||
percentage int32
|
||||
err error
|
||||
}
|
||||
|
||||
func (dmf *dynamicMockFetcher) fetch() PercentageFetcher {
|
||||
return func() (int32, error) {
|
||||
if dmf.err != nil {
|
||||
return 0, dmf.err
|
||||
}
|
||||
return dmf.percentage, nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewProtocolSelector(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
protocol string
|
||||
expectedProtocol Protocol
|
||||
hasFallback bool
|
||||
expectedFallback Protocol
|
||||
namedTunnelConfig *NamedTunnelConfig
|
||||
fetchFunc PercentageFetcher
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "classic tunnel",
|
||||
protocol: "h2mux",
|
||||
expectedProtocol: H2mux,
|
||||
namedTunnelConfig: nil,
|
||||
},
|
||||
{
|
||||
name: "named tunnel over h2mux",
|
||||
protocol: "h2mux",
|
||||
expectedProtocol: H2mux,
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
},
|
||||
{
|
||||
name: "named tunnel over http2",
|
||||
protocol: "http2",
|
||||
expectedProtocol: HTTP2,
|
||||
hasFallback: true,
|
||||
expectedFallback: H2mux,
|
||||
fetchFunc: mockFetcher(0),
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
},
|
||||
{
|
||||
name: "named tunnel http2 disabled",
|
||||
protocol: "http2",
|
||||
expectedProtocol: H2mux,
|
||||
fetchFunc: mockFetcher(-1),
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
},
|
||||
{
|
||||
name: "named tunnel auto all http2 disabled",
|
||||
protocol: "auto",
|
||||
expectedProtocol: H2mux,
|
||||
fetchFunc: mockFetcher(-1),
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
},
|
||||
{
|
||||
name: "named tunnel auto to h2mux",
|
||||
protocol: "auto",
|
||||
expectedProtocol: H2mux,
|
||||
fetchFunc: mockFetcher(0),
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
},
|
||||
{
|
||||
name: "named tunnel auto to http2",
|
||||
protocol: "auto",
|
||||
expectedProtocol: HTTP2,
|
||||
hasFallback: true,
|
||||
expectedFallback: H2mux,
|
||||
fetchFunc: mockFetcher(100),
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
},
|
||||
{
|
||||
// None named tunnel can only use h2mux, so specifying an unknown protocol is not an error
|
||||
name: "classic tunnel unknown protocol",
|
||||
protocol: "unknown",
|
||||
expectedProtocol: H2mux,
|
||||
},
|
||||
{
|
||||
name: "named tunnel unknown protocol",
|
||||
protocol: "unknown",
|
||||
fetchFunc: mockFetcher(100),
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "named tunnel fetch error",
|
||||
protocol: "unknown",
|
||||
fetchFunc: mockFetcherWithError(),
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
selector, err := NewProtocolSelector(test.protocol, test.namedTunnelConfig, test.fetchFunc, testNoTTL, &log)
|
||||
if test.wantErr {
|
||||
assert.Error(t, err, fmt.Sprintf("test %s failed", test.name))
|
||||
} else {
|
||||
assert.NoError(t, err, fmt.Sprintf("test %s failed", test.name))
|
||||
assert.Equal(t, test.expectedProtocol, selector.Current(), fmt.Sprintf("test %s failed", test.name))
|
||||
fallback, ok := selector.Fallback()
|
||||
assert.Equal(t, test.hasFallback, ok, fmt.Sprintf("test %s failed", test.name))
|
||||
if test.hasFallback {
|
||||
assert.Equal(t, test.expectedFallback, fallback, fmt.Sprintf("test %s failed", test.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoProtocolSelectorRefresh(t *testing.T) {
|
||||
fetcher := dynamicMockFetcher{}
|
||||
selector, err := NewProtocolSelector("auto", testNamedTunnelConfig, fetcher.fetch(), testNoTTL, &log)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, H2mux, selector.Current())
|
||||
|
||||
fetcher.percentage = 100
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
|
||||
fetcher.percentage = 0
|
||||
assert.Equal(t, H2mux, selector.Current())
|
||||
|
||||
fetcher.percentage = 100
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
|
||||
fetcher.err = fmt.Errorf("failed to fetch")
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
|
||||
fetcher.percentage = -1
|
||||
fetcher.err = nil
|
||||
assert.Equal(t, H2mux, selector.Current())
|
||||
|
||||
fetcher.percentage = 0
|
||||
assert.Equal(t, H2mux, selector.Current())
|
||||
|
||||
fetcher.percentage = 100
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
}
|
||||
|
||||
func TestHTTP2ProtocolSelectorRefresh(t *testing.T) {
|
||||
fetcher := dynamicMockFetcher{}
|
||||
selector, err := NewProtocolSelector("http2", testNamedTunnelConfig, fetcher.fetch(), testNoTTL, &log)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
|
||||
fetcher.percentage = 100
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
|
||||
fetcher.percentage = 0
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
|
||||
fetcher.err = fmt.Errorf("failed to fetch")
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
|
||||
fetcher.percentage = -1
|
||||
fetcher.err = nil
|
||||
assert.Equal(t, H2mux, selector.Current())
|
||||
|
||||
fetcher.percentage = 0
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
|
||||
fetcher.percentage = 100
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
|
||||
fetcher.percentage = -1
|
||||
assert.Equal(t, H2mux, selector.Current())
|
||||
}
|
||||
|
||||
func TestProtocolSelectorRefreshTTL(t *testing.T) {
|
||||
fetcher := dynamicMockFetcher{percentage: 100}
|
||||
selector, err := NewProtocolSelector("auto", testNamedTunnelConfig, fetcher.fetch(), time.Hour, &log)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
|
||||
fetcher.percentage = 0
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
}
|
||||
+300
-30
@@ -3,47 +3,317 @@ package connection
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
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"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"zombiezen.com/go/capnproto2/rpc"
|
||||
)
|
||||
|
||||
// NewRPCClient creates and returns a new RPC client, which will communicate
|
||||
// using a stream on the given muxer
|
||||
func NewRPCClient(
|
||||
type tunnelServerClient struct {
|
||||
client tunnelpogs.TunnelServer_PogsClient
|
||||
transport rpc.Transport
|
||||
}
|
||||
|
||||
// NewTunnelRPCClient creates and returns a new RPC client, which will communicate using a stream on the given muxer.
|
||||
// This method is exported for supervisor to call Authenticate RPC
|
||||
func NewTunnelServerClient(
|
||||
ctx context.Context,
|
||||
muxer *h2mux.Muxer,
|
||||
logger logger.Service,
|
||||
openStreamTimeout time.Duration,
|
||||
) (client tunnelpogs.TunnelServer_PogsClient, err error) {
|
||||
openStreamCtx, openStreamCancel := context.WithTimeout(ctx, openStreamTimeout)
|
||||
defer openStreamCancel()
|
||||
stream, err := muxer.OpenRPCStream(openStreamCtx)
|
||||
stream io.ReadWriteCloser,
|
||||
log *zerolog.Logger,
|
||||
) *tunnelServerClient {
|
||||
transport := tunnelrpc.NewTransportLogger(log, rpc.StreamTransport(stream))
|
||||
conn := rpc.NewConn(
|
||||
transport,
|
||||
tunnelrpc.ConnLog(log),
|
||||
)
|
||||
registrationClient := tunnelpogs.RegistrationServer_PogsClient{Client: conn.Bootstrap(ctx), Conn: conn}
|
||||
return &tunnelServerClient{
|
||||
client: tunnelpogs.TunnelServer_PogsClient{RegistrationServer_PogsClient: registrationClient, Client: conn.Bootstrap(ctx), Conn: conn},
|
||||
transport: transport,
|
||||
}
|
||||
}
|
||||
|
||||
func (tsc *tunnelServerClient) Authenticate(ctx context.Context, classicTunnel *ClassicTunnelConfig, registrationOptions *tunnelpogs.RegistrationOptions) (tunnelpogs.AuthOutcome, error) {
|
||||
authResp, err := tsc.client.Authenticate(ctx, classicTunnel.OriginCert, classicTunnel.Hostname, registrationOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return authResp.Outcome(), nil
|
||||
}
|
||||
|
||||
func (tsc *tunnelServerClient) Close() {
|
||||
// Closing the client will also close the connection
|
||||
_ = tsc.client.Close()
|
||||
_ = tsc.transport.Close()
|
||||
}
|
||||
|
||||
type NamedTunnelRPCClient interface {
|
||||
RegisterConnection(
|
||||
c context.Context,
|
||||
config *NamedTunnelConfig,
|
||||
options *tunnelpogs.ConnectionOptions,
|
||||
connIndex uint8,
|
||||
observer *Observer,
|
||||
) error
|
||||
GracefulShutdown(ctx context.Context, gracePeriod time.Duration)
|
||||
Close()
|
||||
}
|
||||
|
||||
type registrationServerClient struct {
|
||||
client tunnelpogs.RegistrationServer_PogsClient
|
||||
transport rpc.Transport
|
||||
}
|
||||
|
||||
func newRegistrationRPCClient(
|
||||
ctx context.Context,
|
||||
stream io.ReadWriteCloser,
|
||||
log *zerolog.Logger,
|
||||
) NamedTunnelRPCClient {
|
||||
transport := tunnelrpc.NewTransportLogger(log, rpc.StreamTransport(stream))
|
||||
conn := rpc.NewConn(
|
||||
transport,
|
||||
tunnelrpc.ConnLog(log),
|
||||
)
|
||||
return ®istrationServerClient{
|
||||
client: tunnelpogs.RegistrationServer_PogsClient{Client: conn.Bootstrap(ctx), Conn: conn},
|
||||
transport: transport,
|
||||
}
|
||||
}
|
||||
|
||||
func (rsc *registrationServerClient) RegisterConnection(
|
||||
ctx context.Context,
|
||||
config *NamedTunnelConfig,
|
||||
options *tunnelpogs.ConnectionOptions,
|
||||
connIndex uint8,
|
||||
observer *Observer,
|
||||
) error {
|
||||
conn, err := rsc.client.RegisterConnection(
|
||||
ctx,
|
||||
config.Credentials.Auth(),
|
||||
config.Credentials.TunnelID,
|
||||
connIndex,
|
||||
options,
|
||||
)
|
||||
if err != nil {
|
||||
if err.Error() == DuplicateConnectionError {
|
||||
observer.metrics.regFail.WithLabelValues("dup_edge_conn", "registerConnection").Inc()
|
||||
return errDuplicationConnection
|
||||
}
|
||||
observer.metrics.regFail.WithLabelValues("server_error", "registerConnection").Inc()
|
||||
return serverRegistrationErrorFromRPC(err)
|
||||
}
|
||||
|
||||
observer.metrics.regSuccess.WithLabelValues("registerConnection").Inc()
|
||||
|
||||
observer.logServerInfo(connIndex, conn.Location, fmt.Sprintf("Connection %s registered", conn.UUID))
|
||||
observer.sendConnectedEvent(connIndex, conn.Location)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rsc *registrationServerClient) GracefulShutdown(ctx context.Context, gracePeriod time.Duration) {
|
||||
ctx, cancel := context.WithTimeout(ctx, gracePeriod)
|
||||
defer cancel()
|
||||
_ = rsc.client.UnregisterConnection(ctx)
|
||||
}
|
||||
|
||||
func (rsc *registrationServerClient) Close() {
|
||||
// Closing the client will also close the connection
|
||||
_ = rsc.client.Close()
|
||||
// Closing the transport also closes the stream
|
||||
_ = rsc.transport.Close()
|
||||
}
|
||||
|
||||
type rpcName string
|
||||
|
||||
const (
|
||||
register rpcName = "register"
|
||||
reconnect rpcName = "reconnect"
|
||||
unregister rpcName = "unregister"
|
||||
authenticate rpcName = " authenticate"
|
||||
)
|
||||
|
||||
func (h *h2muxConnection) registerTunnel(ctx context.Context, credentialSetter CredentialManager, classicTunnel *ClassicTunnelConfig, registrationOptions *tunnelpogs.RegistrationOptions) error {
|
||||
h.observer.sendRegisteringEvent(registrationOptions.ConnectionID)
|
||||
|
||||
stream, err := h.newRPCStream(ctx, register)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rpcClient := NewTunnelServerClient(ctx, stream, h.observer.log)
|
||||
defer rpcClient.Close()
|
||||
|
||||
_ = h.logServerInfo(ctx, rpcClient)
|
||||
registration := rpcClient.client.RegisterTunnel(
|
||||
ctx,
|
||||
classicTunnel.OriginCert,
|
||||
classicTunnel.Hostname,
|
||||
registrationOptions,
|
||||
)
|
||||
if registrationErr := registration.DeserializeError(); registrationErr != nil {
|
||||
// RegisterTunnel RPC failure
|
||||
return h.processRegisterTunnelError(registrationErr, register)
|
||||
}
|
||||
|
||||
// Send free tunnel URL to UI
|
||||
h.observer.sendURL(registration.Url)
|
||||
credentialSetter.SetEventDigest(h.connIndex, registration.EventDigest)
|
||||
return h.processRegistrationSuccess(registration, register, credentialSetter, classicTunnel)
|
||||
}
|
||||
|
||||
type CredentialManager interface {
|
||||
ReconnectToken() ([]byte, error)
|
||||
EventDigest(connID uint8) ([]byte, error)
|
||||
SetEventDigest(connID uint8, digest []byte)
|
||||
ConnDigest(connID uint8) ([]byte, error)
|
||||
SetConnDigest(connID uint8, digest []byte)
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) processRegistrationSuccess(
|
||||
registration *tunnelpogs.TunnelRegistration,
|
||||
name rpcName,
|
||||
credentialManager CredentialManager, classicTunnel *ClassicTunnelConfig,
|
||||
) error {
|
||||
for _, logLine := range registration.LogLines {
|
||||
h.observer.log.Info().Msg(logLine)
|
||||
}
|
||||
|
||||
if registration.TunnelID != "" {
|
||||
h.observer.metrics.tunnelsHA.AddTunnelID(h.connIndex, registration.TunnelID)
|
||||
h.observer.log.Info().Msgf("Each HA connection's tunnel IDs: %v", h.observer.metrics.tunnelsHA.String())
|
||||
}
|
||||
|
||||
// Print out the user's trial zone URL in a nice box (if they requested and got one and UI flag is not set)
|
||||
if classicTunnel.IsTrialZone() {
|
||||
err := h.observer.logTrialHostname(registration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
credentialManager.SetConnDigest(h.connIndex, registration.ConnDigest)
|
||||
h.observer.metrics.userHostnamesCounts.WithLabelValues(registration.Url).Inc()
|
||||
|
||||
h.observer.log.Info().Msgf("Route propagating, it may take up to 1 minute for your new route to become functional")
|
||||
h.observer.metrics.regSuccess.WithLabelValues(string(name)).Inc()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) processRegisterTunnelError(err tunnelpogs.TunnelRegistrationError, name rpcName) error {
|
||||
if err.Error() == DuplicateConnectionError {
|
||||
h.observer.metrics.regFail.WithLabelValues("dup_edge_conn", string(name)).Inc()
|
||||
return errDuplicationConnection
|
||||
}
|
||||
h.observer.metrics.regFail.WithLabelValues("server_error", string(name)).Inc()
|
||||
return ServerRegisterTunnelError{
|
||||
Cause: err,
|
||||
Permanent: err.IsPermanent(),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) reconnectTunnel(ctx context.Context, credentialManager CredentialManager, classicTunnel *ClassicTunnelConfig, registrationOptions *tunnelpogs.RegistrationOptions) error {
|
||||
token, err := credentialManager.ReconnectToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
eventDigest, err := credentialManager.EventDigest(h.connIndex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
connDigest, err := credentialManager.ConnDigest(h.connIndex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.observer.log.Debug().Msg("initiating RPC stream to reconnect")
|
||||
stream, err := h.newRPCStream(ctx, register)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rpcClient := NewTunnelServerClient(ctx, stream, h.observer.log)
|
||||
defer rpcClient.Close()
|
||||
|
||||
_ = h.logServerInfo(ctx, rpcClient)
|
||||
registration := rpcClient.client.ReconnectTunnel(
|
||||
ctx,
|
||||
token,
|
||||
eventDigest,
|
||||
connDigest,
|
||||
classicTunnel.Hostname,
|
||||
registrationOptions,
|
||||
)
|
||||
if registrationErr := registration.DeserializeError(); registrationErr != nil {
|
||||
// ReconnectTunnel RPC failure
|
||||
return h.processRegisterTunnelError(registrationErr, reconnect)
|
||||
}
|
||||
return h.processRegistrationSuccess(registration, reconnect, credentialManager, classicTunnel)
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) logServerInfo(ctx context.Context, rpcClient *tunnelServerClient) error {
|
||||
// Request server info without blocking tunnel registration; must use capnp library directly.
|
||||
serverInfoPromise := tunnelrpc.TunnelServer{Client: rpcClient.client.Client}.GetServerInfo(ctx, func(tunnelrpc.TunnelServer_getServerInfo_Params) error {
|
||||
return nil
|
||||
})
|
||||
serverInfoMessage, err := serverInfoPromise.Result().Struct()
|
||||
if err != nil {
|
||||
h.observer.log.Err(err).Msg("Failed to retrieve server information")
|
||||
return err
|
||||
}
|
||||
serverInfo, err := tunnelpogs.UnmarshalServerInfo(serverInfoMessage)
|
||||
if err != nil {
|
||||
h.observer.log.Err(err).Msg("Failed to retrieve server information")
|
||||
return err
|
||||
}
|
||||
h.observer.logServerInfo(h.connIndex, serverInfo.LocationName, "Connection established")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) registerNamedTunnel(
|
||||
ctx context.Context,
|
||||
namedTunnel *NamedTunnelConfig,
|
||||
connOptions *tunnelpogs.ConnectionOptions,
|
||||
) error {
|
||||
stream, err := h.newRPCStream(ctx, register)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rpcClient := h.newRPCClientFunc(ctx, stream, h.observer.log)
|
||||
defer rpcClient.Close()
|
||||
|
||||
if err = rpcClient.RegisterConnection(ctx, namedTunnel, connOptions, h.connIndex, h.observer); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) unregister(isNamedTunnel bool) {
|
||||
h.observer.sendUnregisteringEvent(h.connIndex)
|
||||
|
||||
unregisterCtx, cancel := context.WithTimeout(context.Background(), h.config.GracePeriod)
|
||||
defer cancel()
|
||||
|
||||
stream, err := h.newRPCStream(unregisterCtx, unregister)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer stream.Close()
|
||||
|
||||
if !isRPCStreamResponse(stream.Headers) {
|
||||
stream.Close()
|
||||
err = fmt.Errorf("rpc: bad response headers: %v", stream.Headers)
|
||||
return
|
||||
if isNamedTunnel {
|
||||
rpcClient := h.newRPCClientFunc(unregisterCtx, stream, h.observer.log)
|
||||
defer rpcClient.Close()
|
||||
|
||||
rpcClient.GracefulShutdown(unregisterCtx, h.config.GracePeriod)
|
||||
} else {
|
||||
rpcClient := NewTunnelServerClient(unregisterCtx, stream, h.observer.log)
|
||||
defer rpcClient.Close()
|
||||
|
||||
// gracePeriod is encoded in int64 using capnproto
|
||||
_ = rpcClient.client.UnregisterTunnel(unregisterCtx, h.config.GracePeriod.Nanoseconds())
|
||||
}
|
||||
|
||||
conn := rpc.NewConn(
|
||||
tunnelrpc.NewTransportLogger(logger, rpc.StreamTransport(stream)),
|
||||
tunnelrpc.ConnLog(logger),
|
||||
)
|
||||
client = tunnelpogs.TunnelServer_PogsClient{Client: conn.Bootstrap(ctx), Conn: conn}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func isRPCStreamResponse(headers []h2mux.Header) bool {
|
||||
return len(headers) == 1 &&
|
||||
headers[0].Name == ":status" &&
|
||||
headers[0].Value == "200"
|
||||
h.observer.log.Info().Uint8(LogFieldConnIndex, h.connIndex).Msg("Unregistered tunnel connection")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
// tunnelsForHA maps this cloudflared instance's HA connections to the tunnel IDs they serve.
|
||||
type tunnelsForHA struct {
|
||||
sync.Mutex
|
||||
metrics *prometheus.GaugeVec
|
||||
entries map[uint8]string
|
||||
}
|
||||
|
||||
// NewTunnelsForHA initializes the Prometheus metrics etc for a tunnelsForHA.
|
||||
func newTunnelsForHA() tunnelsForHA {
|
||||
metrics := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "tunnel_ids",
|
||||
Help: "The ID of all tunnels (and their corresponding HA connection ID) running in this instance of cloudflared.",
|
||||
},
|
||||
[]string{"tunnel_id", "ha_conn_id"},
|
||||
)
|
||||
prometheus.MustRegister(metrics)
|
||||
|
||||
return tunnelsForHA{
|
||||
metrics: metrics,
|
||||
entries: make(map[uint8]string),
|
||||
}
|
||||
}
|
||||
|
||||
// Track a new tunnel ID, removing the disconnected tunnel (if any) and update metrics.
|
||||
func (t *tunnelsForHA) AddTunnelID(haConn uint8, tunnelID string) {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
haStr := fmt.Sprintf("%v", haConn)
|
||||
if oldTunnelID, ok := t.entries[haConn]; ok {
|
||||
t.metrics.WithLabelValues(oldTunnelID, haStr).Dec()
|
||||
}
|
||||
t.entries[haConn] = tunnelID
|
||||
t.metrics.WithLabelValues(tunnelID, haStr).Inc()
|
||||
}
|
||||
|
||||
func (t *tunnelsForHA) String() string {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
return fmt.Sprintf("%v", t.entries)
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Client is an interface to talk to any database.
|
||||
//
|
||||
// Currently, the only implementation is SQLClient, but its structure
|
||||
// should be designed to handle a MongoClient or RedisClient in the future.
|
||||
type Client interface {
|
||||
Ping(context.Context) error
|
||||
Submit(context.Context, *Command) (interface{}, error)
|
||||
}
|
||||
|
||||
// NewClient creates a database client based on its URL scheme.
|
||||
func NewClient(ctx context.Context, originURL *url.URL) (Client, error) {
|
||||
return NewSQLClient(ctx, originURL)
|
||||
}
|
||||
|
||||
// Command is a standard, non-vendor format for submitting database commands.
|
||||
//
|
||||
// When determining the scope of this struct, refer to the following litmus test:
|
||||
// Could this (roughly) conform to SQL, Document-based, and Key-value command formats?
|
||||
type Command struct {
|
||||
Statement string `json:"statement"`
|
||||
Arguments Arguments `json:"arguments,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Isolation string `json:"isolation,omitempty"`
|
||||
Timeout time.Duration `json:"timeout,omitempty"`
|
||||
}
|
||||
|
||||
// Validate enforces the contract of Command: non empty statement (both in length and logic),
|
||||
// lowercase mode and isolation, non-zero timeout, and valid Arguments.
|
||||
func (cmd *Command) Validate() error {
|
||||
if cmd.Statement == "" {
|
||||
return fmt.Errorf("cannot provide an empty statement")
|
||||
}
|
||||
|
||||
if strings.Map(func(char rune) rune {
|
||||
if char == ';' || unicode.IsSpace(char) {
|
||||
return -1
|
||||
}
|
||||
return char
|
||||
}, cmd.Statement) == "" {
|
||||
return fmt.Errorf("cannot provide a statement with no logic: '%s'", cmd.Statement)
|
||||
}
|
||||
|
||||
cmd.Mode = strings.ToLower(cmd.Mode)
|
||||
cmd.Isolation = strings.ToLower(cmd.Isolation)
|
||||
|
||||
if cmd.Timeout.Nanoseconds() <= 0 {
|
||||
cmd.Timeout = 24 * time.Hour
|
||||
}
|
||||
|
||||
return cmd.Arguments.Validate()
|
||||
}
|
||||
|
||||
// UnmarshalJSON converts a byte representation of JSON into a Command, which is also validated.
|
||||
func (cmd *Command) UnmarshalJSON(data []byte) error {
|
||||
// Alias is required to avoid infinite recursion from the default UnmarshalJSON.
|
||||
type Alias Command
|
||||
alias := &struct {
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(cmd),
|
||||
}
|
||||
|
||||
err := json.Unmarshal(data, &alias)
|
||||
if err == nil {
|
||||
err = cmd.Validate()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Arguments is a wrapper for either map-based or array-based Command arguments.
|
||||
//
|
||||
// Each field is mutually-exclusive and some Client implementations may not
|
||||
// support both fields (eg. MySQL does not accept named arguments).
|
||||
type Arguments struct {
|
||||
Named map[string]interface{}
|
||||
Positional []interface{}
|
||||
}
|
||||
|
||||
// Validate enforces the contract of Arguments: non nil, mutually exclusive, and no empty or reserved keys.
|
||||
func (args *Arguments) Validate() error {
|
||||
if args.Named == nil {
|
||||
args.Named = map[string]interface{}{}
|
||||
}
|
||||
if args.Positional == nil {
|
||||
args.Positional = []interface{}{}
|
||||
}
|
||||
|
||||
if len(args.Named) > 0 && len(args.Positional) > 0 {
|
||||
return fmt.Errorf("both named and positional arguments cannot be specified: %+v and %+v", args.Named, args.Positional)
|
||||
}
|
||||
|
||||
for key := range args.Named {
|
||||
if key == "" {
|
||||
return fmt.Errorf("named arguments cannot contain an empty key: %+v", args.Named)
|
||||
}
|
||||
if !utf8.ValidString(key) {
|
||||
return fmt.Errorf("named argument does not conform to UTF-8 encoding: %s", key)
|
||||
}
|
||||
if strings.HasPrefix(key, "_") {
|
||||
return fmt.Errorf("named argument cannot start with a reserved keyword '_': %s", key)
|
||||
}
|
||||
if unicode.IsNumber([]rune(key)[0]) {
|
||||
return fmt.Errorf("named argument cannot start with a number: %s", key)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON converts a byte representation of JSON into Arguments, which is also validated.
|
||||
func (args *Arguments) UnmarshalJSON(data []byte) error {
|
||||
var obj interface{}
|
||||
err := json.Unmarshal(data, &obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
named, ok := obj.(map[string]interface{})
|
||||
if ok {
|
||||
args.Named = named
|
||||
} else {
|
||||
positional, ok := obj.([]interface{})
|
||||
if ok {
|
||||
args.Positional = positional
|
||||
} else {
|
||||
return fmt.Errorf("arguments must either be an object {\"0\":\"val\"} or an array [\"val\"]: %s", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
return args.Validate()
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCommandValidateEmpty(t *testing.T) {
|
||||
stmts := []string{
|
||||
"",
|
||||
";",
|
||||
" \n\t",
|
||||
";\n;\t;",
|
||||
}
|
||||
|
||||
for _, stmt := range stmts {
|
||||
cmd := Command{Statement: stmt}
|
||||
|
||||
assert.Error(t, cmd.Validate(), stmt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandValidateMode(t *testing.T) {
|
||||
modes := []string{
|
||||
"",
|
||||
"query",
|
||||
"ExEc",
|
||||
"PREPARE",
|
||||
}
|
||||
|
||||
for _, mode := range modes {
|
||||
cmd := Command{Statement: "Ok", Mode: mode}
|
||||
|
||||
assert.NoError(t, cmd.Validate(), mode)
|
||||
assert.Equal(t, strings.ToLower(mode), cmd.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandValidateIsolation(t *testing.T) {
|
||||
isos := []string{
|
||||
"",
|
||||
"default",
|
||||
"read_committed",
|
||||
"SNAPshot",
|
||||
}
|
||||
|
||||
for _, iso := range isos {
|
||||
cmd := Command{Statement: "Ok", Isolation: iso}
|
||||
|
||||
assert.NoError(t, cmd.Validate(), iso)
|
||||
assert.Equal(t, strings.ToLower(iso), cmd.Isolation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandValidateTimeout(t *testing.T) {
|
||||
cmd := Command{Statement: "Ok", Timeout: 0}
|
||||
|
||||
assert.NoError(t, cmd.Validate())
|
||||
assert.NotZero(t, cmd.Timeout)
|
||||
|
||||
cmd = Command{Statement: "Ok", Timeout: 1 * time.Second}
|
||||
|
||||
assert.NoError(t, cmd.Validate())
|
||||
assert.Equal(t, 1*time.Second, cmd.Timeout)
|
||||
}
|
||||
|
||||
func TestCommandValidateArguments(t *testing.T) {
|
||||
cmd := Command{Statement: "Ok", Arguments: Arguments{
|
||||
Named: map[string]interface{}{"key": "val"},
|
||||
Positional: []interface{}{"val"},
|
||||
}}
|
||||
|
||||
assert.Error(t, cmd.Validate())
|
||||
}
|
||||
|
||||
func TestCommandUnmarshalJSON(t *testing.T) {
|
||||
strs := []string{
|
||||
"{\"statement\":\"Ok\"}",
|
||||
"{\"statement\":\"Ok\",\"arguments\":[0, 3.14, \"apple\"],\"mode\":\"query\"}",
|
||||
"{\"statement\":\"Ok\",\"isolation\":\"read_uncommitted\",\"timeout\":1000}",
|
||||
}
|
||||
|
||||
for _, str := range strs {
|
||||
var cmd Command
|
||||
assert.NoError(t, json.Unmarshal([]byte(str), &cmd), str)
|
||||
}
|
||||
|
||||
strs = []string{
|
||||
"",
|
||||
"\"",
|
||||
"{}",
|
||||
"{\"argument\":{\"key\":\"val\"}}",
|
||||
"{\"statement\":[\"Ok\"]}",
|
||||
}
|
||||
|
||||
for _, str := range strs {
|
||||
var cmd Command
|
||||
assert.Error(t, json.Unmarshal([]byte(str), &cmd), str)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgumentsValidateNotNil(t *testing.T) {
|
||||
args := Arguments{}
|
||||
|
||||
assert.NoError(t, args.Validate())
|
||||
assert.NotNil(t, args.Named)
|
||||
assert.NotNil(t, args.Positional)
|
||||
}
|
||||
|
||||
func TestArgumentsValidateMutuallyExclusive(t *testing.T) {
|
||||
args := []Arguments{
|
||||
Arguments{},
|
||||
Arguments{Named: map[string]interface{}{"key": "val"}},
|
||||
Arguments{Positional: []interface{}{"val"}},
|
||||
}
|
||||
|
||||
for _, arg := range args {
|
||||
assert.NoError(t, arg.Validate())
|
||||
assert.False(t, len(arg.Named) > 0 && len(arg.Positional) > 0)
|
||||
}
|
||||
|
||||
args = []Arguments{
|
||||
Arguments{
|
||||
Named: map[string]interface{}{"key": "val"},
|
||||
Positional: []interface{}{"val"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, arg := range args {
|
||||
assert.Error(t, arg.Validate())
|
||||
assert.True(t, len(arg.Named) > 0 && len(arg.Positional) > 0)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgumentsValidateKeys(t *testing.T) {
|
||||
keys := []string{
|
||||
"",
|
||||
"_",
|
||||
"_key",
|
||||
"1",
|
||||
"1key",
|
||||
"\xf0\x28\x8c\xbc", // non-utf8
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
args := Arguments{Named: map[string]interface{}{key: "val"}}
|
||||
|
||||
assert.Error(t, args.Validate(), key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgumentsUnmarshalJSON(t *testing.T) {
|
||||
strs := []string{
|
||||
"{}",
|
||||
"{\"key\":\"val\"}",
|
||||
"{\"key\":[1, 3.14, {\"key\":\"val\"}]}",
|
||||
"[]",
|
||||
"[\"key\",\"val\"]",
|
||||
"[{}]",
|
||||
}
|
||||
|
||||
for _, str := range strs {
|
||||
var args Arguments
|
||||
assert.NoError(t, json.Unmarshal([]byte(str), &args), str)
|
||||
}
|
||||
|
||||
strs = []string{
|
||||
"",
|
||||
"\"",
|
||||
"1",
|
||||
"\"key\"",
|
||||
"{\"key\",\"val\"}",
|
||||
}
|
||||
|
||||
for _, str := range strs {
|
||||
var args Arguments
|
||||
assert.Error(t, json.Unmarshal([]byte(str), &args), str)
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
"gopkg.in/urfave/cli.v2/altsrc"
|
||||
)
|
||||
|
||||
// Cmd is the entrypoint command for dbconnect.
|
||||
//
|
||||
// The tunnel package is responsible for appending this to tunnel.Commands().
|
||||
func Cmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Category: "Database Connect (ALPHA) - Deprecated",
|
||||
Name: "db-connect",
|
||||
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.
|
||||
|
||||
Connect your database with any of the following commands, you can also try the "playground" without a database:
|
||||
|
||||
cloudflared db-connect --hostname sql.mysite.com --url postgres://user:pass@localhost?sslmode=disable \
|
||||
--auth-domain mysite.cloudflareaccess.com --application-aud my-access-policy-tag
|
||||
cloudflared db-connect --hostname sql-dev.mysite.com --url mysql://localhost --insecure
|
||||
cloudflared db-connect --playground
|
||||
|
||||
Requests should be authenticated using Cloudflare Access, learn more about how to enable it here:
|
||||
|
||||
https://developers.cloudflare.com/access/service-auth/service-token/
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "url",
|
||||
Usage: "URL to the database (eg. postgres://user:pass@localhost?sslmode=disable)",
|
||||
EnvVars: []string{"TUNNEL_URL"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "hostname",
|
||||
Usage: "Hostname to accept commands over HTTPS (eg. sql.mysite.com)",
|
||||
EnvVars: []string{"TUNNEL_HOSTNAME"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "auth-domain",
|
||||
Usage: "Cloudflare Access authentication domain for your account (eg. mysite.cloudflareaccess.com)",
|
||||
EnvVars: []string{"TUNNEL_ACCESS_AUTH_DOMAIN"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "application-aud",
|
||||
Usage: "Cloudflare Access application \"AUD\" to verify JWTs from requests",
|
||||
EnvVars: []string{"TUNNEL_ACCESS_APPLICATION_AUD"},
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "insecure",
|
||||
Usage: "Disable authentication, the database will be open to the Internet",
|
||||
Value: false,
|
||||
EnvVars: []string{"TUNNEL_ACCESS_INSECURE"},
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "playground",
|
||||
Usage: "Run a temporary, in-memory SQLite3 database for testing",
|
||||
Value: false,
|
||||
EnvVars: []string{"TUNNEL_HELLO_WORLD"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "loglevel",
|
||||
Value: "debug", // Make it more verbose than the tunnel default 'info'.
|
||||
EnvVars: []string{"TUNNEL_LOGLEVEL"},
|
||||
Hidden: true,
|
||||
}),
|
||||
},
|
||||
Before: CmdBefore,
|
||||
Action: CmdAction,
|
||||
Hidden: true,
|
||||
}
|
||||
}
|
||||
|
||||
// CmdBefore runs some validation checks before running the command.
|
||||
func CmdBefore(c *cli.Context) error {
|
||||
// Show the help text is no flags are specified.
|
||||
if c.NumFlags() == 0 {
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
|
||||
// Hello-world and playground are synonymous with each other,
|
||||
// unset hello-world to prevent tunnel from initializing the hello package.
|
||||
if c.IsSet("hello-world") {
|
||||
c.Set("playground", "true")
|
||||
c.Set("hello-world", "false")
|
||||
}
|
||||
|
||||
// Unix-socket database urls are supported, but the logic is the same as url.
|
||||
if c.IsSet("unix-socket") {
|
||||
c.Set("url", c.String("unix-socket"))
|
||||
c.Set("unix-socket", "")
|
||||
}
|
||||
|
||||
// When playground mode is enabled, run with an in-memory database.
|
||||
if c.IsSet("playground") {
|
||||
c.Set("url", "sqlite3::memory:?cache=shared")
|
||||
c.Set("insecure", strconv.FormatBool(!c.IsSet("auth-domain") && !c.IsSet("application-aud")))
|
||||
}
|
||||
|
||||
// At this point, insecure configurations are valid.
|
||||
if c.Bool("insecure") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure that secure configurations specify a hostname, domain, and tag for JWT validation.
|
||||
if !c.IsSet("hostname") || !c.IsSet("auth-domain") || !c.IsSet("application-aud") {
|
||||
log.Fatal("must specify --hostname, --auth-domain, and --application-aud unless you want to run in --insecure mode")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CmdAction starts the Proxy and sets the url in cli.Context to point to the Proxy address.
|
||||
func CmdAction(c *cli.Context) error {
|
||||
// STOR-612: sync with context in tunnel daemon.
|
||||
ctx := context.Background()
|
||||
|
||||
var proxy *Proxy
|
||||
var err error
|
||||
if c.Bool("insecure") {
|
||||
proxy, err = NewInsecureProxy(ctx, c.String("url"))
|
||||
} else {
|
||||
proxy, err = NewSecureProxy(ctx, c.String("url"), c.String("auth-domain"), c.String("application-aud"))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
|
||||
listenerC := make(chan net.Listener)
|
||||
defer close(listenerC)
|
||||
|
||||
// Since the Proxy should only talk to the tunnel daemon, find the next available
|
||||
// localhost port and start to listen to requests.
|
||||
go func() {
|
||||
err := proxy.Start(ctx, "127.0.0.1:", listenerC)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Block until the the Proxy is online, retreive its address, and change the url to point to it.
|
||||
// This is effectively "handing over" control to the tunnel package so it can run the tunnel daemon.
|
||||
c.Set("url", "https://"+(<-listenerC).Addr().String())
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
func TestCmd(t *testing.T) {
|
||||
tests := [][]string{
|
||||
{"cloudflared", "db-connect", "--playground"},
|
||||
{"cloudflared", "db-connect", "--playground", "--hostname", "sql.mysite.com"},
|
||||
{"cloudflared", "db-connect", "--url", "sqlite3::memory:?cache=shared", "--insecure"},
|
||||
{"cloudflared", "db-connect", "--url", "sqlite3::memory:?cache=shared", "--hostname", "sql.mysite.com", "--auth-domain", "mysite.cloudflareaccess.com", "--application-aud", "aud"},
|
||||
}
|
||||
|
||||
app := &cli.App{
|
||||
Name: "cloudflared",
|
||||
Commands: []*cli.Command{Cmd()},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
assert.NoError(t, app.Run(test))
|
||||
}
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/hello"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Proxy is an HTTP server that proxies requests to a Client.
|
||||
type Proxy struct {
|
||||
client Client
|
||||
accessValidator *validation.Access
|
||||
logger logger.Service
|
||||
}
|
||||
|
||||
// NewInsecureProxy creates a Proxy that talks to a Client at an origin.
|
||||
//
|
||||
// In insecure mode, the Proxy will allow all Command requests.
|
||||
func NewInsecureProxy(ctx context.Context, origin string) (*Proxy, error) {
|
||||
originURL, err := url.Parse(origin)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "must provide a valid database url")
|
||||
}
|
||||
|
||||
client, err := NewClient(ctx, originURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = client.Ping(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not connect to the database")
|
||||
}
|
||||
|
||||
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.
|
||||
//
|
||||
// In secure mode, the Proxy will reject any Command requests that are
|
||||
// not authenticated by Cloudflare Access with a valid JWT.
|
||||
func NewSecureProxy(ctx context.Context, origin, authDomain, applicationAUD string) (*Proxy, error) {
|
||||
proxy, err := NewInsecureProxy(ctx, origin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
validator, err := validation.NewAccessValidator(ctx, authDomain, authDomain, applicationAUD)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy.accessValidator = validator
|
||||
|
||||
return proxy, err
|
||||
}
|
||||
|
||||
// IsInsecure gets whether the Proxy will accept a Command from any source.
|
||||
func (proxy *Proxy) IsInsecure() bool {
|
||||
return proxy.accessValidator == nil
|
||||
}
|
||||
|
||||
// IsAllowed checks whether a http.Request is allowed to receive data.
|
||||
//
|
||||
// By default, requests must pass through Cloudflare Access for authentication.
|
||||
// If the proxy is explcitly set to insecure mode, all requests will be allowed.
|
||||
func (proxy *Proxy) IsAllowed(r *http.Request, verbose ...bool) bool {
|
||||
if proxy.IsInsecure() {
|
||||
return true
|
||||
}
|
||||
|
||||
// Access and Tunnel should prevent bad JWTs from even reaching the origin,
|
||||
// but validate tokens anyway as an abundance of caution.
|
||||
err := proxy.accessValidator.ValidateRequest(r.Context(), r)
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// 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 {
|
||||
cfRay := proxy.getRayHeader(r)
|
||||
proxy.logger.Infof("dbproxy: Failed JWT authentication: cf-ray: %s %s", cfRay, err)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Start the Proxy at a given address and notify the listener channel when the server is online.
|
||||
func (proxy *Proxy) Start(ctx context.Context, addr string, listenerC chan<- net.Listener) error {
|
||||
// STOR-611: use a seperate listener and consider web socket support.
|
||||
httpListener, err := hello.CreateTLSListener(addr)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not create listener at %s", addr)
|
||||
}
|
||||
|
||||
errC := make(chan error)
|
||||
defer close(errC)
|
||||
|
||||
// Starts the HTTP server and begins to serve requests.
|
||||
go func() {
|
||||
errC <- proxy.httpListen(ctx, httpListener)
|
||||
}()
|
||||
|
||||
// Continually ping the server until it comes online or 10 attempts fail.
|
||||
go func() {
|
||||
var err error
|
||||
for i := 0; i < 10; i++ {
|
||||
_, err = http.Get("http://" + httpListener.Addr().String())
|
||||
|
||||
// Once no error was detected, notify the listener channel and return.
|
||||
if err == nil {
|
||||
listenerC <- httpListener
|
||||
return
|
||||
}
|
||||
|
||||
// Backoff between requests to ping the server.
|
||||
<-time.After(1 * time.Second)
|
||||
}
|
||||
errC <- errors.Wrap(err, "took too long for the http server to start")
|
||||
}()
|
||||
|
||||
return <-errC
|
||||
}
|
||||
|
||||
// httpListen starts the httpServer and blocks until the context closes.
|
||||
func (proxy *Proxy) httpListen(ctx context.Context, listener net.Listener) error {
|
||||
httpServer := &http.Server{
|
||||
Addr: listener.Addr().String(),
|
||||
Handler: proxy.httpRouter(),
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 60 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
httpServer.Close()
|
||||
listener.Close()
|
||||
}()
|
||||
|
||||
return httpServer.Serve(listener)
|
||||
}
|
||||
|
||||
// httpRouter creates a mux.Router for the Proxy.
|
||||
func (proxy *Proxy) httpRouter() *mux.Router {
|
||||
router := mux.NewRouter()
|
||||
|
||||
router.HandleFunc("/ping", proxy.httpPing()).Methods("GET", "HEAD")
|
||||
router.HandleFunc("/submit", proxy.httpSubmit()).Methods("POST")
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
// httpPing tests the connection to the database.
|
||||
//
|
||||
// By default, this endpoint is unauthenticated to allow for health checks.
|
||||
// To enable authentication, Cloudflare Access must be enabled on this route.
|
||||
func (proxy *Proxy) httpPing() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
err := proxy.client.Ping(ctx)
|
||||
|
||||
if err == nil {
|
||||
proxy.httpRespond(w, r, http.StatusOK, "")
|
||||
} else {
|
||||
proxy.httpRespondErr(w, r, http.StatusInternalServerError, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// httpSubmit sends a command to the database and returns its response.
|
||||
//
|
||||
// By default, this endpoint will reject requests that do not pass through Cloudflare Access.
|
||||
// To disable authentication, the --insecure flag must be specified in the command line.
|
||||
func (proxy *Proxy) httpSubmit() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !proxy.IsAllowed(r, true) {
|
||||
proxy.httpRespondErr(w, r, http.StatusForbidden, fmt.Errorf(""))
|
||||
return
|
||||
}
|
||||
|
||||
var cmd Command
|
||||
err := json.NewDecoder(r.Body).Decode(&cmd)
|
||||
if err != nil {
|
||||
proxy.httpRespondErr(w, r, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
data, err := proxy.client.Submit(ctx, &cmd)
|
||||
|
||||
if err != nil {
|
||||
proxy.httpRespondErr(w, r, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-type", "application/json")
|
||||
err = json.NewEncoder(w).Encode(data)
|
||||
if err != nil {
|
||||
proxy.httpRespondErr(w, r, http.StatusInternalServerError, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// httpRespond writes a status code and string response to the response writer.
|
||||
func (proxy *Proxy) httpRespond(w http.ResponseWriter, r *http.Request, status int, message string) {
|
||||
w.WriteHeader(status)
|
||||
|
||||
// Only expose the message detail of the reponse if the request is not HEAD
|
||||
// and the user is authenticated. For example, this prevents an unauthenticated
|
||||
// failed health check from accidentally leaking sensitive information about the Client.
|
||||
if r.Method != http.MethodHead && proxy.IsAllowed(r) {
|
||||
if message == "" {
|
||||
message = http.StatusText(status)
|
||||
}
|
||||
fmt.Fprint(w, message)
|
||||
}
|
||||
}
|
||||
|
||||
// httpRespondErr is similar to httpRespond, except it formats errors to be more friendly.
|
||||
func (proxy *Proxy) httpRespondErr(w http.ResponseWriter, r *http.Request, defaultStatus int, err error) {
|
||||
status, err := httpError(defaultStatus, err)
|
||||
|
||||
proxy.httpRespond(w, r, status, err.Error())
|
||||
if len(err.Error()) > 0 {
|
||||
cfRay := proxy.getRayHeader(r)
|
||||
proxy.logger.Infof("dbproxy: Database proxy error: cf-ray: %s %s", cfRay, 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.
|
||||
func httpError(defaultStatus int, err error) (int, error) {
|
||||
if err == nil {
|
||||
return http.StatusNotImplemented, fmt.Errorf("error expected but found none")
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
return http.StatusBadRequest, fmt.Errorf("request body cannot be empty")
|
||||
}
|
||||
|
||||
if err == context.DeadlineExceeded {
|
||||
return http.StatusRequestTimeout, err
|
||||
}
|
||||
|
||||
_, ok := err.(net.Error)
|
||||
if ok {
|
||||
return http.StatusRequestTimeout, err
|
||||
}
|
||||
|
||||
if err == context.Canceled {
|
||||
// Does not exist in Golang, but would be: http.StatusClientClosedWithoutResponse
|
||||
return 444, err
|
||||
}
|
||||
|
||||
return defaultStatus, err
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewInsecureProxy(t *testing.T) {
|
||||
origins := []string{
|
||||
"",
|
||||
":/",
|
||||
"http://localhost",
|
||||
"tcp://localhost:9000?debug=true",
|
||||
"mongodb://127.0.0.1",
|
||||
}
|
||||
|
||||
for _, origin := range origins {
|
||||
proxy, err := NewInsecureProxy(context.Background(), origin)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, proxy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyIsAllowed(t *testing.T) {
|
||||
proxy := helperNewProxy(t)
|
||||
req := httptest.NewRequest("GET", "https://1.1.1.1/ping", nil)
|
||||
assert.True(t, proxy.IsAllowed(req))
|
||||
|
||||
proxy = helperNewProxy(t, true)
|
||||
req.Header.Set("Cf-access-jwt-assertion", "xxx")
|
||||
assert.False(t, proxy.IsAllowed(req))
|
||||
}
|
||||
|
||||
func TestProxyStart(t *testing.T) {
|
||||
proxy := helperNewProxy(t)
|
||||
ctx := context.Background()
|
||||
listenerC := make(chan net.Listener)
|
||||
|
||||
err := proxy.Start(ctx, "1.1.1.1:", listenerC)
|
||||
assert.Error(t, err)
|
||||
|
||||
err = proxy.Start(ctx, "127.0.0.1:-1", listenerC)
|
||||
assert.Error(t, err)
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 0)
|
||||
defer cancel()
|
||||
|
||||
err = proxy.Start(ctx, "127.0.0.1:", listenerC)
|
||||
assert.IsType(t, http.ErrServerClosed, err)
|
||||
}
|
||||
|
||||
func TestProxyHTTPRouter(t *testing.T) {
|
||||
proxy := helperNewProxy(t)
|
||||
router := proxy.httpRouter()
|
||||
|
||||
tests := []struct {
|
||||
path string
|
||||
method string
|
||||
valid bool
|
||||
}{
|
||||
{"", "GET", false},
|
||||
{"/", "GET", false},
|
||||
{"/ping", "GET", true},
|
||||
{"/ping", "HEAD", true},
|
||||
{"/ping", "POST", false},
|
||||
{"/submit", "POST", true},
|
||||
{"/submit", "GET", false},
|
||||
{"/submit/extra", "POST", false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
match := &mux.RouteMatch{}
|
||||
ok := router.Match(httptest.NewRequest(test.method, "https://1.1.1.1"+test.path, nil), match)
|
||||
|
||||
assert.True(t, ok == test.valid, test.path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyHTTPPing(t *testing.T) {
|
||||
proxy := helperNewProxy(t)
|
||||
|
||||
server := httptest.NewServer(proxy.httpPing())
|
||||
defer server.Close()
|
||||
client := server.Client()
|
||||
|
||||
res, err := client.Get(server.URL)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
assert.Equal(t, int64(2), res.ContentLength)
|
||||
|
||||
res, err = client.Head(server.URL)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
assert.Equal(t, int64(-1), res.ContentLength)
|
||||
}
|
||||
|
||||
func TestProxyHTTPSubmit(t *testing.T) {
|
||||
proxy := helperNewProxy(t)
|
||||
|
||||
server := httptest.NewServer(proxy.httpSubmit())
|
||||
defer server.Close()
|
||||
client := server.Client()
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
status int
|
||||
output string
|
||||
}{
|
||||
{"", http.StatusBadRequest, "request body cannot be empty"},
|
||||
{"{}", http.StatusBadRequest, "cannot provide an empty statement"},
|
||||
{"{\"statement\":\"Ok\"}", http.StatusUnprocessableEntity, "cannot provide invalid sql mode: ''"},
|
||||
{"{\"statement\":\"Ok\",\"mode\":\"query\"}", http.StatusUnprocessableEntity, "near \"Ok\": syntax error"},
|
||||
{"{\"statement\":\"CREATE TABLE t (a INT);\",\"mode\":\"exec\"}", http.StatusOK, "{\"last_insert_id\":0,\"rows_affected\":0}\n"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
res, err := client.Post(server.URL, "application/json", strings.NewReader(test.input))
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.status, res.StatusCode)
|
||||
if res.StatusCode > http.StatusOK {
|
||||
assert.Equal(t, "text/plain; charset=utf-8", res.Header.Get("Content-type"))
|
||||
} else {
|
||||
assert.Equal(t, "application/json", res.Header.Get("Content-type"))
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
defer res.Body.Close()
|
||||
str := string(data)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.output, str)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyHTTPSubmitForbidden(t *testing.T) {
|
||||
proxy := helperNewProxy(t, true)
|
||||
|
||||
server := httptest.NewServer(proxy.httpSubmit())
|
||||
defer server.Close()
|
||||
client := server.Client()
|
||||
|
||||
res, err := client.Get(server.URL)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusForbidden, res.StatusCode)
|
||||
assert.Zero(t, res.ContentLength)
|
||||
}
|
||||
|
||||
func TestProxyHTTPRespond(t *testing.T) {
|
||||
proxy := helperNewProxy(t)
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
proxy.httpRespond(w, r, http.StatusAccepted, "Hello")
|
||||
}))
|
||||
defer server.Close()
|
||||
client := server.Client()
|
||||
|
||||
res, err := client.Get(server.URL)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusAccepted, res.StatusCode)
|
||||
assert.Equal(t, int64(5), res.ContentLength)
|
||||
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
defer res.Body.Close()
|
||||
assert.Equal(t, []byte("Hello"), data)
|
||||
}
|
||||
|
||||
func TestProxyHTTPRespondForbidden(t *testing.T) {
|
||||
proxy := helperNewProxy(t, true)
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
proxy.httpRespond(w, r, http.StatusAccepted, "Hello")
|
||||
}))
|
||||
defer server.Close()
|
||||
client := server.Client()
|
||||
|
||||
res, err := client.Get(server.URL)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusAccepted, res.StatusCode)
|
||||
assert.Equal(t, int64(0), res.ContentLength)
|
||||
}
|
||||
|
||||
func TestHTTPError(t *testing.T) {
|
||||
_, errTimeout := net.DialTimeout("tcp", "127.0.0.1", 0)
|
||||
assert.Error(t, errTimeout)
|
||||
|
||||
tests := []struct {
|
||||
input error
|
||||
status int
|
||||
output error
|
||||
}{
|
||||
{nil, http.StatusNotImplemented, fmt.Errorf("error expected but found none")},
|
||||
{io.EOF, http.StatusBadRequest, fmt.Errorf("request body cannot be empty")},
|
||||
{context.DeadlineExceeded, http.StatusRequestTimeout, nil},
|
||||
{context.Canceled, 444, nil},
|
||||
{errTimeout, http.StatusRequestTimeout, nil},
|
||||
{fmt.Errorf(""), http.StatusInternalServerError, nil},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
status, err := httpError(http.StatusInternalServerError, test.input)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, test.status, status)
|
||||
if test.output == nil {
|
||||
test.output = test.input
|
||||
}
|
||||
assert.Equal(t, test.output, err)
|
||||
}
|
||||
}
|
||||
|
||||
func helperNewProxy(t *testing.T, secure ...bool) *Proxy {
|
||||
t.Helper()
|
||||
|
||||
proxy, err := NewSecureProxy(context.Background(), "file::memory:?cache=shared", "test.cloudflareaccess.com", "")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, proxy)
|
||||
|
||||
if len(secure) == 0 {
|
||||
proxy.accessValidator = nil // Mark as insecure
|
||||
}
|
||||
|
||||
return proxy
|
||||
}
|
||||
@@ -1,318 +0,0 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/xo/dburl"
|
||||
|
||||
// SQL drivers self-register with the database/sql package.
|
||||
// https://github.com/golang/go/wiki/SQLDrivers
|
||||
_ "github.com/denisenkom/go-mssqldb"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"github.com/kshvakov/clickhouse"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
// SQLClient is a Client that talks to a SQL database.
|
||||
type SQLClient struct {
|
||||
Dialect string
|
||||
driver *sqlx.DB
|
||||
}
|
||||
|
||||
// NewSQLClient creates a SQL client based on its URL scheme.
|
||||
func NewSQLClient(ctx context.Context, originURL *url.URL) (Client, error) {
|
||||
res, err := dburl.Parse(originURL.String())
|
||||
if err != nil {
|
||||
helpText := fmt.Sprintf("supported drivers: %+q, see documentation for more details: %s", sql.Drivers(), "https://godoc.org/github.com/xo/dburl")
|
||||
return nil, fmt.Errorf("could not parse sql database url '%s': %s\n%s", originURL, err.Error(), helpText)
|
||||
}
|
||||
|
||||
// Establishes the driver, but does not test the connection.
|
||||
driver, err := sqlx.Open(res.Driver, res.DSN)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open sql driver %s: %s\n%s", res.Driver, err.Error(), res.DSN)
|
||||
}
|
||||
|
||||
// Closes the driver, will occur when the context finishes.
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
driver.Close()
|
||||
}()
|
||||
|
||||
return &SQLClient{driver.DriverName(), driver}, nil
|
||||
}
|
||||
|
||||
// Ping verifies a connection to the database is still alive.
|
||||
func (client *SQLClient) Ping(ctx context.Context) error {
|
||||
return client.driver.PingContext(ctx)
|
||||
}
|
||||
|
||||
// Submit queries or executes a command to the SQL database.
|
||||
func (client *SQLClient) Submit(ctx context.Context, cmd *Command) (interface{}, error) {
|
||||
txx, err := cmd.ValidateSQL(client.Dialect)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, cmd.Timeout)
|
||||
defer cancel()
|
||||
|
||||
var res interface{}
|
||||
|
||||
// Get the next available sql.Conn and submit the Command.
|
||||
err = sqlConn(ctx, client.driver, txx, func(conn *sql.Conn) error {
|
||||
stmt := cmd.Statement
|
||||
args := cmd.Arguments.Positional
|
||||
|
||||
if cmd.Mode == "query" {
|
||||
res, err = sqlQuery(ctx, conn, stmt, args)
|
||||
} else {
|
||||
res, err = sqlExec(ctx, conn, stmt, args)
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// ValidateSQL extends the contract of Command for SQL dialects:
|
||||
// mode is conformed, arguments are []sql.NamedArg, and isolation is a sql.IsolationLevel.
|
||||
//
|
||||
// When the command should not be wrapped in a transaction, *sql.TxOptions and error will both be nil.
|
||||
func (cmd *Command) ValidateSQL(dialect string) (*sql.TxOptions, error) {
|
||||
err := cmd.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mode, err := sqlMode(cmd.Mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Mutates Arguments to only use positional arguments with the type sql.NamedArg.
|
||||
// This is a required by the sql.Driver before submitting arguments.
|
||||
cmd.Arguments.sql(dialect)
|
||||
|
||||
iso, err := sqlIsolation(cmd.Isolation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// When isolation is out-of-range, this is indicative that no
|
||||
// transaction should be executed and sql.TxOptions should be nil.
|
||||
if iso < sql.LevelDefault {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// In query mode, execute the transaction in read-only, unless it's Microsoft SQL
|
||||
// which does not support that type of transaction.
|
||||
readOnly := mode == "query" && dialect != "mssql"
|
||||
|
||||
return &sql.TxOptions{Isolation: iso, ReadOnly: readOnly}, nil
|
||||
}
|
||||
|
||||
// sqlConn gets the next available sql.Conn in the connection pool and runs a function to use it.
|
||||
//
|
||||
// If the transaction options are nil, run the useIt function outside a transaction.
|
||||
// This is potentially an unsafe operation if the command does not clean up its state.
|
||||
func sqlConn(ctx context.Context, driver *sqlx.DB, txx *sql.TxOptions, useIt func(*sql.Conn) error) error {
|
||||
conn, err := driver.Conn(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// If transaction options are specified, begin and defer a rollback to catch errors.
|
||||
var tx *sql.Tx
|
||||
if txx != nil {
|
||||
tx, err = conn.BeginTx(ctx, txx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
}
|
||||
|
||||
err = useIt(conn)
|
||||
|
||||
// Check if useIt was successful and a transaction exists before committing.
|
||||
if err == nil && tx != nil {
|
||||
err = tx.Commit()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// sqlQuery queries rows on a sql.Conn and returns an array of result objects.
|
||||
func sqlQuery(ctx context.Context, conn *sql.Conn, stmt string, args []interface{}) ([]map[string]interface{}, error) {
|
||||
rows, err := conn.QueryContext(ctx, stmt, args...)
|
||||
if err == nil {
|
||||
return sqlRows(rows)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// sqlExec executes a command on a sql.Conn and returns the result of the operation.
|
||||
func sqlExec(ctx context.Context, conn *sql.Conn, stmt string, args []interface{}) (sqlResult, error) {
|
||||
exec, err := conn.ExecContext(ctx, stmt, args...)
|
||||
if err == nil {
|
||||
return sqlResultFrom(exec), nil
|
||||
}
|
||||
return sqlResult{}, err
|
||||
}
|
||||
|
||||
// sql mutates Arguments to contain a positional []sql.NamedArg.
|
||||
//
|
||||
// The actual return type is []interface{} due to the native Golang
|
||||
// function signatures for sql.Exec and sql.Query being generic.
|
||||
func (args *Arguments) sql(dialect string) {
|
||||
result := args.Positional
|
||||
|
||||
for i, val := range result {
|
||||
result[i] = sqlArg("", val, dialect)
|
||||
}
|
||||
|
||||
for key, val := range args.Named {
|
||||
result = append(result, sqlArg(key, val, dialect))
|
||||
}
|
||||
|
||||
args.Positional = result
|
||||
args.Named = map[string]interface{}{}
|
||||
}
|
||||
|
||||
// sqlArg creates a sql.NamedArg from a key-value pair and an optional dialect.
|
||||
//
|
||||
// Certain dialects will need to wrap objects, such as arrays, to conform its driver requirements.
|
||||
func sqlArg(key, val interface{}, dialect string) sql.NamedArg {
|
||||
switch reflect.ValueOf(val).Kind() {
|
||||
|
||||
// PostgreSQL and Clickhouse require arrays to be wrapped before
|
||||
// being inserted into the driver interface.
|
||||
case reflect.Slice, reflect.Array:
|
||||
switch dialect {
|
||||
case "postgres":
|
||||
val = pq.Array(val)
|
||||
case "clickhouse":
|
||||
val = clickhouse.Array(val)
|
||||
}
|
||||
}
|
||||
|
||||
return sql.Named(fmt.Sprint(key), val)
|
||||
}
|
||||
|
||||
// sqlIsolation tries to match a string to a sql.IsolationLevel.
|
||||
func sqlIsolation(str string) (sql.IsolationLevel, error) {
|
||||
if str == "none" {
|
||||
return sql.IsolationLevel(-1), nil
|
||||
}
|
||||
|
||||
for iso := sql.LevelDefault; ; iso++ {
|
||||
if iso > sql.LevelLinearizable {
|
||||
return -1, fmt.Errorf("cannot provide an invalid sql isolation level: '%s'", str)
|
||||
}
|
||||
|
||||
if str == "" || strings.EqualFold(iso.String(), strings.ReplaceAll(str, "_", " ")) {
|
||||
return iso, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sqlMode tries to match a string to a command mode: 'query' or 'exec' for now.
|
||||
func sqlMode(str string) (string, error) {
|
||||
switch str {
|
||||
case "query", "exec":
|
||||
return str, nil
|
||||
default:
|
||||
return "", fmt.Errorf("cannot provide invalid sql mode: '%s'", str)
|
||||
}
|
||||
}
|
||||
|
||||
// sqlRows scans through a SQL result set and returns an array of objects.
|
||||
func sqlRows(rows *sql.Rows) ([]map[string]interface{}, error) {
|
||||
columns, err := rows.Columns()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not extract columns from result")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
types, err := rows.ColumnTypes()
|
||||
if err != nil {
|
||||
// Some drivers do not support type extraction, so fail silently and continue.
|
||||
types = make([]*sql.ColumnType, len(columns))
|
||||
}
|
||||
|
||||
values := make([]interface{}, len(columns))
|
||||
pointers := make([]interface{}, len(columns))
|
||||
|
||||
var results []map[string]interface{}
|
||||
for rows.Next() {
|
||||
for i := range columns {
|
||||
pointers[i] = &values[i]
|
||||
}
|
||||
rows.Scan(pointers...)
|
||||
|
||||
// Convert a row, an array of values, into an object where
|
||||
// each key is the name of its respective column.
|
||||
entry := make(map[string]interface{})
|
||||
for i, col := range columns {
|
||||
entry[col] = sqlValue(values[i], types[i])
|
||||
}
|
||||
results = append(results, entry)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// sqlValue handles special cases where sql.Rows does not return a "human-readable" object.
|
||||
func sqlValue(val interface{}, col *sql.ColumnType) interface{} {
|
||||
bytes, ok := val.([]byte)
|
||||
if ok {
|
||||
// Opportunistically check for embeded JSON and convert it to a first-class object.
|
||||
var embeded interface{}
|
||||
if json.Unmarshal(bytes, &embeded) == nil {
|
||||
return embeded
|
||||
}
|
||||
|
||||
// STOR-604: investigate a way to coerce PostgreSQL arrays '{a, b, ...}' into JSON.
|
||||
// Although easy with strings, it becomes more difficult with special types like INET[].
|
||||
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
// sqlResult is a thin wrapper around sql.Result.
|
||||
type sqlResult struct {
|
||||
LastInsertId int64 `json:"last_insert_id"`
|
||||
RowsAffected int64 `json:"rows_affected"`
|
||||
}
|
||||
|
||||
// sqlResultFrom converts sql.Result into a JSON-marshable sqlResult.
|
||||
func sqlResultFrom(res sql.Result) sqlResult {
|
||||
insertID, errID := res.LastInsertId()
|
||||
rowsAffected, errRows := res.RowsAffected()
|
||||
|
||||
// If an error occurs when extracting the result, it is because the
|
||||
// driver does not support that specific field. Instead of passing this
|
||||
// to the user, omit the field in the response.
|
||||
if errID != nil {
|
||||
insertID = -1
|
||||
}
|
||||
if errRows != nil {
|
||||
rowsAffected = -1
|
||||
}
|
||||
|
||||
return sqlResult{insertID, rowsAffected}
|
||||
}
|
||||
@@ -1,336 +0,0 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kshvakov/clickhouse"
|
||||
"github.com/lib/pq"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewSQLClient(t *testing.T) {
|
||||
originURLs := []string{
|
||||
"postgres://localhost",
|
||||
"cockroachdb://localhost:1337",
|
||||
"postgresql://user:pass@127.0.0.1",
|
||||
"mysql://localhost",
|
||||
"clickhouse://127.0.0.1:9000/?debug",
|
||||
"sqlite3::memory:",
|
||||
"file:test.db?cache=shared",
|
||||
}
|
||||
|
||||
for _, originURL := range originURLs {
|
||||
origin, _ := url.Parse(originURL)
|
||||
_, err := NewSQLClient(context.Background(), origin)
|
||||
|
||||
assert.NoError(t, err, originURL)
|
||||
}
|
||||
|
||||
originURLs = []string{
|
||||
"",
|
||||
"/",
|
||||
"http://localhost",
|
||||
"coolthing://user:pass@127.0.0.1",
|
||||
}
|
||||
|
||||
for _, originURL := range originURLs {
|
||||
origin, _ := url.Parse(originURL)
|
||||
_, err := NewSQLClient(context.Background(), origin)
|
||||
|
||||
assert.Error(t, err, originURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgumentsSQL(t *testing.T) {
|
||||
args := []Arguments{
|
||||
Arguments{
|
||||
Positional: []interface{}{
|
||||
"val", 10, 3.14,
|
||||
},
|
||||
},
|
||||
Arguments{
|
||||
Named: map[string]interface{}{
|
||||
"key": time.Unix(0, 0),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var nameType sql.NamedArg
|
||||
for _, arg := range args {
|
||||
arg.sql("")
|
||||
for _, named := range arg.Positional {
|
||||
assert.IsType(t, nameType, named)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLArg(t *testing.T) {
|
||||
tests := []struct {
|
||||
key interface{}
|
||||
val interface{}
|
||||
dialect string
|
||||
arg sql.NamedArg
|
||||
}{
|
||||
{"key", "val", "mssql", sql.Named("key", "val")},
|
||||
{0, 1, "sqlite3", sql.Named("0", 1)},
|
||||
{1, []string{"a", "b", "c"}, "postgres", sql.Named("1", pq.Array([]string{"a", "b", "c"}))},
|
||||
{"in", []uint{0, 1}, "clickhouse", sql.Named("in", clickhouse.Array([]uint{0, 1}))},
|
||||
{"", time.Unix(0, 0), "", sql.Named("", time.Unix(0, 0))},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
arg := sqlArg(test.key, test.val, test.dialect)
|
||||
assert.Equal(t, test.arg, arg, test.key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLIsolation(t *testing.T) {
|
||||
tests := []struct {
|
||||
str string
|
||||
iso sql.IsolationLevel
|
||||
}{
|
||||
{"", sql.LevelDefault},
|
||||
{"DEFAULT", sql.LevelDefault},
|
||||
{"read_UNcommitted", sql.LevelReadUncommitted},
|
||||
{"serializable", sql.LevelSerializable},
|
||||
{"none", sql.IsolationLevel(-1)},
|
||||
{"SNAP shot", -2},
|
||||
{"blah", -2},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
iso, err := sqlIsolation(test.str)
|
||||
|
||||
if test.iso < -1 {
|
||||
assert.Error(t, err, test.str)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.iso, iso, test.str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLMode(t *testing.T) {
|
||||
modes := []string{
|
||||
"query",
|
||||
"exec",
|
||||
}
|
||||
|
||||
for _, mode := range modes {
|
||||
actual, err := sqlMode(mode)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.ToLower(mode), actual, mode)
|
||||
}
|
||||
|
||||
modes = []string{
|
||||
"",
|
||||
"blah",
|
||||
}
|
||||
|
||||
for _, mode := range modes {
|
||||
_, err := sqlMode(mode)
|
||||
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func helperRows(mockRows *sqlmock.Rows) *sql.Rows {
|
||||
db, mock, _ := sqlmock.New()
|
||||
mock.ExpectQuery("SELECT").WillReturnRows(mockRows)
|
||||
rows, _ := db.Query("SELECT")
|
||||
return rows
|
||||
}
|
||||
|
||||
func TestSQLRows(t *testing.T) {
|
||||
actual, err := sqlRows(helperRows(sqlmock.NewRows(
|
||||
[]string{"name", "age", "dept"}).
|
||||
AddRow("alice", 19, "prod")))
|
||||
expected := []map[string]interface{}{map[string]interface{}{
|
||||
"name": "alice",
|
||||
"age": int64(19),
|
||||
"dept": "prod"}}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestSQLValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
input interface{}
|
||||
output interface{}
|
||||
}{
|
||||
{"hello", "hello"},
|
||||
{1, 1},
|
||||
{false, false},
|
||||
{[]byte("random"), "random"},
|
||||
{[]byte("{\"json\":true}"), map[string]interface{}{"json": true}},
|
||||
{[]byte("[]"), []interface{}{}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
assert.Equal(t, test.output, sqlValue(test.input, nil), test.input)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLResultFrom(t *testing.T) {
|
||||
res := sqlResultFrom(sqlmock.NewResult(1, 2))
|
||||
assert.Equal(t, sqlResult{1, 2}, res)
|
||||
|
||||
res = sqlResultFrom(sqlmock.NewErrorResult(fmt.Errorf("")))
|
||||
assert.Equal(t, sqlResult{-1, -1}, res)
|
||||
}
|
||||
|
||||
func helperSQLite3(t *testing.T) (context.Context, Client) {
|
||||
t.Helper()
|
||||
|
||||
ctx := context.Background()
|
||||
url, _ := url.Parse("file::memory:?cache=shared")
|
||||
|
||||
sqlite3, err := NewSQLClient(ctx, url)
|
||||
assert.NoError(t, err)
|
||||
|
||||
return ctx, sqlite3
|
||||
}
|
||||
|
||||
func TestPing(t *testing.T) {
|
||||
ctx, sqlite3 := helperSQLite3(t)
|
||||
err := sqlite3.Ping(ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSubmit(t *testing.T) {
|
||||
ctx, sqlite3 := helperSQLite3(t)
|
||||
|
||||
res, err := sqlite3.Submit(ctx, &Command{
|
||||
Statement: "CREATE TABLE t (a INTEGER, b FLOAT, c TEXT, d BLOB);",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sqlResult{0, 0}, res)
|
||||
|
||||
res, err = sqlite3.Submit(ctx, &Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "query",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, res)
|
||||
|
||||
res, err = sqlite3.Submit(ctx, &Command{
|
||||
Statement: "INSERT INTO t VALUES (?, ?, ?, ?);",
|
||||
Mode: "exec",
|
||||
Arguments: Arguments{
|
||||
Positional: []interface{}{
|
||||
1,
|
||||
3.14,
|
||||
"text",
|
||||
"blob",
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sqlResult{1, 1}, res)
|
||||
|
||||
res, err = sqlite3.Submit(ctx, &Command{
|
||||
Statement: "UPDATE t SET c = NULL;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sqlResult{1, 1}, res)
|
||||
|
||||
res, err = sqlite3.Submit(ctx, &Command{
|
||||
Statement: "SELECT * FROM t WHERE a = ?;",
|
||||
Mode: "query",
|
||||
Arguments: Arguments{
|
||||
Positional: []interface{}{1},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, res, 1)
|
||||
|
||||
resf, ok := res.([]map[string]interface{})
|
||||
assert.True(t, ok)
|
||||
assert.EqualValues(t, map[string]interface{}{
|
||||
"a": int64(1),
|
||||
"b": 3.14,
|
||||
"c": nil,
|
||||
"d": "blob",
|
||||
}, resf[0])
|
||||
|
||||
res, err = sqlite3.Submit(ctx, &Command{
|
||||
Statement: "DROP TABLE t;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sqlResult{1, 1}, res)
|
||||
}
|
||||
|
||||
func TestSubmitTransaction(t *testing.T) {
|
||||
ctx, sqlite3 := helperSQLite3(t)
|
||||
|
||||
res, err := sqlite3.Submit(ctx, &Command{
|
||||
Statement: "BEGIN;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, res)
|
||||
|
||||
res, err = sqlite3.Submit(ctx, &Command{
|
||||
Statement: "BEGIN; CREATE TABLE tt (a INT); COMMIT;",
|
||||
Mode: "exec",
|
||||
Isolation: "none",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sqlResult{0, 0}, res)
|
||||
|
||||
rows, err := sqlite3.Submit(ctx, &Command{
|
||||
Statement: "SELECT * FROM tt;",
|
||||
Mode: "query",
|
||||
Isolation: "repeatable_read",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, rows)
|
||||
}
|
||||
|
||||
func TestSubmitTimeout(t *testing.T) {
|
||||
ctx, sqlite3 := helperSQLite3(t)
|
||||
|
||||
res, err := sqlite3.Submit(ctx, &Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "query",
|
||||
Timeout: 1 * time.Nanosecond,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, res)
|
||||
}
|
||||
|
||||
func TestSubmitMode(t *testing.T) {
|
||||
ctx, sqlite3 := helperSQLite3(t)
|
||||
|
||||
res, err := sqlite3.Submit(ctx, &Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "notanoption",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, res)
|
||||
}
|
||||
|
||||
func TestSubmitEmpty(t *testing.T) {
|
||||
ctx, sqlite3 := helperSQLite3(t)
|
||||
|
||||
res, err := sqlite3.Submit(ctx, &Command{
|
||||
Statement: "; ; ; ;",
|
||||
Mode: "query",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, res)
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
# docker-compose -f ./dbconnect_tests/dbconnect.yaml up --build --force-recreate --renew-anon-volumes --exit-code-from cloudflared
|
||||
|
||||
version: "2.3"
|
||||
networks:
|
||||
test-dbconnect-network:
|
||||
driver: bridge
|
||||
services:
|
||||
cloudflared:
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: dev.Dockerfile
|
||||
command: go test github.com/cloudflare/cloudflared/dbconnect_tests -v
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
mssql:
|
||||
condition: service_healthy
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DBCONNECT_INTEGRATION_TEST: "true"
|
||||
POSTGRESQL_URL: postgres://postgres:secret@postgres/db?sslmode=disable
|
||||
MYSQL_URL: mysql://root:secret@mysql/db?tls=false
|
||||
MSSQL_URL: mssql://sa:secret12345!@mssql
|
||||
CLICKHOUSE_URL: clickhouse://clickhouse:9000/db
|
||||
networks:
|
||||
- test-dbconnect-network
|
||||
postgres:
|
||||
image: postgres:11.4-alpine
|
||||
environment:
|
||||
POSTGRES_DB: db
|
||||
POSTGRES_PASSWORD: secret
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "-U", "postgres"]
|
||||
start_period: 3s
|
||||
interval: 1s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
networks:
|
||||
- test-dbconnect-network
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
environment:
|
||||
MYSQL_DATABASE: db
|
||||
MYSQL_ROOT_PASSWORD: secret
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping"]
|
||||
start_period: 3s
|
||||
interval: 1s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
networks:
|
||||
- test-dbconnect-network
|
||||
mssql:
|
||||
image: mcr.microsoft.com/mssql/server:2017-CU8-ubuntu
|
||||
environment:
|
||||
ACCEPT_EULA: "Y"
|
||||
SA_PASSWORD: secret12345!
|
||||
healthcheck:
|
||||
test: ["CMD", "/opt/mssql-tools/bin/sqlcmd", "-S", "localhost", "-U", "sa", "-P", "secret12345!", "-Q", "SELECT 1"]
|
||||
start_period: 3s
|
||||
interval: 1s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
networks:
|
||||
- test-dbconnect-network
|
||||
clickhouse:
|
||||
image: yandex/clickhouse-server:19.11
|
||||
healthcheck:
|
||||
test: ["CMD", "clickhouse-client", "--query", "SELECT 1"]
|
||||
start_period: 3s
|
||||
interval: 1s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
networks:
|
||||
- test-dbconnect-network
|
||||
@@ -1,265 +0,0 @@
|
||||
package dbconnect_tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/dbconnect"
|
||||
)
|
||||
|
||||
func TestIntegrationPostgreSQL(t *testing.T) {
|
||||
ctx, pq := helperNewSQLClient(t, "POSTGRESQL_URL")
|
||||
|
||||
err := pq.Ping(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = pq.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "CREATE TABLE t (a TEXT, b UUID, c JSON, d INET[], e SERIAL);",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = pq.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "INSERT INTO t VALUES ($1, $2, $3, $4);",
|
||||
Mode: "exec",
|
||||
Arguments: dbconnect.Arguments{
|
||||
Positional: []interface{}{
|
||||
"text",
|
||||
"6b8d686d-bd8e-43bc-b09a-cfcbbe702c10",
|
||||
"{\"bool\":true,\"array\":[\"a\", 1, 3.14],\"embed\":{\"num\":21}}",
|
||||
[]string{"1.1.1.1", "1.0.0.1"},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = pq.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "UPDATE t SET b = NULL;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err := pq.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "query",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.IsType(t, make([]map[string]interface{}, 0), res)
|
||||
|
||||
actual := res.([]map[string]interface{})[0]
|
||||
expected := map[string]interface{}{
|
||||
"a": "text",
|
||||
"b": nil,
|
||||
"c": map[string]interface{}{
|
||||
"bool": true,
|
||||
"array": []interface{}{"a", float64(1), 3.14},
|
||||
"embed": map[string]interface{}{"num": float64(21)},
|
||||
},
|
||||
"d": "{1.1.1.1,1.0.0.1}",
|
||||
"e": int64(1),
|
||||
}
|
||||
assert.EqualValues(t, expected, actual)
|
||||
|
||||
_, err = pq.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "DROP TABLE t;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestIntegrationMySQL(t *testing.T) {
|
||||
ctx, my := helperNewSQLClient(t, "MYSQL_URL")
|
||||
|
||||
err := my.Ping(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = my.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "CREATE TABLE t (a CHAR, b TINYINT, c FLOAT, d JSON, e YEAR);",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = my.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "INSERT INTO t VALUES (?, ?, ?, ?, ?);",
|
||||
Mode: "exec",
|
||||
Arguments: dbconnect.Arguments{
|
||||
Positional: []interface{}{
|
||||
"a",
|
||||
10,
|
||||
3.14,
|
||||
"{\"bool\":true}",
|
||||
2000,
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = my.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "ALTER TABLE t ADD COLUMN f GEOMETRY;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err := my.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "query",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.IsType(t, make([]map[string]interface{}, 0), res)
|
||||
|
||||
actual := res.([]map[string]interface{})[0]
|
||||
expected := map[string]interface{}{
|
||||
"a": "a",
|
||||
"b": float64(10),
|
||||
"c": 3.14,
|
||||
"d": map[string]interface{}{"bool": true},
|
||||
"e": float64(2000),
|
||||
"f": nil,
|
||||
}
|
||||
assert.EqualValues(t, expected, actual)
|
||||
|
||||
_, err = my.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "DROP TABLE t;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestIntegrationMSSQL(t *testing.T) {
|
||||
ctx, ms := helperNewSQLClient(t, "MSSQL_URL")
|
||||
|
||||
err := ms.Ping(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = ms.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "CREATE TABLE t (a BIT, b DECIMAL, c MONEY, d TEXT);",
|
||||
Mode: "exec"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = ms.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "INSERT INTO t VALUES (?, ?, ?, ?);",
|
||||
Mode: "exec",
|
||||
Arguments: dbconnect.Arguments{
|
||||
Positional: []interface{}{
|
||||
0,
|
||||
3,
|
||||
"$0.99",
|
||||
"text",
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = ms.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "UPDATE t SET d = NULL;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err := ms.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "query",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.IsType(t, make([]map[string]interface{}, 0), res)
|
||||
|
||||
actual := res.([]map[string]interface{})[0]
|
||||
expected := map[string]interface{}{
|
||||
"a": false,
|
||||
"b": float64(3),
|
||||
"c": float64(0.99),
|
||||
"d": nil,
|
||||
}
|
||||
assert.EqualValues(t, expected, actual)
|
||||
|
||||
_, err = ms.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "DROP TABLE t;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestIntegrationClickhouse(t *testing.T) {
|
||||
ctx, ch := helperNewSQLClient(t, "CLICKHOUSE_URL")
|
||||
|
||||
err := ch.Ping(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = ch.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "CREATE TABLE t (a UUID, b String, c Float64, d UInt32, e Int16, f Array(Enum8('a'=1, 'b'=2, 'c'=3))) engine=Memory;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = ch.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "INSERT INTO t VALUES (?, ?, ?, ?, ?, ?);",
|
||||
Mode: "exec",
|
||||
Arguments: dbconnect.Arguments{
|
||||
Positional: []interface{}{
|
||||
"ec65f626-6f50-4c86-9628-6314ef1edacd",
|
||||
"",
|
||||
3.14,
|
||||
314,
|
||||
-144,
|
||||
[]string{"a", "b", "c"},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err := ch.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "query",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.IsType(t, make([]map[string]interface{}, 0), res)
|
||||
|
||||
actual := res.([]map[string]interface{})[0]
|
||||
expected := map[string]interface{}{
|
||||
"a": "ec65f626-6f50-4c86-9628-6314ef1edacd",
|
||||
"b": "",
|
||||
"c": float64(3.14),
|
||||
"d": uint32(314),
|
||||
"e": int16(-144),
|
||||
"f": []string{"a", "b", "c"},
|
||||
}
|
||||
assert.EqualValues(t, expected, actual)
|
||||
|
||||
_, err = ch.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "DROP TABLE t;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func helperNewSQLClient(t *testing.T, env string) (context.Context, dbconnect.Client) {
|
||||
_, ok := os.LookupEnv("DBCONNECT_INTEGRATION_TEST")
|
||||
if ok {
|
||||
t.Helper()
|
||||
} else {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
val, ok := os.LookupEnv(env)
|
||||
if !ok {
|
||||
log.Fatalf("must provide database url as environment variable: %s", env)
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(val)
|
||||
if err != nil {
|
||||
log.Fatalf("cannot provide invalid database url: %s=%s", env, val)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
client, err := dbconnect.NewSQLClient(ctx, parsed)
|
||||
if err != nil {
|
||||
log.Fatalf("could not start test client: %s", err)
|
||||
}
|
||||
|
||||
return ctx, client
|
||||
}
|
||||
+6
-1
@@ -1,4 +1,9 @@
|
||||
FROM golang:1.12 as builder
|
||||
FROM golang:1.15.7 as builder
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0
|
||||
WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
||||
RUN apt-get update
|
||||
COPY . .
|
||||
# compile cloudflared
|
||||
RUN make cloudflared
|
||||
RUN cp /go/src/github.com/cloudflare/cloudflared/cloudflared /usr/local/bin/
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,12 +21,7 @@ const (
|
||||
// discover HA origintunneld servers (GitHub issue #75).
|
||||
dotServerName = "cloudflare-dns.com"
|
||||
dotServerAddr = "1.1.1.1:853"
|
||||
dotTimeout = time.Duration(15 * time.Second)
|
||||
|
||||
// SRV record resolution TTL
|
||||
resolveEdgeAddrTTL = 1 * time.Hour
|
||||
|
||||
subsystemEdgeAddrResolver = "edgeAddrResolver"
|
||||
dotTimeout = 15 * time.Second
|
||||
)
|
||||
|
||||
// Redeclare network functions so they can be overridden in tests.
|
||||
@@ -58,15 +53,15 @@ var friendlyDNSErrorLines = []string{
|
||||
}
|
||||
|
||||
// EdgeDiscovery implements HA service discovery lookup.
|
||||
func edgeDiscovery(logger logger.Service) ([][]*net.TCPAddr, error) {
|
||||
func edgeDiscovery(log *zerolog.Logger) ([][]*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.Errorf("Error looking up Cloudflare edge IPs: the DNS query failed: %s", err)
|
||||
log.Err(err).Msg("Error looking up Cloudflare edge IPs: the DNS query failed")
|
||||
for _, s := range friendlyDNSErrorLines {
|
||||
logger.Error(s)
|
||||
log.Error().Msg(s)
|
||||
}
|
||||
return nil, errors.Wrapf(err, "Could not lookup srv records on _%v._%v.%v", srvService, srvProto, srvName)
|
||||
}
|
||||
@@ -121,15 +116,15 @@ func resolveSRVToTCP(srv *net.SRV) ([]*net.TCPAddr, error) {
|
||||
}
|
||||
|
||||
// ResolveAddrs resolves TCP address given a list of addresses. Address can be a hostname, however, it will return at most one
|
||||
// of the hostname's IP addresses
|
||||
func ResolveAddrs(addrs []string) ([]*net.TCPAddr, error) {
|
||||
var tcpAddrs []*net.TCPAddr
|
||||
// of the hostname's IP addresses.
|
||||
func ResolveAddrs(addrs []string, log *zerolog.Logger) (resolved []*net.TCPAddr) {
|
||||
for _, addr := range addrs {
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
log.Err(err).Msgf("Failed to resolve %s", addr)
|
||||
} else {
|
||||
resolved = append(resolved, tcpAddr)
|
||||
}
|
||||
tcpAddrs = append(tcpAddrs, tcpAddr)
|
||||
}
|
||||
return tcpAddrs, nil
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package allregions
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -19,8 +19,8 @@ func TestEdgeDiscovery(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
l := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
addrLists, err := edgeDiscovery(l)
|
||||
l := zerolog.Nop()
|
||||
addrLists, err := edgeDiscovery(&l)
|
||||
assert.NoError(t, err)
|
||||
actualAddrSet := map[string]bool{}
|
||||
for _, addrs := range addrLists {
|
||||
|
||||
@@ -57,7 +57,6 @@ 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)
|
||||
return
|
||||
}
|
||||
r.connFor[addr] = InUse(connID)
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// Regions stores Cloudflare edge network IPs, partitioned into two regions.
|
||||
@@ -19,8 +19,8 @@ type Regions struct {
|
||||
// ------------------------------------
|
||||
|
||||
// ResolveEdge resolves the Cloudflare edge, returning all regions discovered.
|
||||
func ResolveEdge(logger logger.Service) (*Regions, error) {
|
||||
addrLists, err := edgeDiscovery(logger)
|
||||
func ResolveEdge(log *zerolog.Logger) (*Regions, error) {
|
||||
addrLists, err := edgeDiscovery(log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -35,12 +35,12 @@ func ResolveEdge(logger logger.Service) (*Regions, error) {
|
||||
|
||||
// StaticEdge creates a list of edge addresses from the list of hostnames.
|
||||
// Mainly used for testing connectivity.
|
||||
func StaticEdge(hostnames []string) (*Regions, error) {
|
||||
addrs, err := ResolveAddrs(hostnames)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func StaticEdge(hostnames []string, log *zerolog.Logger) (*Regions, error) {
|
||||
resolved := ResolveAddrs(hostnames, log)
|
||||
if len(resolved) == 0 {
|
||||
return nil, fmt.Errorf("failed to resolve any edge address")
|
||||
}
|
||||
return NewNoResolve(addrs), nil
|
||||
return NewNoResolve(resolved), nil
|
||||
}
|
||||
|
||||
// NewNoResolve doesn't resolve the edge. Instead it just uses the given addresses.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package connection
|
||||
package edgediscovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// DialEdge makes a TLS connection to a Cloudflare edge node
|
||||
// DialEdgeWithH2Mux makes a TLS connection to a Cloudflare edge node
|
||||
func DialEdge(
|
||||
ctx context.Context,
|
||||
timeout time.Duration,
|
||||
@@ -25,6 +25,7 @@ func DialEdge(
|
||||
if err != nil {
|
||||
return nil, newDialError(err, "DialContext error")
|
||||
}
|
||||
|
||||
tlsEdgeConn := tls.Client(edgeConn, tlsConfig)
|
||||
tlsEdgeConn.SetDeadline(time.Now().Add(timeout))
|
||||
|
||||
@@ -6,20 +6,21 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
const (
|
||||
subsystem = "edgediscovery"
|
||||
LogFieldAddress = "address"
|
||||
LogFieldConnIndex = "connIndex"
|
||||
)
|
||||
|
||||
var errNoAddressesLeft = fmt.Errorf("There are no free edge addresses left")
|
||||
var errNoAddressesLeft = fmt.Errorf("there are no free edge addresses left")
|
||||
|
||||
// Edge finds addresses on the Cloudflare edge and hands them out to connections.
|
||||
type Edge struct {
|
||||
regions *allregions.Regions
|
||||
sync.Mutex
|
||||
logger logger.Service
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
@@ -28,34 +29,34 @@ type Edge struct {
|
||||
|
||||
// ResolveEdge runs the initial discovery of the Cloudflare edge, finding Addrs that can be allocated
|
||||
// to connections.
|
||||
func ResolveEdge(l logger.Service) (*Edge, error) {
|
||||
regions, err := allregions.ResolveEdge(l)
|
||||
func ResolveEdge(log *zerolog.Logger) (*Edge, error) {
|
||||
regions, err := allregions.ResolveEdge(log)
|
||||
if err != nil {
|
||||
return new(Edge), err
|
||||
}
|
||||
return &Edge{
|
||||
logger: l,
|
||||
log: log,
|
||||
regions: regions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// StaticEdge creates a list of edge addresses from the list of hostnames. Mainly used for testing connectivity.
|
||||
func StaticEdge(l logger.Service, hostnames []string) (*Edge, error) {
|
||||
regions, err := allregions.StaticEdge(hostnames)
|
||||
func StaticEdge(log *zerolog.Logger, hostnames []string) (*Edge, error) {
|
||||
regions, err := allregions.StaticEdge(hostnames, log)
|
||||
if err != nil {
|
||||
return new(Edge), err
|
||||
}
|
||||
return &Edge{
|
||||
logger: l,
|
||||
log: log,
|
||||
regions: regions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MockEdge creates a Cloudflare Edge from arbitrary TCP addresses. Used for testing.
|
||||
func MockEdge(l logger.Service, addrs []*net.TCPAddr) *Edge {
|
||||
func MockEdge(log *zerolog.Logger, addrs []*net.TCPAddr) *Edge {
|
||||
regions := allregions.NewNoResolve(addrs)
|
||||
return &Edge{
|
||||
logger: l,
|
||||
log: log,
|
||||
regions: regions,
|
||||
}
|
||||
}
|
||||
@@ -76,42 +77,46 @@ func (ed *Edge) GetAddrForRPC() (*net.TCPAddr, error) {
|
||||
}
|
||||
|
||||
// GetAddr gives this proxy connection an edge Addr. Prefer Addrs this connection has already used.
|
||||
func (ed *Edge) GetAddr(connID int) (*net.TCPAddr, error) {
|
||||
func (ed *Edge) GetAddr(connIndex int) (*net.TCPAddr, error) {
|
||||
log := ed.log.With().Int(LogFieldConnIndex, connIndex).Logger()
|
||||
|
||||
ed.Lock()
|
||||
defer ed.Unlock()
|
||||
|
||||
// If this connection has already used an edge addr, return it.
|
||||
if addr := ed.regions.AddrUsedBy(connID); addr != nil {
|
||||
ed.logger.Debugf("edgediscovery - GetAddr: Returning same address back to proxy connection: connID: %d", connID)
|
||||
if addr := ed.regions.AddrUsedBy(connIndex); addr != nil {
|
||||
log.Debug().Msg("edgediscovery - GetAddr: Returning same address back to proxy connection")
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// Otherwise, give it an unused one
|
||||
addr := ed.regions.GetUnusedAddr(nil, connID)
|
||||
addr := ed.regions.GetUnusedAddr(nil, connIndex)
|
||||
if addr == nil {
|
||||
ed.logger.Debugf("edgediscovery - GetAddr: No addresses left to give proxy connection: connID: %d", connID)
|
||||
log.Debug().Msg("edgediscovery - GetAddr: No addresses left to give proxy connection")
|
||||
return nil, errNoAddressesLeft
|
||||
}
|
||||
ed.logger.Debugf("edgediscovery - GetAddr: Giving connection its new address %s: connID: %d", addr, connID)
|
||||
log.Debug().Str(LogFieldAddress, addr.String()).Msg("edgediscovery - GetAddr: Giving connection its new address")
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// GetDifferentAddr gives back the proxy connection's edge Addr and uses a new one.
|
||||
func (ed *Edge) GetDifferentAddr(connID int) (*net.TCPAddr, error) {
|
||||
func (ed *Edge) GetDifferentAddr(connIndex int) (*net.TCPAddr, error) {
|
||||
log := ed.log.With().Int(LogFieldConnIndex, connIndex).Logger()
|
||||
|
||||
ed.Lock()
|
||||
defer ed.Unlock()
|
||||
|
||||
oldAddr := ed.regions.AddrUsedBy(connID)
|
||||
oldAddr := ed.regions.AddrUsedBy(connIndex)
|
||||
if oldAddr != nil {
|
||||
ed.regions.GiveBack(oldAddr)
|
||||
}
|
||||
addr := ed.regions.GetUnusedAddr(oldAddr, connID)
|
||||
addr := ed.regions.GetUnusedAddr(oldAddr, connIndex)
|
||||
if addr == nil {
|
||||
ed.logger.Debugf("edgediscovery - GetDifferentAddr: No addresses left to give proxy connection: connID: %d", connID)
|
||||
log.Debug().Msg("edgediscovery - GetDifferentAddr: No addresses left to give proxy connection")
|
||||
// note: if oldAddr were not nil, it will become available on the next iteration
|
||||
return nil, errNoAddressesLeft
|
||||
}
|
||||
ed.logger.Debugf("edgediscovery - GetDifferentAddr: Giving connection its new address %s: connID: %d", addr, connID)
|
||||
log.Debug().Str(LogFieldAddress, addr.String()).Msg("edgediscovery - GetDifferentAddr: Giving connection its new address")
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
@@ -127,6 +132,6 @@ func (ed *Edge) AvailableAddrs() int {
|
||||
func (ed *Edge) GiveBack(addr *net.TCPAddr) bool {
|
||||
ed.Lock()
|
||||
defer ed.Unlock()
|
||||
ed.logger.Debug("edgediscovery - GiveBack: Address now unused")
|
||||
ed.log.Debug().Msg("edgediscovery - GiveBack: Address now unused")
|
||||
return ed.regions.GiveBack(addr)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -29,11 +29,12 @@ var (
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
}
|
||||
|
||||
log = zerolog.Nop()
|
||||
)
|
||||
|
||||
func TestGiveBack(t *testing.T) {
|
||||
l := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
edge := MockEdge(l, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
edge := MockEdge(&log, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
|
||||
// Give this connection an address
|
||||
assert.Equal(t, 4, edge.AvailableAddrs())
|
||||
@@ -49,10 +50,8 @@ func TestGiveBack(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRPCAndProxyShareSingleEdgeIP(t *testing.T) {
|
||||
l := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
// Make an edge with a single IP
|
||||
edge := MockEdge(l, []*net.TCPAddr{&addr0})
|
||||
edge := MockEdge(&log, []*net.TCPAddr{&addr0})
|
||||
tunnelConnID := 0
|
||||
|
||||
// Use the IP for a tunnel
|
||||
@@ -66,8 +65,7 @@ func TestRPCAndProxyShareSingleEdgeIP(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetAddrForRPC(t *testing.T) {
|
||||
l := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
edge := MockEdge(l, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
edge := MockEdge(&log, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
|
||||
// Get a connection
|
||||
assert.Equal(t, 4, edge.AvailableAddrs())
|
||||
@@ -84,10 +82,8 @@ func TestGetAddrForRPC(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOnePerRegion(t *testing.T) {
|
||||
l := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
// Make an edge with only one address
|
||||
edge := MockEdge(l, []*net.TCPAddr{&addr0, &addr1})
|
||||
edge := MockEdge(&log, []*net.TCPAddr{&addr0, &addr1})
|
||||
|
||||
// Use the only address
|
||||
const connID = 0
|
||||
@@ -108,10 +104,8 @@ func TestOnePerRegion(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOnlyOneAddrLeft(t *testing.T) {
|
||||
l := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
// Make an edge with only one address
|
||||
edge := MockEdge(l, []*net.TCPAddr{&addr0})
|
||||
edge := MockEdge(&log, []*net.TCPAddr{&addr0})
|
||||
|
||||
// Use the only address
|
||||
const connID = 0
|
||||
@@ -130,10 +124,8 @@ func TestOnlyOneAddrLeft(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNoAddrsLeft(t *testing.T) {
|
||||
l := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
// Make an edge with no addresses
|
||||
edge := MockEdge(l, []*net.TCPAddr{})
|
||||
edge := MockEdge(&log, []*net.TCPAddr{})
|
||||
|
||||
_, err := edge.GetAddr(2)
|
||||
assert.Error(t, err)
|
||||
@@ -142,8 +134,7 @@ func TestNoAddrsLeft(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetAddr(t *testing.T) {
|
||||
l := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
edge := MockEdge(l, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
edge := MockEdge(&log, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
|
||||
// Give this connection an address
|
||||
const connID = 0
|
||||
@@ -158,8 +149,7 @@ func TestGetAddr(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetDifferentAddr(t *testing.T) {
|
||||
l := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
edge := MockEdge(l, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
edge := MockEdge(&log, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
|
||||
// Give this connection an address
|
||||
assert.Equal(t, 4, edge.AvailableAddrs())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package connection
|
||||
package edgediscovery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -0,0 +1,45 @@
|
||||
package edgediscovery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
protocolRecord = "protocol.argotunnel.com"
|
||||
)
|
||||
|
||||
var (
|
||||
errNoProtocolRecord = fmt.Errorf("No TXT record found for %s to determine connection protocol", protocolRecord)
|
||||
)
|
||||
|
||||
func HTTP2Percentage() (int32, error) {
|
||||
records, err := net.LookupTXT(protocolRecord)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(records) == 0 {
|
||||
return 0, errNoProtocolRecord
|
||||
}
|
||||
return parseHTTP2Precentage(records[0])
|
||||
}
|
||||
|
||||
// The record looks like http2=percentage
|
||||
func parseHTTP2Precentage(record string) (int32, error) {
|
||||
const key = "http2"
|
||||
slices := strings.Split(record, "=")
|
||||
if len(slices) != 2 {
|
||||
return 0, fmt.Errorf("Malformed TXT record %s, expect http2=percentage", record)
|
||||
}
|
||||
if slices[0] != key {
|
||||
return 0, fmt.Errorf("Incorrect key %s, expect %s", slices[0], key)
|
||||
}
|
||||
percentage, err := strconv.ParseInt(slices[1], 10, 32)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int32(percentage), nil
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user