Compare commits

..

82 Commits

Author SHA1 Message Date
Adam Chalmers 42ac30f2fb Release 2020.9.2 2020-09-23 15:30:35 -05:00
Adam Chalmers c065fae792 TUN-3410: Request the v1 Tunnelstore API 2020-09-23 20:24:53 +00:00
Lee Valentine 4117162109 TRAFFIC-448: build cloudflare for junos and publish to s3 2020-09-23 18:04:01 +00:00
Bojan Zelic fe554668e5 updater service exit code should be 11 2020-09-23 18:02:18 +04:00
Dalton 691e86a564 AUTH-3109 upload the checksum to workers kv on github releases 2020-09-22 19:43:30 +00:00
cthuang 197d65659a TUN-3291: cloudflared tunnel run -h explains how to use flags from parent command 2020-09-21 19:07:30 +00:00
Igor Postelnik 85d0afd3b0 TUN-3295: Show route command results 2020-09-21 16:32:08 +00:00
Michael Borkenstein 20623255dd AUTH-3110-use-cfsetup-precache 2020-09-18 16:26:33 -05:00
Dalton afa5e68fe5 AUTH-3103 CI build fixes 2020-09-18 19:33:30 +00:00
Areg Harutyunyan 747427f816 TUN-3216: UI improvements 2020-09-17 13:22:08 +04:00
Rachel Williams f99b6c6421 TUN-3335: Dynamically set connection table size for UI 2020-09-17 11:52:10 +04:00
Rachel Williams 250bc54110 TUN-3333: Add text to UI explaining how to exit 2020-09-17 11:52:10 +04:00
Rachel Williams bb530b87dd TUN-3328: Filter out free tunnel has started log from UI 2020-09-17 11:52:10 +04:00
Rachel Williams 02587c1edc TUN-3321: Add box around logs on UI 2020-09-17 11:52:10 +04:00
Rachel Williams 26fc20d406 TUN-3198: Handle errors while running tunnel UI 2020-09-17 11:52:10 +04:00
Rachel Williams 8a829b773a Rebased and passed TunnelEventChan to LogServerInfo in new ReconnectTunnel function 2020-09-17 11:52:10 +04:00
Rachel Williams 71b98e6111 TUN-3261: Display connections on UI for free classic tunnels 2020-09-17 11:52:10 +04:00
Rachel Williams fee13dc62f TUN-3255: Update UI to display URL instead of hostname 2020-09-17 11:52:10 +04:00
Rachel Williams 094e0c7592 TUN-3238: Update UI when connection re-connects 2020-09-17 11:52:10 +04:00
Rachel Williams b57a953caa TUN-3200: Add connection information to UI 2020-09-17 11:52:10 +04:00
Rachel Williams d8ebde37ca TUN-3201: Create base cloudflared UI structure 2020-09-17 11:52:07 +04:00
Adam Chalmers 0708a49848 Release 2020.9.1 2020-09-16 12:54:59 -05:00
Adam Chalmers b26f3082e6 Use Go 1.15.2 2020-09-16 12:45:49 -05:00
Igor Postelnik cb6d424765 TUN-3395: Improve help for list command 2020-09-16 16:48:43 +00:00
Igor Postelnik 5753aa9f18 TUN-3294: Perform basic validation on arguments of route command; remove default pool name which wasn't valid 2020-09-16 16:48:43 +00:00
Igor Postelnik bfae12008d TUN-3395: Improve help for list command 2020-09-16 08:30:24 -05:00
Igor Postelnik c52e0dc8ef TUN-3395: Unhide named tunnel subcommands, tweak help 2020-09-15 12:41:15 +00:00
Adam Chalmers 55346444c9 Release 2020.9.0 2020-09-14 16:41:59 -05:00
Igor Postelnik ba785ec58d TUN-3293: Try to use error information from the body of a failed tunnelstore reresponse if available 2020-09-11 17:12:17 -05:00
Adam Chalmers 3be2545ad4 TUN-3292: Mention cleanup in tunnel run help. 2020-09-10 11:02:31 -05:00
Dalton 4a8597c245 AUTH-2653 add notarization to mac build 2020-09-09 19:43:00 +00:00
Igor Postelnik 741cd66c9e TUN-3375: Upgrade coredns and prometheus dependencies 2020-09-09 13:14:38 -05:00
Adam Chalmers 7acea1ac99 TUN-3375: Upgrade x/text and gorilla websocket deps 2020-09-09 12:07:35 -05:00
cthuang 22d771b51d TUN-3284: Use cloudflared/<version> as user agent of tunnelstore client 2020-09-09 10:34:26 +01:00
Dalton 00d6ab2eb7 AUTH-2653 search for the certificate and not the identity 2020-09-08 12:21:10 -05:00
Dalton cc0a5ac3df AUTH-2653 disble error pipe to see what is failing 2020-09-08 10:41:06 -05:00
cthuang 5fb938d6d6 TUN-3345: tunnel run accepts name of tunnel as argument 2020-09-05 09:32:50 +00:00
Dalton cd5bdb837e AUTH-2653 handle duplicate key import errors 2020-09-04 17:08:09 -05:00
Dalton 107abf9d29 AUTH-2653 changed to proper file extension 2020-09-04 20:23:42 +00:00
Adam Chalmers 218ee30206 TUN-3377: Tunnel route should check dns/lb before checking tunnel ID 2020-09-04 18:44:06 +00:00
Dalton 8764fbfdfa AUTH-2653 renabled signing 2020-09-04 11:29:17 -05:00
Dalton 2463d6b92c TUN-3352 extra debug logging for websockets 2020-08-31 17:14:02 -05:00
Dalton 810d268c99 AUTH-2016 fixed variable fail 2020-08-20 12:56:12 -05:00
Dalton f5b479dbbc Release 2020.8.2 2020-08-20 11:15:11 -05:00
Dalton b52728e829 AUTH-3021 fixed the git version call by using the older flag 2020-08-20 16:13:10 +00:00
cthuang 8eeb452cce TUN-3268: Each connection has its own event digest to reconnect 2020-08-20 16:44:22 +01:00
Dalton 9323844ea7 Release 2020.8.1 2020-08-19 21:59:21 -05:00
Dalton b698fe5ef3 AUTH-2864 - add macos build to github release 2020-08-19 21:31:50 -05:00
Dalton edb980d439 AUTH-2653 disabling signing until we can get the keys 2020-08-19 15:32:49 -05:00
Dalton 310bd0dbf1 AUTH-2712 fixed the mac build script 2020-08-19 14:57:43 +00:00
Adam Chalmers 1a96889141 TUN-3286: Use either ID or name in Named Tunnel subcommands. 2020-08-19 14:39:45 +00:00
Dalton Cherry 60de05bfc1 AUTH-2712 added MSI build for a windows agent 2020-08-17 14:44:28 -05:00
cthuang 3deef6197f TUN-3213: Create, route and run named tunnels in one command 2020-08-17 19:38:38 +00:00
Dalton 70114c2145 AUTH-2977 log file protection 2020-08-17 17:55:58 +00:00
Dalton 5499c77e62 AUTH-2975 don't check /etc on windows 2020-08-17 12:40:36 -05:00
cthuang 292a7f07a2 TUN-3243: Refactor tunnel subcommands to allow commands to compose better 2020-08-11 10:02:52 +01:00
Igor Postelnik 679f36303a TUN-3242: Build with go 1.14 2020-08-10 23:17:23 +00:00
Adam Chalmers 1b61d699c4 TUN-3237: By default, don't show connections that are pending reconnect 2020-08-07 10:16:13 -05:00
cthuang a7562dff68 TUN-3233: List tunnels support filtering by deleted, name, existed at and id 2020-08-07 10:09:26 +01:00
Dalton 8daf1e882f AUTH-2857 update homebrew script to use new url 2020-08-04 16:47:10 -05:00
Michael Borkenstein 20466dacb7 Release 2020.8.0 2020-08-03 12:18:44 -06:00
Dalton 5b3b592108 AUTH-2927 run message update after all github builds are done 2020-08-03 10:12:03 -05:00
Adam Chalmers 9952ce0069 TUN-3221: ConnectionOptions tracks numPreviousAttempts. 2020-07-31 20:22:33 +00:00
cthuang 1cbc8fb8ac TUN-3220: tunnel route reports created route 2020-07-31 17:48:58 +01:00
Michael Borkenstein edc69694cb AUTH-2714: Adds arm64 cloudflared build 2020-07-29 13:12:57 -06:00
Igor Postelnik bd15c6b8c3 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.
benchmark                                    old ns/op     new ns/op     delta
BenchmarkSingleStreamLargeResponseBody-8     17786594      12163494      -31.61%

benchmark                                    old allocs     new allocs     delta
BenchmarkSingleStreamLargeResponseBody-8     17086          15869          -7.12%

benchmark                                    old bytes     new bytes     delta
BenchmarkSingleStreamLargeResponseBody-8     58215169      21604391      -62.89%
2020-07-29 14:30:12 +00:00
Igor Postelnik 42fe2e7266 TUN-3208: Add benchmark for large response write 2020-07-29 14:30:12 +00:00
Igor Postelnik 44e3be2c88 TUN-3209: improve performance and reduce allocations during user header serialization from h1 to h2
benchmark                                    old ns/op     new ns/op     delta
BenchmarkH1ResponseToH2ResponseHeaders-4     10360         5048          -51.27%

benchmark                                    old allocs     new allocs     delta
BenchmarkH1ResponseToH2ResponseHeaders-4     135            26             -80.74%

benchmark                                    old bytes     new bytes     delta
BenchmarkH1ResponseToH2ResponseHeaders-4     8543          3667          -57.08%
2020-07-29 14:29:07 +00:00
Igor Postelnik 61d5461138 TUN-3209: Add benchmark for header serialization 2020-07-29 14:29:07 +00:00
Michael Borkenstein b696ca8b1c Removes centos 6 build 2020-07-29 14:01:34 +00:00
Dalton 6a4d020c27 AUTH-2016 adds sha256 hashes to releases 2020-07-27 10:36:31 -05:00
Igor Postelnik 4791ba3b87 TUN-3194: Don't render log output when level is not enabled 2020-07-23 18:36:31 -05:00
Igor Postelnik cf1c9a3083 TUN-3192: Use zone ID in tunnelstore request path; improve debug logging 2020-07-21 18:07:50 -05:00
Igor Postelnik d61e3fb130 TUN-3190: Initialize logger using command line flags in tunnels subcommands 2020-07-21 17:25:48 +00:00
Dalton ca7d6797e1 AUTH-2902 redirect with just the root host on curl commands 2020-07-21 11:08:31 -05:00
Areg Harutyunyan af0d04d0f3 Release 2020.7.4 2020-07-21 11:02:29 +01:00
Ivan Babrou 6274567e16 Build cloudflared for arm64 on native agents 2020-07-20 17:42:50 -07:00
cthuang 8836ee1dda TUN-3156: Add route subcommand under tunnel 2020-07-17 05:51:24 +08:00
Michael Borkenstein 7afde79600 AUTH-2890: adds error handler to cli actions 2020-07-15 14:33:36 +00:00
Rachel Williams 3d782f7162 TUN-3048: Handle error when user tries to delete active tunnel 2020-07-14 15:32:46 +00:00
Michael Borkenstein 058598ea58 Release 2020.7.3 2020-07-13 13:32:38 -05:00
Michael Borkenstein 8e617df914 Change scp command to use file glob that matches both cloudflared rpms and debs 2020-07-13 12:15:01 -05:00
1097 changed files with 153392 additions and 41277 deletions
+3
View File
@@ -5,12 +5,15 @@ guide/public
/.GOPATH
/bin
.idea
.build
.vscode
\#*\#
cscope.*
cloudflared
cloudflared.pkg
cloudflared.exe
cloudflared.msi
cloudflared-x86-64*
!cmd/cloudflared/
.DS_Store
*-session.log
+169 -3
View File
@@ -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
+1 -1
View File
@@ -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'
+76 -6
View File
@@ -1,6 +1,9 @@
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].*.*")
DATE := $(shell date -u '+%Y-%m-%d-%H%M UTC')
VERSION_FLAGS := -ldflags='-X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)"'
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.
IMPORT_PATH := github.com/cloudflare/cloudflared
PACKAGE_DIR := $(CURDIR)/packaging
@@ -22,6 +25,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 +44,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 +58,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
@@ -83,17 +88,17 @@ test-ssh-server:
define publish_package
for HOST in $(CF_PKG_HOSTS); do \
ssh-keyscan -t rsa $$HOST >> ~/.ssh/known_hosts; \
scp -4 cloudflared_$(VERSION)_amd64.deb cfsync@$$HOST:/state/cf-pkg/staging/$(1)/$(TARGET_PUBLIC_REPO)/cloudflared/; \
scp -4 cloudflared*.$(1) cfsync@$$HOST:/state/cf-pkg/staging/$(2)/$(TARGET_PUBLIC_REPO)/cloudflared/; \
done
endef
.PHONY: publish-deb
publish-deb: cloudflared-deb
$(call publish_package,apt)
$(call publish_package,deb,apt)
.PHONY: publish-rpm
publish-rpm: cloudflared-rpm
$(call publish_package,yum)
$(call publish_package,rpm,yum)
define build_package
mkdir -p $(PACKAGE_DIR)
@@ -122,6 +127,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 +196,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 +222,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)
+90
View File
@@ -1,3 +1,93 @@
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
2020.7.2
- 2020-07-02 AUTH-2644: Change install location and add man page
- 2020-07-02 TUN-3131: Allow user to specify tunnel credentials path, and remove it in tunnel delete command
+13 -1
View File
@@ -86,6 +86,7 @@ func createWebsocketStream(options *StartOptions, logger logger.Service) (*cfweb
wsConn, resp, err := cfwebsocket.ClientConnect(req, nil)
defer closeRespBody(resp)
if err != nil && IsAccessResponse(resp) {
wsConn, err = createAccessAuthenticatedStream(options, logger)
if err != nil {
@@ -141,5 +142,16 @@ func createAccessWebSocketStream(options *StartOptions, logger logger.Service) (
dump, err := httputil.DumpRequest(req, false)
logger.Debugf("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 {
logger.Debugf("Websocket response: %q", r)
} else if err != nil {
logger.Debugf("Websocket response error: %v", err)
}
}
return conn, resp, err
}
+87 -42
View File
@@ -1,4 +1,4 @@
pinned_go: &pinned_go go=1.12.9-1
pinned_go: &pinned_go go=1.15.2-1
build_dir: &build_dir /cfsetup_build
default-flavor: stretch
stretch: &stretch
@@ -22,6 +22,17 @@ stretch: &stretch
- export GOOS=linux
- export GOARCH=amd64
- make cloudflared-deb
build-deb-arm64:
build_dir: *build_dir
builddeps:
- *pinned_go
- build-essential
- fakeroot
- rubygem-fpm
post-cache:
- export GOOS=linux
- export GOARCH=arm64
- make cloudflared-deb
publish-deb:
build_dir: *build_dir
builddeps:
@@ -50,8 +61,9 @@ stretch: &stretch
- build-essential
- python3-setuptools
- python3-pip
post-cache:
pre-cache: &install_pygithub
- pip3 install pygithub
post-cache:
- export GOOS=linux
- export GOARCH=amd64
- make github-release
@@ -74,8 +86,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 +108,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 +130,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,12 +153,35 @@ 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
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:
@@ -165,50 +200,59 @@ stretch: &stretch
- s3cmd
post-cache:
- .teamcity/update-homebrew.sh
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
jessie: *stretch
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
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.2.linux-amd64.tar.gz -P /tmp/
- tar -C /usr/local -xzf /tmp/go1.15.2.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,10 +260,11 @@ centos-7:
build-rpm:
build_dir: *build_dir
builddeps: *el7_builddeps
pre-cache:
- yum install -y fakeroot
- wget https://golang.org/dl/go1.15.2.linux-amd64.tar.gz -P /tmp/
- tar -C /usr/local -xzf /tmp/go1.15.2.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
+1 -1
View File
@@ -12,7 +12,7 @@ import (
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/validation"
"github.com/pkg/errors"
cli "gopkg.in/urfave/cli.v2"
cli "github.com/urfave/cli/v2"
)
// StartForwarder starts a client side websocket forward
+1 -1
View File
@@ -21,7 +21,7 @@ import (
"golang.org/x/net/idna"
"github.com/getsentry/raven-go"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
)
const (
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
)
type usageError string
+16 -5
View File
@@ -7,8 +7,8 @@ import (
"runtime"
homedir "github.com/mitchellh/go-homedir"
"gopkg.in/urfave/cli.v2"
"gopkg.in/urfave/cli.v2/altsrc"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"gopkg.in/yaml.v2"
"github.com/cloudflare/cloudflared/validation"
@@ -26,7 +26,8 @@ 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}
)
const DefaultCredentialFile = "cert.pem"
@@ -63,6 +64,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)
@@ -86,10 +97,10 @@ func FindInputSourceContext(context *cli.Context) (altsrc.InputSourceContext, er
}
// 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 {
+1 -1
View File
@@ -5,7 +5,7 @@ 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{}) {
+5 -4
View File
@@ -7,10 +7,11 @@ import (
"os"
"path/filepath"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
cli "gopkg.in/urfave/cli.v2"
cli "github.com/urfave/cli/v2"
)
func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
@@ -21,12 +22,12 @@ func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
&cli.Command{
Name: "install",
Usage: "Install Argo Tunnel as a system service",
Action: installLinuxService,
Action: cliutil.ErrorHandler(installLinuxService),
},
&cli.Command{
Name: "uninstall",
Usage: "Uninstall the Argo Tunnel service",
Action: uninstallLinuxService,
Action: cliutil.ErrorHandler(uninstallLinuxService),
},
},
})
@@ -66,7 +67,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'
`,
},
{
+4 -3
View File
@@ -6,8 +6,9 @@ import (
"fmt"
"os"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
)
@@ -24,12 +25,12 @@ 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),
},
},
})
+3 -3
View File
@@ -17,7 +17,7 @@ import (
raven "github.com/getsentry/raven-go"
homedir "github.com/mitchellh/go-homedir"
cli "gopkg.in/urfave/cli.v2"
cli "github.com/urfave/cli/v2"
"github.com/pkg/errors"
)
@@ -88,7 +88,7 @@ func commands(version func(c *cli.Context)) []*cli.Command {
cmds := []*cli.Command{
{
Name: "update",
Action: updater.Update,
Action: cliutil.ErrorHandler(updater.Update),
Usage: "Update the agent if a new version exists",
ArgsUsage: " ",
Description: `Looks for a new version on the official download server.
@@ -129,7 +129,7 @@ func action(version string, shutdownC, graceShutdownC chan struct{}) cli.ActionF
tags := make(map[string]string)
tags["hostname"] = c.String("hostname")
raven.SetTagsContext(tags)
raven.CapturePanic(func() { err = tunnel.StartServer(c, version, shutdownC, graceShutdownC, nil) }, nil)
raven.CapturePanic(func() { err = tunnel.TunnelCommand(c) }, nil)
exitCode := 0
if err != nil {
handleError(err)
+1 -1
View File
@@ -13,7 +13,7 @@ import (
// 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])
configPath, err := homedir.Expand(config.DefaultConfigSearchDirectories()[0])
if err != nil {
return "", err
}
+1 -1
View File
@@ -80,7 +80,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)
+9 -9
View File
@@ -128,19 +128,19 @@ 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
// it appends the full url as the redirect URL to the access cli request if opening the browser
func FetchTokenWithRedirect(appURL *url.URL, logger logger.Service) (string, error) {
return getToken(appURL, true, logger)
}
// FetchToken will either load a stored token or generate a new one
// 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)
}
// FetchToken will either load a stored token or generate a new one
// 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, logger logger.Service) (string, error) {
return getToken(appURL, true, logger)
}
// getToken will either load a stored token or generate a new one
func getToken(appURL *url.URL, shouldRedirect bool, logger logger.Service) (string, error) {
func getToken(appURL *url.URL, useHostOnly bool, logger logger.Service) (string, error) {
if token, err := GetTokenIfExists(appURL); token != "" && err == nil {
return token, nil
}
@@ -166,7 +166,7 @@ func getToken(appURL *url.URL, shouldRedirect bool, logger logger.Service) (stri
// 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)
token, err := transfer.Run(appURL, keyName, keyName, "", path, true, useHostOnly, logger)
if err != nil {
return "", err
}
+8 -8
View File
@@ -28,12 +28,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, path string, shouldEncrypt bool, useHostOnly bool, logger logger.Service) ([]byte, error) {
encrypterClient, err := encrypter.New("cloudflared_priv.pem", "cloudflared_pub.pem")
if err != nil {
return nil, err
}
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
}
@@ -82,18 +82,18 @@ func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncr
// BuildRequestURL creates a request suitable for a resource transfer.
// it will return a constructed url based off the base url and query key/value provided.
// cli will build a url for cli transfer request.
func buildRequestURL(baseURL *url.URL, key, value string, cli, 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
baseURL.RawQuery = q.Encode() // and this actual baseURL.
baseURL.Path = "cdn-cgi/access/cli"
return baseURL.String(), nil
}
+233 -100
View File
@@ -20,6 +20,7 @@ import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/cmd/cloudflared/ui"
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
"github.com/cloudflare/cloudflared/dbconnect"
"github.com/cloudflare/cloudflared/h2mux"
@@ -33,6 +34,7 @@ import (
"github.com/cloudflare/cloudflared/sshserver"
"github.com/cloudflare/cloudflared/tlsconfig"
"github.com/cloudflare/cloudflared/tunneldns"
"github.com/cloudflare/cloudflared/tunnelstore"
"github.com/cloudflare/cloudflared/websocket"
"github.com/coreos/go-systemd/daemon"
@@ -42,8 +44,8 @@ import (
"github.com/google/uuid"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"gopkg.in/urfave/cli.v2"
"gopkg.in/urfave/cli.v2/altsrc"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
)
const (
@@ -90,6 +92,9 @@ const (
// bastionFlag is to enable bastion, or jump host, operation
bastionFlag = "bastion"
// uiFlag is to enable launching cloudflared in interactive UI mode
uiFlag = "ui"
logDirectoryFlag = "log-directory"
debugLevelWarning = "At debug level, request URL, method, protocol, content legnth and header will be logged. " +
@@ -169,15 +174,23 @@ func Commands() []*cli.Command {
c.Hidden = false
subcommands = append(subcommands, &c)
}
subcommands = append(subcommands, buildCreateCommand())
subcommands = append(subcommands, buildListCommand())
subcommands = append(subcommands, buildDeleteCommand())
subcommands = append(subcommands, buildRunCommand())
subcommands = append(subcommands, buildCleanupCommand())
subcommands = append(subcommands, buildRouteCommand())
cmds = append(cmds, &cli.Command{
cmds = append(cmds, buildTunnelCommand(subcommands))
return cmds
}
func buildTunnelCommand(subcommands []*cli.Command) *cli.Command {
return &cli.Command{
Name: "tunnel",
Action: cliutil.ErrorHandler(tunnel),
Action: cliutil.ErrorHandler(TunnelCommand),
Before: Before,
Category: "Tunnel",
Usage: "Make a locally-running web service accessible over the internet using Argo Tunnel.",
@@ -191,32 +204,86 @@ func Commands() []*cli.Command {
To use, begin by calling login to download a certificate:
cloudflared tunnel login
$ cloudflared tunnel login
With your certificate installed you can then launch your first tunnel,
replacing my.site.com with a subdomain of your site:
cloudflared tunnel --hostname my.site.com --url http://localhost:8080
$ cloudflared tunnel --hostname my.site.com --url http://localhost:8080
If you have a web server running on port 8080 (in this example), it will be available on
the internet!`,
Subcommands: subcommands,
Flags: tunnelFlags(false),
})
return cmds
}
}
func tunnel(c *cli.Context) error {
return StartServer(c, version, shutdownC, graceShutdownC, nil)
func TunnelCommand(c *cli.Context) error {
if name := c.String("name"); name != "" { // Start a named tunnel
return adhocNamedTunnel(c, name)
} else { // Start a classic tunnel
return classicTunnel(c)
}
}
func Init(v string, s, g chan struct{}) {
version, shutdownC, graceShutdownC = v, s, g
}
func createLogger(c *cli.Context, isTransport bool) (logger.Service, error) {
loggerOpts := []logger.Option{}
// adhocNamedTunnel create, route and run a named tunnel in one command
func adhocNamedTunnel(c *cli.Context, name string) error {
sc, err := newSubcommandContext(c)
if err != nil {
return err
}
tunnel, ok, err := sc.tunnelActive(name)
if err != nil || !ok {
tunnel, err = sc.create(name)
if err != nil {
return errors.Wrap(err, "failed to create tunnel")
}
} else {
sc.logger.Infof("Tunnel already created with ID %s", tunnel.ID)
}
if r, ok := routeFromFlag(c); ok {
if res, err := sc.route(tunnel.ID, r); err != nil {
sc.logger.Errorf("failed to create route, please create it manually. err: %v.", err)
} else {
sc.logger.Infof(res.SuccessSummary())
}
}
if err := sc.run(tunnel.ID); err != nil {
return errors.Wrap(err, "error running tunnel")
}
return nil
}
// classicTunnel creates a "classic" non-named tunnel
func classicTunnel(c *cli.Context) error {
sc, err := newSubcommandContext(c)
if err != nil {
return err
}
return StartServer(c, version, shutdownC, graceShutdownC, nil, sc.logger, sc.isUIEnabled)
}
func routeFromFlag(c *cli.Context) (tunnelstore.Route, bool) {
if hostname := c.String("hostname"); hostname != "" {
if lbPool := c.String("lb-pool"); lbPool != "" {
return tunnelstore.NewLBRoute(hostname, lbPool), true
}
return tunnelstore.NewDNSRoute(hostname), true
}
return nil, false
}
func createLogger(c *cli.Context, isTransport bool, disableTerminal bool) (*logger.OutputWriter, error) {
var loggerOpts []logger.Option
logPath := c.String("logfile")
if logPath == "" {
@@ -236,15 +303,28 @@ func createLogger(c *cli.Context, isTransport bool) (logger.Service, error) {
}
loggerOpts = append(loggerOpts, logger.LogLevelString(logLevel))
return logger.New(loggerOpts...)
}
func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan struct{}, namedTunnel *origin.NamedTunnelConfig) error {
logger, err := createLogger(c, false)
if err != nil {
return cliutil.PrintLoggerSetupError("error setting up logger", err)
if disableTerminal {
disableOption := logger.DisableTerminal(true)
loggerOpts = append(loggerOpts, disableOption)
}
l, err := logger.New(loggerOpts...)
if err != nil {
return nil, err
}
return l, nil
}
func StartServer(
c *cli.Context,
version string,
shutdownC,
graceShutdownC chan struct{},
namedTunnel *origin.NamedTunnelConfig,
log logger.Service,
isUIEnabled bool,
) error {
_ = raven.SetDSN(sentryDSN)
var wg sync.WaitGroup
listeners := gracenet.Net{}
@@ -253,45 +333,49 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
dnsReadySignal := make(chan struct{})
if c.String("config") == "" {
logger.Infof("Cannot determine default configuration path. No file %v in %v", config.DefaultConfigFiles, config.DefaultConfigDirs)
log.Infof(
"Cannot determine default configuration path. No file %v in %v",
config.DefaultConfigFiles,
config.DefaultConfigSearchDirectories(),
)
}
if c.IsSet("trace-output") {
tmpTraceFile, err := ioutil.TempFile("", "trace")
if err != nil {
logger.Errorf("Failed to create new temporary file to save trace output: %s", err)
log.Errorf("Failed to create new temporary file to save trace output: %s", err)
}
defer func() {
if err := tmpTraceFile.Close(); err != nil {
logger.Errorf("Failed to close trace output file %s with error: %s", tmpTraceFile.Name(), err)
log.Errorf("Failed to close trace output file %s with error: %s", tmpTraceFile.Name(), err)
}
if err := os.Rename(tmpTraceFile.Name(), c.String("trace-output")); err != nil {
logger.Errorf("Failed to rename temporary trace output file %s to %s with error: %s", tmpTraceFile.Name(), c.String("trace-output"), err)
log.Errorf("Failed to rename temporary trace output file %s to %s with error: %s", tmpTraceFile.Name(), c.String("trace-output"), err)
} else {
err := os.Remove(tmpTraceFile.Name())
if err != nil {
logger.Errorf("Failed to remove the temporary trace file %s with error: %s", tmpTraceFile.Name(), err)
log.Errorf("Failed to remove the temporary trace file %s with error: %s", tmpTraceFile.Name(), err)
}
}
}()
if err := trace.Start(tmpTraceFile); err != nil {
logger.Errorf("Failed to start trace: %s", err)
log.Errorf("Failed to start trace: %s", err)
return errors.Wrap(err, "Error starting tracing")
}
defer trace.Stop()
}
buildInfo := buildinfo.GetBuildInfo(version)
buildInfo.Log(logger)
logClientOptions(c, logger)
buildInfo.Log(log)
logClientOptions(c, log)
if c.IsSet("proxy-dns") {
wg.Add(1)
go func() {
defer wg.Done()
errC <- runDNSProxyServer(c, dnsReadySignal, shutdownC, logger)
errC <- runDNSProxyServer(c, dnsReadySignal, shutdownC, log)
}()
} else {
close(dnsReadySignal)
@@ -302,24 +386,24 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
metricsListener, err := listeners.Listen("tcp", c.String("metrics"))
if err != nil {
logger.Errorf("Error opening metrics server listener: %s", err)
log.Errorf("Error opening metrics server listener: %s", err)
return errors.Wrap(err, "Error opening metrics server listener")
}
defer metricsListener.Close()
wg.Add(1)
go func() {
defer wg.Done()
errC <- metrics.ServeMetrics(metricsListener, shutdownC, logger)
errC <- metrics.ServeMetrics(metricsListener, shutdownC, log)
}()
go notifySystemd(connectedSignal)
if c.IsSet("pidfile") {
go writePidFile(connectedSignal, c.String("pidfile"), logger)
go writePidFile(connectedSignal, c.String("pidfile"), log)
}
cloudflaredID, err := uuid.NewRandom()
if err != nil {
logger.Errorf("Cannot generate cloudflared ID: %s", err)
log.Errorf("Cannot generate cloudflared ID: %s", err)
return err
}
@@ -330,12 +414,12 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
}()
// update needs to be after DNS proxy is up to resolve equinox server address
if updater.IsAutoupdateEnabled(c, logger) {
logger.Infof("Autoupdate frequency is set to %v", c.Duration("autoupdate-freq"))
if updater.IsAutoupdateEnabled(c, log) {
log.Infof("Autoupdate frequency is set to %v", c.Duration("autoupdate-freq"))
wg.Add(1)
go func() {
defer wg.Done()
autoupdater := updater.NewAutoUpdater(c.Duration("autoupdate-freq"), &listeners, logger)
autoupdater := updater.NewAutoUpdater(c.Duration("autoupdate-freq"), &listeners, log)
errC <- autoupdater.Run(ctx)
}()
}
@@ -344,21 +428,21 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
if dnsProxyStandAlone(c) {
connectedSignal.Notify()
// no grace period, handle SIGINT/SIGTERM immediately
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, 0, logger)
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, 0, log)
}
if c.IsSet("hello-world") {
logger.Infof("hello-world set")
log.Infof("hello-world set")
helloListener, err := hello.CreateTLSListener("127.0.0.1:")
if err != nil {
logger.Errorf("Cannot start Hello World Server: %s", err)
log.Errorf("Cannot start Hello World Server: %s", err)
return errors.Wrap(err, "Cannot start Hello World Server")
}
defer helloListener.Close()
wg.Add(1)
go func() {
defer wg.Done()
hello.StartHelloWorldServer(logger, helloListener, shutdownC)
_ = hello.StartHelloWorldServer(log, helloListener, shutdownC)
}()
forceSetFlag(c, "url", "https://"+helloListener.Addr().String())
}
@@ -366,11 +450,11 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
if c.IsSet(sshServerFlag) {
if runtime.GOOS != "darwin" && runtime.GOOS != "linux" {
msg := fmt.Sprintf("--ssh-server is not supported on %s", runtime.GOOS)
logger.Error(msg)
log.Error(msg)
return errors.New(msg)
}
logger.Infof("ssh-server set")
log.Infof("ssh-server set")
logManager := sshlog.NewEmptyManager()
if c.IsSet(bucketNameFlag) && c.IsSet(regionNameFlag) && c.IsSet(accessKeyIDFlag) && c.IsSet(secretIDFlag) {
@@ -378,34 +462,34 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
c.String(accessKeyIDFlag), c.String(secretIDFlag), c.String(sessionTokenIDFlag), c.String(s3URLFlag))
if err != nil {
msg := "Cannot create uploader for SSH Server"
logger.Errorf("%s: %s", msg, err)
log.Errorf("%s: %s", msg, err)
return errors.Wrap(err, msg)
}
if err := os.MkdirAll(sshLogFileDirectory, 0700); err != nil {
msg := fmt.Sprintf("Cannot create SSH log file directory %s", sshLogFileDirectory)
logger.Errorf("%s: %s", msg, err)
log.Errorf("%s: %s", msg, err)
return errors.Wrap(err, msg)
}
logManager = sshlog.New(sshLogFileDirectory)
uploadManager := awsuploader.NewDirectoryUploadManager(logger, uploader, sshLogFileDirectory, 30*time.Minute, shutdownC)
uploadManager := awsuploader.NewDirectoryUploadManager(log, uploader, sshLogFileDirectory, 30*time.Minute, shutdownC)
uploadManager.Start()
}
localServerAddress := "127.0.0.1:" + c.String(sshPortFlag)
server, err := sshserver.New(logManager, logger, version, localServerAddress, c.String("hostname"), c.Path(hostKeyPath), shutdownC, c.Duration(sshIdleTimeoutFlag), c.Duration(sshMaxTimeoutFlag))
server, err := sshserver.New(logManager, log, version, localServerAddress, c.String("hostname"), c.Path(hostKeyPath), shutdownC, c.Duration(sshIdleTimeoutFlag), c.Duration(sshMaxTimeoutFlag))
if err != nil {
msg := "Cannot create new SSH Server"
logger.Errorf("%s: %s", msg, err)
log.Errorf("%s: %s", msg, err)
return errors.Wrap(err, msg)
}
wg.Add(1)
go func() {
defer wg.Done()
if err = server.Start(); err != nil && err != ssh.ErrServerClosed {
logger.Errorf("SSH server error: %s", err)
log.Errorf("SSH server error: %s", err)
// TODO: remove when declarative tunnels are implemented.
close(shutdownC)
}
@@ -417,14 +501,14 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
hostname := c.String("hostname")
if url == hostname && url != "" && hostname != "" {
errText := "hostname and url shouldn't match. See --help for more information"
logger.Error(errText)
log.Error(errText)
return fmt.Errorf(errText)
}
if staticHost := hostnameFromURI(c.String("url")); isProxyDestinationConfigured(staticHost, c) {
listener, err := net.Listen("tcp", "127.0.0.1:")
if err != nil {
logger.Errorf("Cannot start Websocket Proxy Server: %s", err)
log.Errorf("Cannot start Websocket Proxy Server: %s", err)
return errors.Wrap(err, "Cannot start Websocket Proxy Server")
}
wg.Add(1)
@@ -432,7 +516,7 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
defer wg.Done()
streamHandler := websocket.DefaultStreamHandler
if c.IsSet(socks5Flag) {
logger.Info("SOCKS5 server started")
log.Info("SOCKS5 server started")
streamHandler = func(wsConn *websocket.Conn, remoteConn net.Conn, _ http.Header) {
dialer := socks.NewConnDialer(remoteConn)
requestHandler := socks.NewRequestHandler(dialer)
@@ -445,32 +529,32 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
if finalDestination := requestHeaders.Get(h2mux.CFJumpDestinationHeader); finalDestination != "" {
token := requestHeaders.Get(h2mux.CFAccessTokenHeader)
if err := websocket.SendSSHPreamble(remoteConn, finalDestination, token); err != nil {
logger.Errorf("Failed to send SSH preamble: %s", err)
log.Errorf("Failed to send SSH preamble: %s", err)
return
}
}
websocket.DefaultStreamHandler(wsConn, remoteConn, requestHeaders)
}
}
errC <- websocket.StartProxyServer(logger, listener, staticHost, shutdownC, streamHandler)
errC <- websocket.StartProxyServer(log, listener, staticHost, shutdownC, streamHandler)
}()
forceSetFlag(c, "url", "http://"+listener.Addr().String())
}
transportLogger, err := createLogger(c, true)
transportLogger, err := createLogger(c, true, false)
if err != nil {
return errors.Wrap(err, "error setting up transport logger")
}
tunnelConfig, err := prepareTunnelConfig(c, buildInfo, version, logger, transportLogger, namedTunnel)
tunnelConfig, err := prepareTunnelConfig(c, buildInfo, version, log, transportLogger, namedTunnel)
if err != nil {
return err
}
reconnectCh := make(chan origin.ReconnectSignal, 1)
if c.IsSet("stdin-control") {
logger.Info("Enabling control through stdin")
go stdinControl(reconnectCh, logger)
log.Info("Enabling control through stdin")
go stdinControl(reconnectCh, log)
}
wg.Add(1)
@@ -479,7 +563,26 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
errC <- origin.StartTunnelDaemon(ctx, tunnelConfig, connectedSignal, cloudflaredID, reconnectCh)
}()
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, c.Duration("grace-period"), logger)
if isUIEnabled {
const tunnelEventChanBufferSize = 16
tunnelEventChan := make(chan ui.TunnelEvent, tunnelEventChanBufferSize)
tunnelConfig.TunnelEventChan = tunnelEventChan
tunnelInfo := ui.NewUIModel(
version,
hostname,
metricsListener.Addr().String(),
tunnelConfig.OriginUrl,
tunnelConfig.HAConnections,
)
logLevels, err := logger.ParseLevelString(c.String("loglevel"))
if err != nil {
return err
}
tunnelInfo.LaunchUI(ctx, log, logLevels, tunnelEventChan)
}
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, c.Duration("grace-period"), log)
}
// forceSetFlag attempts to set the given flag value in the closest context that has it defined
@@ -492,13 +595,13 @@ func forceSetFlag(c *cli.Context, name, value string) {
}
func Before(c *cli.Context) error {
logger, err := createLogger(c, false)
logger, err := createLogger(c, false, false)
if err != nil {
return cliutil.PrintLoggerSetupError("error setting up logger", err)
}
if c.String("config") == "" {
logger.Debugf("Cannot determine default configuration path. No file %v in %v", config.DefaultConfigFiles, config.DefaultConfigDirs)
logger.Debugf("Cannot determine default configuration path. No file %v in %v", config.DefaultConfigFiles, config.DefaultConfigSearchDirectories())
}
inputSource, err := config.FindInputSourceContext(c)
if err != nil {
@@ -613,7 +716,7 @@ func dbConnectCmd() *cli.Command {
cmd.Action = cliutil.ErrorHandler(func(c *cli.Context) error {
err := dbconnect.CmdAction(c)
if err == nil {
err = tunnel(c)
err = TunnelCommand(c)
}
return err
})
@@ -649,7 +752,7 @@ func appendFlags(flags []cli.Flag, extraFlags ...cli.Flag) []cli.Flag {
}
func tunnelFlags(shouldHide bool) []cli.Flag {
return []cli.Flag{
flags := []cli.Flag{
&cli.StringFlag{
Name: "config",
Usage: "Specifies a config file in YAML format.",
@@ -706,13 +809,6 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
EnvVars: []string{"TUNNEL_ORIGIN_CA_POOL"},
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "url",
Value: "http://localhost:8080",
Usage: "Connect to the local webserver at `URL`.",
EnvVars: []string{"TUNNEL_URL"},
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "unix-socket",
Usage: "Path to unix socket to use instead of --url",
@@ -828,20 +924,6 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
EnvVars: []string{"TUNNEL_RETRIES"},
Hidden: shouldHide,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "hello-world",
Value: false,
Usage: "Run Hello World Server",
EnvVars: []string{"TUNNEL_HELLO_WORLD"},
Hidden: shouldHide,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: sshServerFlag,
Value: false,
Usage: "Run an SSH Server",
EnvVars: []string{"TUNNEL_SSH_SERVER"},
Hidden: true, // TODO: remove when feature is complete
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "pidfile",
Usage: "Write the application's PID to this file after first successful connection.",
@@ -986,6 +1068,54 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
EnvVars: []string{"DIAL_EDGE_TIMEOUT"},
Hidden: true,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "stdin-control",
Usage: "Control the process using commands sent through stdin",
EnvVars: []string{"STDIN-CONTROL"},
Hidden: true,
Value: false,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
EnvVars: []string{"TUNNEL_NAME"},
Usage: "Stable name to identify the tunnel. Using this flag will create, route and run a tunnel. For production usage, execute each command separately",
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: uiFlag,
Usage: "Launch tunnel UI. Tunnel logs are scrollable via 'j', 'k', or arrow keys.",
Value: false,
Hidden: shouldHide,
}),
urlFlag(shouldHide),
helloWorldFlag(shouldHide),
createSocks5Flag(shouldHide),
}
return append(flags, sshFlags(shouldHide)...)
}
func urlFlag(shouldHide bool) cli.Flag {
return altsrc.NewStringFlag(&cli.StringFlag{
Name: "url",
Value: "http://localhost:8080",
Usage: "Connect to the local webserver at `URL`.",
EnvVars: []string{"TUNNEL_URL"},
Hidden: shouldHide,
})
}
func helloWorldFlag(shouldHide bool) cli.Flag {
return altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "hello-world",
Value: false,
Usage: "Run Hello World Server",
EnvVars: []string{"TUNNEL_HELLO_WORLD"},
Hidden: shouldHide,
})
}
func sshFlags(shouldHide bool) []cli.Flag {
return []cli.Flag{
altsrc.NewStringFlag(&cli.StringFlag{
Name: sshPortFlag,
Usage: "Localhost port that cloudflared SSH server will run on",
@@ -1017,18 +1147,18 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
EnvVars: []string{"REGION_ID"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: accessKeyIDFlag,
Usage: "Access Key ID of where to upload SSH logs",
EnvVars: []string{"ACCESS_CLIENT_ID"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: secretIDFlag,
Usage: "Secret ID of where to upload SSH logs",
EnvVars: []string{"SECRET_ID"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: accessKeyIDFlag,
Usage: "Access Key ID of where to upload SSH logs",
EnvVars: []string{"ACCESS_CLIENT_ID"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: sessionTokenIDFlag,
Usage: "Session Token to use in the configuration of SSH logs uploading",
@@ -1048,18 +1178,11 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
Hidden: true,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "stdin-control",
Usage: "Control the process using commands sent through stdin",
EnvVars: []string{"STDIN-CONTROL"},
Hidden: true,
Name: sshServerFlag,
Value: false,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: socks5Flag,
Usage: "specify if this tunnel is running as a SOCK5 Server",
EnvVars: []string{"TUNNEL_SOCKS"},
Value: false,
Hidden: shouldHide,
Usage: "Run an SSH Server",
EnvVars: []string{"TUNNEL_SSH_SERVER"},
Hidden: true, // TODO: remove when feature is complete
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: bastionFlag,
@@ -1071,6 +1194,16 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
}
}
func createSocks5Flag(shouldHide bool) cli.Flag {
return altsrc.NewBoolFlag(&cli.BoolFlag{
Name: socks5Flag,
Usage: "specify if this tunnel is running as a SOCK5 Server",
EnvVars: []string{"TUNNEL_SOCKS"},
Value: false,
Hidden: shouldHide,
})
}
func stdinControl(reconnectCh chan origin.ReconnectSignal, logger logger.Service) {
for {
scanner := bufio.NewScanner(os.Stdin)
+5 -5
View File
@@ -23,8 +23,8 @@ import (
"github.com/google/uuid"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"golang.org/x/crypto/ssh/terminal"
"gopkg.in/urfave/cli.v2"
)
var (
@@ -34,10 +34,10 @@ var (
argumentsUrl = developerPortal + "/reference/arguments/"
)
// 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
@@ -95,7 +95,7 @@ func dnsProxyStandAlone(c *cli.Context) bool {
func findOriginCert(c *cli.Context, logger logger.Service) (string, error) {
originCertPath := c.String("origincert")
if originCertPath == "" {
logger.Infof("Cannot determine default origin certificate path. No file %s in %v", config.DefaultCredentialFile, config.DefaultConfigDirs)
logger.Infof("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")
@@ -302,7 +302,7 @@ func prepareTunnelConfig(
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"),
UseReconnectToken: compatibilityMode && c.Bool("use-reconnect-token"),
}, nil
}
+3 -3
View File
@@ -12,7 +12,7 @@ import (
"github.com/cloudflare/cloudflared/logger"
homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
cli "gopkg.in/urfave/cli.v2"
cli "github.com/urfave/cli/v2"
)
const (
@@ -40,7 +40,7 @@ func login(c *cli.Context) error {
return err
}
_, err = transfer.Run(loginURL, "cert", "callback", callbackStoreURL, path, false, true, logger)
_, err = transfer.Run(loginURL, "cert", "callback", callbackStoreURL, path, false, false, logger)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to write the certificate due to the following error:\n%v\n\nYour browser will download the certificate instead. You will have to manually\ncopy it to the following path:\n\n%s\n", err, path)
return err
@@ -51,7 +51,7 @@ func login(c *cli.Context) error {
}
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 -1
View File
@@ -4,7 +4,7 @@ import (
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tunneldns"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
"github.com/pkg/errors"
)
@@ -0,0 +1,353 @@
package tunnel
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/cloudflare/cloudflared/certutil"
"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/tunnelstore"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
)
// subcommandContext carries structs shared between subcommands, to reduce number of arguments needed to
// pass between subcommands, and make sure they are only initialized once
type subcommandContext struct {
c *cli.Context
logger logger.Service
// These fields should be accessed using their respective Getter
tunnelstoreClient tunnelstore.Client
userCredential *userCredential
isUIEnabled bool
}
func newSubcommandContext(c *cli.Context) (*subcommandContext, error) {
isUIEnabled := c.IsSet(uiFlag) && c.String("name") != ""
// If UI is enabled, terminal log output should be disabled -- log should be written into a UI log window instead
logger, err := createLogger(c, false, isUIEnabled)
if err != nil {
return nil, errors.Wrap(err, "error setting up logger")
}
return &subcommandContext{
c: c,
logger: logger,
isUIEnabled: isUIEnabled,
}, nil
}
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.logger)
if err != nil {
return nil, err
}
sc.tunnelstoreClient = client
return client, nil
}
func (sc *subcommandContext) credential() (*userCredential, error) {
if sc.userCredential == nil {
originCertPath, err := findOriginCert(sc.c, sc.logger)
if err != nil {
return nil, errors.Wrap(err, "Error locating origin cert")
}
blocks, err := readOriginCert(originCertPath, sc.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)
}
sc.userCredential = &userCredential{
cert: cert,
certPath: originCertPath,
}
}
return sc.userCredential, nil
}
func (sc *subcommandContext) readTunnelCredentials(tunnelID uuid.UUID) (*pogs.TunnelAuth, error) {
filePath, err := sc.tunnelCredentialsPath(tunnelID)
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 (sc *subcommandContext) tunnelCredentialsPath(tunnelID uuid.UUID) (string, error) {
if filePath := sc.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(sc.c, sc.logger); err == nil {
originCertDir := filepath.Dir(originCertPath)
if filePath, err := tunnelFilePath(tunnelID, originCertDir); err == nil {
if validFilePath(filePath) {
return filePath, nil
}
}
}
// Last resort look under default config directories
for _, configDir := range config.DefaultConfigSearchDirectories() {
if filePath, err := tunnelFilePath(tunnelID, configDir); err == nil {
if validFilePath(filePath) {
return filePath, nil
}
}
}
return "", fmt.Errorf("Tunnel credentials file not found")
}
func (sc *subcommandContext) create(name 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
}
if writeFileErr := writeTunnelCredentials(tunnel.ID, credential.cert.AccountID, credential.certPath, tunnelSecret, sc.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 nil, errors.New(errorMsg)
}
if outputFormat := sc.c.String(outputFormatFlag.Name); outputFormat != "" {
return nil, renderOutput(outputFormat, &tunnel)
}
sc.logger.Infof("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)
}
tunnelCredentialsPath, err := sc.tunnelCredentialsPath(tunnel.ID)
if err != nil {
sc.logger.Infof("Cannot locate tunnel credentials to delete, error: %v. Please delete the file manually", err)
return nil
}
if err = os.Remove(tunnelCredentialsPath); err != nil {
sc.logger.Infof("Cannot delete tunnel credentials, error: %v. Please delete the file manually", err)
}
}
return nil
}
func (sc *subcommandContext) run(tunnelID uuid.UUID) error {
credentials, err := sc.readTunnelCredentials(tunnelID)
if err != nil {
return err
}
return StartServer(
sc.c,
version,
shutdownC,
graceShutdownC,
&origin.NamedTunnelConfig{Auth: *credentials, ID: tunnelID},
sc.logger,
sc.isUIEnabled,
)
}
func (sc *subcommandContext) cleanupConnections(tunnelIDs []uuid.UUID) error {
client, err := sc.client()
if err != nil {
return err
}
for _, tunnelID := range tunnelIDs {
sc.logger.Infof("Cleanup connection for tunnel %s", tunnelID)
if err := client.CleanupConnections(tunnelID); err != nil {
sc.logger.Errorf("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)
}
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
}
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,82 @@
package tunnel
import (
"reflect"
"testing"
"github.com/cloudflare/cloudflared/tunnelstore"
"github.com/google/uuid"
)
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)
}
})
}
}
+266 -221
View File
@@ -7,6 +7,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"text/tabwriter"
@@ -15,14 +16,12 @@ import (
"github.com/google/uuid"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
"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/tunnelstore"
)
@@ -37,6 +36,28 @@ var (
Aliases: []string{"d"},
Usage: "Include deleted tunnels in the list",
}
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 = &cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
@@ -55,17 +76,19 @@ var (
Aliases: []string{credFileFlagAlias},
Usage: "File path of tunnel credentials",
}
forceDeleteFlag = &cli.BoolFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "Allows you to delete a tunnel, even if it has active connections.",
}
)
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},
}
}
@@ -77,62 +100,28 @@ 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)
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 {
func writeTunnelCredentials(tunnelID uuid.UUID, accountID, originCertPath string, tunnelSecret []byte, logger logger.Service) error {
originCertDir := filepath.Dir(originCertPath)
filePath, err := tunnelFilePath(tunnelID, originCertDir)
if err != nil {
@@ -150,51 +139,6 @@ func writeTunnelCredentials(tunnelID, accountID, originCertPath string, tunnelSe
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 {
originCertDir := filepath.Dir(originCertPath)
if filePath, err := tunnelFilePath(tunnelID, originCertDir); err == nil {
if validFilePath(filePath) {
return filePath, nil
}
}
}
// Last resort look under default config directories
for _, configDir := range config.DefaultConfigDirs {
if filePath, err := tunnelFilePath(tunnelID, configDir); err == nil {
if validFilePath(filePath) {
return filePath, nil
}
}
}
return "", fmt.Errorf("Tunnel credentials file not found")
}
func validFilePath(path string) bool {
fileStat, err := os.Stat(path)
if err != nil {
@@ -206,40 +150,40 @@ func validFilePath(path string) bool {
func buildListCommand() *cli.Command {
return &cli.Command{
Name: "list",
Action: cliutil.ErrorHandler(listTunnels),
Action: cliutil.ErrorHandler(listCommand),
Usage: "List existing tunnels",
ArgsUsage: " ",
Hidden: hideSubcommands,
Flags: []cli.Flag{outputFormatFlag, showDeletedFlag},
Flags: []cli.Flag{outputFormatFlag, showDeletedFlag, listNameFlag, listExistedAtFlag, listIDFlag, showRecentlyDisconnected},
}
}
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
}
if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" {
@@ -247,15 +191,14 @@ func listTunnels(c *cli.Context) error {
}
if len(tunnels) > 0 {
fmtAndPrintTunnelList(tunnels)
fmtAndPrintTunnelList(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 fmtAndPrintTunnelList(tunnels []*tunnelstore.Tunnel, showRecentlyDisconnected bool) {
const (
minWidth = 0
tabWidth = 8
@@ -265,26 +208,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")
// 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))
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
@@ -305,46 +254,29 @@ 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},
Action: cliutil.ErrorHandler(deleteCommand),
Usage: "Delete existing tunnel by UUID or name",
ArgsUsage: "TUNNEL",
Flags: []cli.Flag{credentialsFileFlag, forceDeleteFlag},
}
}
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 +292,212 @@ 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,
urlFlag(false),
helloWorldFlag(false),
createSocks5Flag(false),
}
flags = append(flags, sshFlags(false)...)
var runCommandHelpTemplate = `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{.UsageText}}
DESCRIPTION:
{{.Description}}
TUNNEL COMMAND OPTIONS:
See cloudflared tunnel -h
RUN COMMAND OPTIONS:
{{range .VisibleFlags}}{{.}}
{{end}}
`
return &cli.Command{
Name: "run",
Action: cliutil.ErrorHandler(runTunnel),
Action: cliutil.ErrorHandler(runCommand),
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 [run command options]",
ArgsUsage: "TUNNEL",
Description: `Runs the tunnel identified by name or UUUD, creating a highly available connection
between your server and the Cloudflare edge.
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 experience problems running
the tunnel, "cloudflared tunnel cleanup" may help by removing any old connection records.
All the flags from the tunnel command are available, note that they have to be specified before the run command. There are flags defined both in tunnel and run command. The one in run command will take precedence.
For example cloudflared tunnel --url localhost:3000 run --url localhost:5000 <TUNNEL ID> will proxy requests to localhost:5000.
`,
Flags: flags,
CustomHelpTemplate: runCommandHelpTemplate,
}
}
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" requires exactly 1 argument, the ID or name of the tunnel to run.`)
}
tunnelID, err := sc.findID(c.Args().First())
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")
}
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,
Action: cliutil.ErrorHandler(cleanupCommand),
Usage: "Cleanup tunnel connections",
ArgsUsage: "TUNNEL",
}
}
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",
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>`,
ArgsUsage: "dns|lb TUNNEL HOSTNAME [LB-POOL]",
}
}
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) {
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) {
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) {
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]*$")
func validateName(s string) bool {
return nameRegex.MatchString(s)
}
func validateHostname(s string) bool {
// Slightly stricter than PunyCodeProfile
idnaProfile := idna.New(
idna.ValidateLabels(true),
idna.VerifyDNSLength(true))
puny, err := idnaProfile.ToASCII(s)
return err == nil && validateName(puny)
}
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.logger.Infof(res.SuccessSummary())
return nil
}
+42 -3
View File
@@ -40,6 +40,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 +80,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 +88,37 @@ 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); got != tt.want {
t.Errorf("validateName() = %v, want %v", got, tt.want)
}
}
}
+236
View File
@@ -0,0 +1,236 @@
package ui
import (
"context"
"fmt"
"time"
"github.com/cloudflare/cloudflared/logger"
"github.com/gdamore/tcell"
"github.com/rivo/tview"
)
type connState struct {
location string
state status
}
type status int
const (
Disconnected status = iota
Connected
Reconnecting
SetUrl
RegisteringTunnel
)
type TunnelEvent struct {
Index uint8
EventType status
Location string
Url string
}
type uiModel struct {
version string
edgeURL string
metricsURL string
proxyURL string
connections []connState
}
type palette struct {
url string
connected string
defaultText string
disconnected string
reconnecting string
}
func NewUIModel(version, hostname, metricsURL, proxyURL string, haConnections int) *uiModel {
return &uiModel{
version: version,
edgeURL: hostname,
metricsURL: metricsURL,
proxyURL: proxyURL,
connections: make([]connState, haConnections),
}
}
func (data *uiModel) LaunchUI(
ctx context.Context,
log logger.Service,
logLevels []logger.Level,
tunnelEventChan <-chan TunnelEvent,
) {
// Configure the logger to stream logs into the textview
// Add TextView as a group to write output to
logTextView := NewDynamicColorTextView()
log.Add(logTextView, logger.NewUIFormatter(time.RFC3339), logLevels...)
// Construct the UI
palette := palette{
url: "lightblue",
connected: "lime",
defaultText: "white",
disconnected: "red",
reconnecting: "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)
grid.AddItem(NewDynamicColorTextView().SetText(fmt.Sprintf("[%s]\u2022[%s] Proxying to [%s::b]%s", palette.connected, palette.defaultText, palette.url, data.proxyURL)), 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() {
for {
select {
case <-ctx.Done():
app.Stop()
return
case event := <-tunnelEventChan:
switch event.EventType {
case Connected:
data.setConnTableCell(event, connTable, palette)
case Disconnected, Reconnecting:
data.changeConnStatus(event, connTable, log, palette)
case SetUrl:
tunnelHostText.SetText(event.Url)
data.edgeURL = event.Url
case RegisteringTunnel:
if data.edgeURL == "" {
tunnelHostText.SetText("Registering tunnel...")
}
}
}
app.Draw()
}
}()
go func() {
if err := app.SetRoot(frame, true).Run(); err != nil {
log.Errorf("Error launching UI: %s", err)
}
}()
}
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 TunnelEvent, table *tview.Table, logger logger.Service, 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 {
logger.Info("Connection is not in the UI table")
return
}
locationState := event.Location
if event.EventType == Disconnected {
connState.state = Disconnected
} else if event.EventType == Reconnecting {
connState.state = 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 TunnelEvent, table *tview.Table, palette palette) {
index := int(event.Index)
connectionNum := index + 1
// Update slice to keep track of connection location and state in UI table
data.connections[index].state = Connected
data.connections[index].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 status) string {
// HA connection indicator formatted as: "• #<CONNECTION_INDEX>: <COLO>",
// where the left middle dot's color depends on the status of the connection
const connFmtString = "[%s]\u2022[%s] #%d: %s"
var dotColor string
switch connectedStatus {
case Connected:
dotColor = palette.connected
case Disconnected:
dotColor = palette.disconnected
case Reconnecting:
dotColor = palette.reconnecting
}
return fmt.Sprintf(connFmtString, dotColor, palette.defaultText, connectionNum, location)
}
+2 -2
View File
@@ -8,8 +8,8 @@ import (
"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"
@@ -38,7 +38,7 @@ EKx0BZogHSor9Wy5VztdFaAaVbsJiCbO
)
// 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
}
+4 -5
View File
@@ -14,7 +14,7 @@ import (
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
cli "gopkg.in/urfave/cli.v2"
cli "github.com/urfave/cli/v2"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/svc"
@@ -150,12 +150,11 @@ func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeReques
switch c.Cmd {
case svc.Interrogate:
statusChan <- c.CurrentStatus
case svc.Stop:
case svc.Stop, svc.Shutdown:
close(s.graceShutdownC)
statusChan <- svc.Status{State: svc.Stopped, Accepts: cmdsAccepted}
statusChan <- svc.Status{State: svc.StopPending}
case svc.Shutdown:
close(s.shutdownC)
statusChan <- svc.Status{State: svc.StopPending}
return
default:
elog.Error(1, fmt.Sprintf("unexpected control request #%d", c))
}
+2 -2
View File
@@ -6,8 +6,8 @@ import (
"net"
"strconv"
"gopkg.in/urfave/cli.v2"
"gopkg.in/urfave/cli.v2/altsrc"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
)
// Cmd is the entrypoint command for dbconnect.
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/assert"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
)
func TestCmd(t *testing.T) {
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/python3
"""
Creates Github Releases Notes with content hashes
"""
import argparse
import logging
import os
import hashlib
import glob
from github import Github, GithubException, UnknownObjectException
FORMAT = "%(levelname)s - %(asctime)s: %(message)s"
logging.basicConfig(format=FORMAT)
CLOUDFLARED_REPO = os.environ.get("GITHUB_REPO", "cloudflare/cloudflared")
GITHUB_CONFLICT_CODE = "already_exists"
def get_sha256(filename):
""" get the sha256 of a file """
sha256_hash = hashlib.sha256()
with open(filename,"rb") as f:
for byte_block in iter(lambda: f.read(4096),b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
def update_or_add_message(msg, name, sha):
"""
updates or builds the github version message for each new asset's sha256.
Searches the existing message string to update or create.
"""
new_text = '{0}: {1}\n'.format(name, sha)
start = msg.find(name)
if (start != -1):
end = msg.find("\n", start)
if (end != -1):
return msg.replace(msg[start:end+1], new_text)
back = msg.rfind("```")
if (back != -1):
return '{0}{1}```'.format(msg[:back], new_text)
return '{0} \n### SHA256 Checksums:\n```\n{1}```'.format(msg, new_text)
def get_release(repo, version):
""" Get a Github Release matching the version tag. """
try:
release = repo.get_release(version)
logging.info("Release %s found", version)
return release
except UnknownObjectException:
logging.info("Release %s not found", version)
def parse_args():
""" Parse and validate args """
parser = argparse.ArgumentParser(
description="Creates Github Releases and uploads assets."
)
parser.add_argument(
"--api-key", default=os.environ.get("API_KEY"), help="Github API key"
)
parser.add_argument(
"--release-version",
metavar="version",
default=os.environ.get("VERSION"),
help="Release version",
)
parser.add_argument(
"--dry-run", action="store_true", help="Do not modify the release message"
)
args = parser.parse_args()
is_valid = True
if not args.release_version:
logging.error("Missing release version")
is_valid = False
if not args.api_key:
logging.error("Missing API key")
is_valid = False
if is_valid:
return args
parser.print_usage()
exit(1)
def main():
""" Attempts to update the Github Release message with the github asset's checksums """
try:
args = parse_args()
client = Github(args.api_key)
repo = client.get_repo(CLOUDFLARED_REPO)
release = get_release(repo, args.release_version)
msg = release.body
for filename in glob.glob(".artifacts/*.*"):
pkg_hash = get_sha256(filename)
# add the sha256 of the new artifact to the release message body
msg = update_or_add_message(msg, filename, pkg_hash)
if args.dry_run:
logging.info("Skipping asset upload because of dry-run")
return
# update the release body text
release.update_release(args.release_version, msg)
except Exception as e:
logging.exception(e)
exit(1)
main()
+70
View File
@@ -6,6 +6,9 @@ Creates Github Releases and uploads assets
import argparse
import logging
import os
import shutil
import hashlib
import requests
from github import Github, GithubException, UnknownObjectException
@@ -14,6 +17,36 @@ logging.basicConfig(format=FORMAT)
CLOUDFLARED_REPO = os.environ.get("GITHUB_REPO", "cloudflare/cloudflared")
GITHUB_CONFLICT_CODE = "already_exists"
BASE_KV_URL = 'https://api.cloudflare.com/client/v4/accounts/'
UPDATER_PREFIX = 'update'
def get_sha256(filename):
""" get the sha256 of a file """
sha256_hash = hashlib.sha256()
with open(filename,"rb") as f:
for byte_block in iter(lambda: f.read(4096),b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
def send_hash(pkg_hash, name, version, account, namespace, api_token):
""" send the checksum of a file to workers kv """
key = '{0}_{1}_{2}'.format(UPDATER_PREFIX, version, name)
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer " + api_token,
}
response = requests.put(
BASE_KV_URL + account + "/storage/kv/namespaces/" + namespace + "/values/" + key,
headers=headers,
data=pkg_hash
)
if response.status_code != 200:
jsonResponse = response.json()
errors = jsonResponse["errors"]
if len(errors) > 0:
raise Exception("failed to upload checksum: {0}", errors[0])
def assert_tag_exists(repo, version):
@@ -78,6 +111,15 @@ def parse_args():
parser.add_argument(
"--name", default=os.environ.get("ASSET_NAME"), help="Asset Name"
)
parser.add_argument(
"--namespace-id", default=os.environ.get("KV_NAMESPACE"), help="workersKV namespace id"
)
parser.add_argument(
"--kv-account-id", default=os.environ.get("KV_ACCOUNT"), help="workersKV account id"
)
parser.add_argument(
"--kv-api-token", default=os.environ.get("KV_API_TOKEN"), help="workersKV API Token"
)
parser.add_argument(
"--dry-run", action="store_true", help="Do not create release or upload asset"
)
@@ -99,6 +141,18 @@ def parse_args():
if not args.api_key:
logging.error("Missing API key")
is_valid = False
if not args.namespace_id:
logging.error("Missing KV namespace id")
is_valid = False
if not args.kv_account_id:
logging.error("Missing KV account id")
is_valid = False
if not args.kv_api_token:
logging.error("Missing KV API token")
is_valid = False
if is_valid:
return args
@@ -120,6 +174,22 @@ def main():
return
release.upload_asset(args.path, name=args.name)
# send the sha256 (the checksum) to workers kv
pkg_hash = get_sha256(args.path)
send_hash(pkg_hash, args.name, args.release_version, args.kv_account_id, args.namespace_id, args.kv_api_token)
# create the artifacts directory if it doesn't exist
artifact_path = os.path.join(os.getcwd(), 'artifacts')
if not os.path.isdir(artifact_path):
os.mkdir(artifact_path)
# copy the binary to the path
copy_path = os.path.join(artifact_path, args.name)
shutil.copy(args.path, copy_path)
except Exception as e:
logging.exception(e)
exit(1)
+32 -42
View File
@@ -1,22 +1,19 @@
module github.com/cloudflare/cloudflared
go 1.12
go 1.15
require (
github.com/BurntSushi/go-sumtype v0.0.0-20190304192233-fcb4a6205bdc // indirect
github.com/DATA-DOG/go-sqlmock v1.3.3
github.com/acmacalister/skittles v0.0.0-20160609003031-7423546701e1
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect
github.com/aws/aws-sdk-go v1.25.8
github.com/beorn7/perks v1.0.1 // indirect
github.com/aws/aws-sdk-go v1.34.19
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect
github.com/cloudflare/brotli-go v0.0.0-20191101163834-d34379f7ff93
github.com/cloudflare/cfssl v0.0.0-20141119014638-2f7f44e802e2 // indirect
github.com/cloudflare/golibs v0.0.0-20170913112048-333127dbecfc
github.com/coredns/coredns v1.2.0
github.com/coredns/coredns v1.7.0
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73
github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0
github.com/equinox-io/equinox v1.2.0
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect
@@ -24,55 +21,48 @@ require (
github.com/facebookgo/grace v0.0.0-20180706040059-75cf19382434
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/frankban/quicktest v1.10.0 // indirect
github.com/fsnotify/fsnotify v1.4.9
github.com/gdamore/tcell v1.3.0
github.com/getsentry/raven-go v0.0.0-20180517221441-ed7bcb39ff10
github.com/gliderlabs/ssh v0.0.0-20191009160644-63518b5243e0
github.com/go-sql-driver/mysql v1.4.1
github.com/go-sql-driver/mysql v1.5.0
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
github.com/google/uuid v1.1.1
github.com/google/go-cmp v0.5.2 // indirect
github.com/google/uuid v1.1.2
github.com/gorilla/mux v1.7.3
github.com/gorilla/websocket v1.4.0
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
github.com/gorilla/websocket v1.4.2
github.com/jmoiron/sqlx v1.2.0
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/kshvakov/clickhouse v1.3.11
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lib/pq v1.2.0
github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mattn/go-isatty v0.0.10 // indirect
github.com/mattn/go-sqlite3 v1.11.0
github.com/mholt/caddy v0.0.0-20180807230124-d3b731e9255b // indirect
github.com/miekg/dns v1.1.27
github.com/miekg/dns v1.1.31
github.com/mitchellh/go-homedir v1.1.0
github.com/opentracing/opentracing-go v1.1.0 // indirect
github.com/philhofer/fwd v1.0.0 // indirect
github.com/pkg/errors v0.8.1
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pierrec/lz4 v2.5.2+incompatible // indirect
github.com/pkg/errors v0.9.1
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/prometheus/client_golang v1.0.0
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 // indirect
github.com/prometheus/common v0.7.0 // indirect
github.com/prometheus/procfs v0.0.5 // indirect
github.com/prometheus/client_golang v1.7.1
github.com/prometheus/common v0.13.0 // indirect
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 // indirect
github.com/stretchr/testify v1.3.0
github.com/tinylib/msgp v1.1.0 // indirect
github.com/rivo/tview v0.0.0-20200712113419-c65badfc3d92
github.com/stretchr/testify v1.6.0
github.com/urfave/cli/v2 v2.2.0
github.com/xo/dburl v0.0.0-20191005012637-293c3298d6c0
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/net v0.0.0-20200625001655-4c5254603344
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 // indirect
golang.org/x/sync v0.0.0-20190423024810-112230192c58
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae
google.golang.org/appengine v1.5.0 // indirect
google.golang.org/genproto v0.0.0-20191007204434-a023cd5227bd // indirect
google.golang.org/grpc v1.24.0 // indirect
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
golang.org/x/net v0.0.0-20200904194848-62affa334b73
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 // indirect
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d // indirect
google.golang.org/grpc v1.32.0 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/coreos/go-oidc.v2 v2.1.0
gopkg.in/square/go-jose.v2 v2.4.0 // indirect
gopkg.in/urfave/cli.v2 v2.0.0-20180128181224-d604b6ffeee8
gopkg.in/yaml.v2 v2.2.4
gopkg.in/yaml.v2 v2.3.0
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
zombiezen.com/go/capnproto2 v2.18.0+incompatible
)
// ../../go/pkg/mod/github.com/coredns/coredns@v1.2.0/plugin/metrics/metrics.go:40:49: too many arguments in call to prometheus.NewProcessCollector
// have (int, string)
// want (prometheus.ProcessCollectorOpts)
replace github.com/prometheus/client_golang => github.com/prometheus/client_golang v0.9.0-pre1
+835 -119
View File
File diff suppressed because it is too large Load Diff
+6 -4
View File
@@ -7,11 +7,12 @@ import (
"sync"
"time"
"github.com/cloudflare/cloudflared/logger"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/net/http2"
"golang.org/x/net/http2/hpack"
"golang.org/x/sync/errgroup"
"github.com/cloudflare/cloudflared/logger"
)
const (
@@ -21,6 +22,7 @@ const (
defaultTimeout time.Duration = 5 * time.Second
defaultRetries uint64 = 5
defaultWriteBufferMaxLen int = 1024 * 1024 // 1mb
writeBufferInitialSize int = 16 * 1024 // 16KB
SettingMuxerMagic http2.SettingID = 0x42db
MuxerMagicOrigin uint32 = 0xa2e43c8b
@@ -206,9 +208,9 @@ func Handshake(
initialStreamWindow: m.config.DefaultWindowSize,
streamWindowMax: m.config.MaxWindowSize,
streamWriteBufferMaxLen: m.config.StreamWriteBufferMaxLen,
r: m.r,
metricsUpdater: m.muxMetricsUpdater,
bytesRead: inBoundCounter,
r: m.r,
metricsUpdater: m.muxMetricsUpdater,
bytesRead: inBoundCounter,
}
m.muxWriter = &MuxWriter{
f: m.f,
+107 -1
View File
@@ -15,10 +15,11 @@ import (
"testing"
"time"
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/sync/errgroup"
"github.com/cloudflare/cloudflared/logger"
)
const (
@@ -1032,3 +1033,108 @@ func openStreams(b *testing.B, muxPair *DefaultMuxerPair, n int) {
}
assert.NoError(b, errGroup.Wait())
}
func BenchmarkSingleStreamLargeResponseBody(b *testing.B) {
const bodySize = 1 << 24
const writeBufferSize = 16 << 10
const writeN = bodySize / writeBufferSize
payload := make([]byte, writeBufferSize)
for i := range payload {
payload[i] = byte(i % 256)
}
const readBufferSize = 16 << 10
const readN = bodySize / readBufferSize
responseBody := make([]byte, readBufferSize)
f := MuxedStreamFunc(func(stream *MuxedStream) error {
if len(stream.Headers) != 1 {
b.Fatalf("expected %d headers, got %d", 1, len(stream.Headers))
}
if stream.Headers[0].Name != "test-header" {
b.Fatalf("expected header name %s, got %s", "test-header", stream.Headers[0].Name)
}
if stream.Headers[0].Value != "headerValue" {
b.Fatalf("expected header value %s, got %s", "headerValue", stream.Headers[0].Value)
}
stream.WriteHeaders([]Header{
{Name: "response-header", Value: "responseValue"},
})
for i := 0; i < writeN; i++ {
n, err := stream.Write(payload)
if err != nil {
b.Fatalf("origin write error: %s", err)
}
if n != len(payload) {
b.Fatalf("origin short write: %d/%d bytes", n, len(payload))
}
}
return nil
})
name := fmt.Sprintf("%s_%d", b.Name(), rand.Int())
origin, edge := net.Pipe()
muxPair := &DefaultMuxerPair{
OriginMuxConfig: MuxerConfig{
Timeout: testHandshakeTimeout,
Handler: f,
IsClient: true,
Name: "origin",
Logger: logger.NewOutputWriter(logger.NewMockWriteManager()),
DefaultWindowSize: defaultWindowSize,
MaxWindowSize: maxWindowSize,
StreamWriteBufferMaxLen: defaultWriteBufferMaxLen,
HeartbeatInterval: defaultTimeout,
MaxHeartbeats: defaultRetries,
},
OriginConn: origin,
EdgeMuxConfig: MuxerConfig{
Timeout: testHandshakeTimeout,
IsClient: false,
Name: "edge",
Logger: logger.NewOutputWriter(logger.NewMockWriteManager()),
DefaultWindowSize: defaultWindowSize,
MaxWindowSize: maxWindowSize,
StreamWriteBufferMaxLen: defaultWriteBufferMaxLen,
HeartbeatInterval: defaultTimeout,
MaxHeartbeats: defaultRetries,
},
EdgeConn: edge,
doneC: make(chan struct{}),
}
assert.NoError(b, muxPair.Handshake(name))
muxPair.Serve(b)
b.ReportAllocs()
for i := 0; i < b.N; i++ {
stream, err := muxPair.OpenEdgeMuxStream(
[]Header{{Name: "test-header", Value: "headerValue"}},
nil,
)
if err != nil {
b.Fatalf("error in OpenStream: %s", err)
}
if len(stream.Headers) != 1 {
b.Fatalf("expected %d headers, got %d", 1, len(stream.Headers))
}
if stream.Headers[0].Name != "response-header" {
b.Fatalf("expected header name %s, got %s", "response-header", stream.Headers[0].Name)
}
if stream.Headers[0].Value != "responseValue" {
b.Fatalf("expected header value %s, got %s", "responseValue", stream.Headers[0].Value)
}
for k := 0; k < readN; k++ {
n, err := io.ReadFull(stream, responseBody)
if err != nil {
b.Fatalf("error from (*MuxedStream).Read: %s", err)
}
if n != len(responseBody) {
b.Fatalf("expected response body to have %d bytes, got %d", len(responseBody), n)
}
}
}
}
+52 -50
View File
@@ -38,11 +38,12 @@ const (
// HTTP/1 equivalents. See https://tools.ietf.org/html/rfc7540#section-8.1.2.3
func H2RequestHeadersToH1Request(h2 []Header, h1 *http.Request) error {
for _, header := range h2 {
if !IsControlHeader(header.Name) {
name := strings.ToLower(header.Name)
if !IsControlHeader(name) {
continue
}
switch strings.ToLower(header.Name) {
switch name {
case ":method":
h1.Method = header.Value
case ":scheme":
@@ -116,18 +117,14 @@ func ParseUserHeaders(headerNameToParseFrom string, headers []Header) ([]Header,
}
func IsControlHeader(headerName string) bool {
headerName = strings.ToLower(headerName)
return headerName == "content-length" ||
headerName == "connection" || headerName == "upgrade" || // Websocket headers
strings.HasPrefix(headerName, ":") ||
strings.HasPrefix(headerName, "cf-")
}
// IsWebsocketClientHeader returns true if the header name is required by the client to upgrade properly
func IsWebsocketClientHeader(headerName string) bool {
headerName = strings.ToLower(headerName)
// isWebsocketClientHeader returns true if the header name is required by the client to upgrade properly
func isWebsocketClientHeader(headerName string) bool {
return headerName == "sec-websocket-accept" ||
headerName == "connection" ||
headerName == "upgrade"
@@ -137,29 +134,24 @@ func H1ResponseToH2ResponseHeaders(h1 *http.Response) (h2 []Header) {
h2 = []Header{
{Name: ":status", Value: strconv.Itoa(h1.StatusCode)},
}
userHeaders := http.Header{}
userHeaders := make(http.Header, len(h1.Header))
for header, values := range h1.Header {
for _, value := range values {
if strings.ToLower(header) == "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.
h2name := strings.ToLower(header)
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.
// Since these are http2 headers, they're required to be lowercase
h2 = append(h2, Header{Name: strings.ToLower(header), Value: value})
} else if !IsControlHeader(header) || IsWebsocketClientHeader(header) {
// 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
if _, ok := userHeaders[header]; ok {
userHeaders[header] = append(userHeaders[header], value)
} else {
userHeaders[header] = []string{value}
}
}
// Since these are http2 headers, they're required to be lowercase
h2 = append(h2, Header{Name: "content-length", Value: values[0]})
} else if !IsControlHeader(h2name) || 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[header] = values
}
}
// Perform user header serialization and set them in the single header
h2 = append(h2, CreateSerializedHeaders(ResponseUserHeadersField, userHeaders)...)
h2 = append(h2, Header{ResponseUserHeadersField, SerializeHeaders(userHeaders)})
return h2
}
@@ -167,26 +159,48 @@ func H1ResponseToH2ResponseHeaders(h1 *http.Response) (h2 []Header) {
// Serialize HTTP1.x headers by base64-encoding each header name and value,
// and then joining them in the format of [key:value;]
func SerializeHeaders(h1Headers http.Header) string {
var serializedHeaders []string
// compute size of the fully serialized value and largest temp buffer we will need
serializedLen := 0
maxTempLen := 0
for headerName, headerValues := range h1Headers {
for _, headerValue := range headerValues {
encodedName := make([]byte, headerEncoding.EncodedLen(len(headerName)))
headerEncoding.Encode(encodedName, []byte(headerName))
nameLen := headerEncoding.EncodedLen(len(headerName))
valueLen := headerEncoding.EncodedLen(len(headerValue))
const delims = 2
serializedLen += delims + nameLen + valueLen
if nameLen > maxTempLen {
maxTempLen = nameLen
}
if valueLen > maxTempLen {
maxTempLen = valueLen
}
}
}
var buf strings.Builder
buf.Grow(serializedLen)
encodedValue := make([]byte, headerEncoding.EncodedLen(len(headerValue)))
headerEncoding.Encode(encodedValue, []byte(headerValue))
temp := make([]byte, maxTempLen)
writeB64 := func(s string) {
n := headerEncoding.EncodedLen(len(s))
if n > len(temp) {
temp = make([]byte, n)
}
headerEncoding.Encode(temp[:n], []byte(s))
buf.Write(temp[:n])
}
serializedHeaders = append(
serializedHeaders,
strings.Join(
[]string{string(encodedName), string(encodedValue)},
":",
),
)
for headerName, headerValues := range h1Headers {
for _, headerValue := range headerValues {
if buf.Len() > 0 {
buf.WriteByte(';')
}
writeB64(headerName)
buf.WriteByte(':')
writeB64(headerValue)
}
}
return strings.Join(serializedHeaders, ";")
return buf.String()
}
// Deserialize headers serialized by `SerializeHeader`
@@ -225,18 +239,6 @@ func DeserializeHeaders(serializedHeaders string) ([]Header, error) {
return deserialized, nil
}
func CreateSerializedHeaders(headersField string, headers ...http.Header) []Header {
var serializedHeaderChunks []string
for _, headerChunk := range headers {
serializedHeaderChunks = append(serializedHeaderChunks, SerializeHeaders(headerChunk))
}
return []Header{{
headersField,
strings.Join(serializedHeaderChunks, ";"),
}}
}
type ResponseMetaHeader struct {
Source string `json:"src"`
}
+45 -23
View File
@@ -37,12 +37,19 @@ func TestH2RequestHeadersToH1Request_RegularHeaders(t *testing.T) {
"Mock header 2": {"Mock value 2"},
}
headersConversionErr := H2RequestHeadersToH1Request(CreateSerializedHeaders(RequestUserHeadersField, mockHeaders), request)
headersConversionErr := H2RequestHeadersToH1Request(createSerializedHeaders(RequestUserHeadersField, mockHeaders), request)
assert.True(t, reflect.DeepEqual(mockHeaders, request.Header))
assert.NoError(t, headersConversionErr)
}
func createSerializedHeaders(headersField string, headers http.Header) []Header {
return []Header{{
headersField,
SerializeHeaders(headers),
}}
}
func TestH2RequestHeadersToH1Request_NoHeaders(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
assert.NoError(t, err)
@@ -365,18 +372,18 @@ func randomASCIIPrintableChar(rand *rand.Rand) int {
// between 1 and `maxLength`.
func randomASCIIText(rand *rand.Rand, minLength int, maxLength int) string {
length := minLength + rand.Intn(maxLength)
result := ""
var result strings.Builder
for i := 0; i < length; i++ {
c := randomASCIIPrintableChar(rand)
// 1/4 chance of using percent encoding when not necessary
if c == '%' || rand.Intn(4) == 0 {
result += fmt.Sprintf("%%%02X", c)
result.WriteString(fmt.Sprintf("%%%02X", c))
} else {
result += string(c)
result.WriteByte(byte(c))
}
}
return result
return result.String()
}
// Calls `randomASCIIText` and ensures the result is a valid URL path,
@@ -509,40 +516,32 @@ func TestParseHeaders(t *testing.T) {
}
mockHeaders := []Header{
{Name: "One", Value: "1"},
{Name: "One", Value: "1"}, // will be dropped
{Name: "Cf-Two", Value: "cf-value-1"},
{Name: "Cf-Two", Value: "cf-value-2"},
{Name: RequestUserHeadersField, Value: SerializeHeaders(mockUserHeadersToSerialize)},
}
expectedHeaders := []Header{
{Name: "Cf-Two", Value: "cf-value-1"},
{Name: "Cf-Two", Value: "cf-value-2"},
{Name: "Mock-Header-One", Value: "1"},
{Name: "Mock-Header-One", Value: "1.5"},
{Name: "Mock-Header-Two", Value: "2"},
{Name: "Mock-Header-Three", Value: "3"},
}
parsedHeaders, err := ParseUserHeaders(RequestUserHeadersField, mockHeaders)
assert.NoError(t, err)
assert.ElementsMatch(t, expectedHeaders, parsedHeaders)
}
func TestParseHeadersNoSerializedHeader(t *testing.T) {
mockHeaders := []Header{
{Name: "One", Value: "1"},
{Name: "Cf-Two", Value: "cf-value-1"},
{Name: "Cf-Two", Value: "cf-value-2"},
h1 := &http.Request{
Header: make(http.Header),
}
_, err := ParseUserHeaders(RequestUserHeadersField, mockHeaders)
assert.EqualError(t, err, fmt.Sprintf("%s header not found", RequestUserHeadersField))
err := H2RequestHeadersToH1Request(mockHeaders, h1)
assert.NoError(t, err)
assert.ElementsMatch(t, expectedHeaders, stdlibHeaderToH2muxHeader(h1.Header))
}
func TestIsControlHeader(t *testing.T) {
controlHeaders := []string{
// Anything that begins with cf-
"cf-sample-header",
"CF-SAMPLE-HEADER",
"Cf-Sample-Header",
// Any http2 pseudoheader
":sample-pseudo-header",
@@ -559,8 +558,8 @@ func TestIsControlHeader(t *testing.T) {
func TestIsNotControlHeader(t *testing.T) {
notControlHeaders := []string{
"Mock-header",
"Another-sample-header",
"mock-header",
"another-sample-header",
}
for _, header := range notControlHeaders {
@@ -650,3 +649,26 @@ func randSeq(n int) string {
}
return string(b)
}
func BenchmarkH1ResponseToH2ResponseHeaders(b *testing.B) {
ser := "eC1mb3J3YXJkZWQtcHJvdG8:aHR0cHM;dXBncmFkZS1pbnNlY3VyZS1yZXF1ZXN0cw:MQ;YWNjZXB0LWxhbmd1YWdl:ZW4tVVMsZW47cT0wLjkscnU7cT0wLjg;YWNjZXB0LWVuY29kaW5n:Z3ppcA;eC1mb3J3YXJkZWQtZm9y:MTczLjI0NS42MC42;dXNlci1hZ2VudA:TW96aWxsYS81LjAgKE1hY2ludG9zaDsgSW50ZWwgTWFjIE9TIFggMTBfMTRfNikgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzg0LjAuNDE0Ny44OSBTYWZhcmkvNTM3LjM2;c2VjLWZldGNoLW1vZGU:bmF2aWdhdGU;Y2RuLWxvb3A:Y2xvdWRmbGFyZQ;c2VjLWZldGNoLWRlc3Q:ZG9jdW1lbnQ;c2VjLWZldGNoLXVzZXI:PzE;c2VjLWZldGNoLXNpdGU:bm9uZQ;Y29va2ll:X19jZmR1aWQ9ZGNkOWZjOGNjNWMxMzE0NTMyYTFkMjhlZDEyOWRhOTYwMTU2OTk1MTYzNDsgX19jZl9ibT1mYzY2MzMzYzAzZmM0MWFiZTZmOWEyYzI2ZDUwOTA0YzIxYzZhMTQ2LTE1OTU2MjIzNDEtMTgwMC1BZTVzS2pIU2NiWGVFM05mMUhrTlNQMG1tMHBLc2pQWkloVnM1Z2g1SkNHQkFhS1UxVDB2b003alBGN3FjMHVSR2NjZGcrWHdhL1EzbTJhQzdDVU4xZ2M9;YWNjZXB0:dGV4dC9odG1sLGFwcGxpY2F0aW9uL3hodG1sK3htbCxhcHBsaWNhdGlvbi94bWw7cT0wLjksaW1hZ2Uvd2VicCxpbWFnZS9hcG5nLCovKjtxPTAuOCxhcHBsaWNhdGlvbi9zaWduZWQtZXhjaGFuZ2U7dj1iMztxPTAuOQ"
h2, _ := DeserializeHeaders(ser)
h1 := make(http.Header)
for _, header := range h2 {
h1.Add(header.Name, header.Value)
}
h1.Add("Content-Length", "200")
h1.Add("Cf-Something", "Else")
h1.Add("Upgrade", "websocket")
h1resp := &http.Response{
StatusCode: 200,
Header: h1,
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = H1ResponseToH2ResponseHeaders(h1resp)
}
}
+31 -5
View File
@@ -138,6 +138,14 @@ func (s *MuxedStream) Write(p []byte) (int, error) {
if s.writeEOF {
return 0, io.EOF
}
// pre-allocate some space in the write buffer if possible
if buffer, ok := s.writeBuffer.(*bytes.Buffer); ok {
if buffer.Cap() == 0 {
buffer.Grow(writeBufferInitialSize)
}
}
totalWritten := 0
for totalWritten < len(p) {
// If the buffer is full, block till there is more room.
@@ -367,7 +375,9 @@ type streamChunk struct {
// true if data frames should be sent
sendData bool
eof bool
buffer bytes.Buffer
buffer []byte
offset int
}
// getChunk atomically extracts a chunk of data to be written by MuxWriter.
@@ -385,8 +395,17 @@ func (s *MuxedStream) getChunk() *streamChunk {
eof: s.writeEOF && uint32(s.writeBuffer.Len()) <= s.sendWindow,
}
// Copy at most s.sendWindow bytes, adjust the sendWindow accordingly
writeLen, _ := io.CopyN(&chunk.buffer, s.writeBuffer, int64(s.sendWindow))
s.sendWindow -= uint32(writeLen)
toCopy := int(s.sendWindow)
if toCopy > s.writeBuffer.Len() {
toCopy = s.writeBuffer.Len()
}
if toCopy > 0 {
buf := make([]byte, toCopy)
writeLen, _ := s.writeBuffer.Read(buf)
chunk.buffer = buf[:writeLen]
s.sendWindow -= uint32(writeLen)
}
// Allow MuxedStream::Write() to continue, if needed
if s.writeBuffer.Len() < s.writeBufferMaxLen {
@@ -421,8 +440,15 @@ func (c *streamChunk) sendDataFrame() bool {
}
func (c *streamChunk) nextDataFrame(frameSize int) (payload []byte, endStream bool) {
payload = c.buffer.Next(frameSize)
if c.buffer.Len() == 0 {
bytesLeft := len(c.buffer) - c.offset
if frameSize > bytesLeft {
frameSize = bytesLeft
}
nextOffset := c.offset + frameSize
payload = c.buffer[c.offset:nextOffset]
c.offset = nextOffset
if c.offset == len(c.buffer) {
// this is the last data frame in this chunk
c.sendData = false
if c.eof {
+9
View File
@@ -0,0 +1,9 @@
---
basename: "cloudflared"
comment: "Cloudflare Argo Tunnel"
copyright: "Cloudflare, Inc"
arch: "x86"
abi: "64"
files:
- source: cloudflared
destination: /var/db/scripts/jet/cloudflared
+22 -10
View File
@@ -3,6 +3,7 @@ package logger
import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
@@ -97,31 +98,32 @@ func Parse(opts ...Option) (*Options, error) {
// New setups a new logger based on the options.
// The default behavior is to write to standard out
func New(opts ...Option) (Service, error) {
config, err := Parse(opts...)
func New(opts ...Option) (*OutputWriter, error) {
options, err := Parse(opts...)
if err != nil {
return nil, err
}
l := NewOutputWriter(SharedWriteManager)
if config.logFileDirectory != "" {
l.Add(NewFileRollingWriter(config.logFileDirectory,
if options.logFileDirectory != "" {
l.Add(NewFileRollingWriter(SanitizeLogPath(options.logFileDirectory),
"cloudflared",
int64(config.maxFileSize),
config.maxFileCount),
NewDefaultFormatter(time.RFC3339Nano), config.supportedFileLevels...)
int64(options.maxFileSize),
options.maxFileCount),
NewDefaultFormatter(time.RFC3339Nano), options.supportedFileLevels...)
}
if !config.terminalOutputDisabled {
if !options.terminalOutputDisabled {
terminalFormatter := NewTerminalFormatter(time.RFC3339)
if len(config.supportedTerminalLevels) == 0 {
if len(options.supportedTerminalLevels) == 0 {
l.Add(os.Stderr, terminalFormatter, InfoLevel, ErrorLevel, FatalLevel)
} else {
l.Add(os.Stderr, terminalFormatter, config.supportedTerminalLevels...)
l.Add(os.Stderr, terminalFormatter, options.supportedTerminalLevels...)
}
}
return l, nil
}
@@ -139,3 +141,13 @@ func ParseLevelString(lvl string) ([]Level, error) {
}
return []Level{}, fmt.Errorf("not a valid log level: %q", lvl)
}
// SanitizeLogPath checks that the logger log path
func SanitizeLogPath(path string) string {
newPath := strings.TrimSpace(path)
// make sure it has a log file extension and is not a directory
if filepath.Ext(newPath) != ".log" && !(isDirectory(newPath) || strings.HasSuffix(newPath, "/")) {
newPath = newPath + ".log"
}
return newPath
}
+46
View File
@@ -0,0 +1,46 @@
package logger
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestLogLevelParse(t *testing.T) {
lvls, err := ParseLevelString("fatal")
assert.NoError(t, err)
assert.Equal(t, []Level{FatalLevel}, lvls)
lvls, err = ParseLevelString("error")
assert.NoError(t, err)
assert.Equal(t, []Level{FatalLevel, ErrorLevel}, lvls)
lvls, err = ParseLevelString("info")
assert.NoError(t, err)
assert.Equal(t, []Level{FatalLevel, ErrorLevel, InfoLevel}, lvls)
lvls, err = ParseLevelString("info")
assert.NoError(t, err)
assert.Equal(t, []Level{FatalLevel, ErrorLevel, InfoLevel}, lvls)
lvls, err = ParseLevelString("warn")
assert.NoError(t, err)
assert.Equal(t, []Level{FatalLevel, ErrorLevel, InfoLevel}, lvls)
lvls, err = ParseLevelString("debug")
assert.NoError(t, err)
assert.Equal(t, []Level{FatalLevel, ErrorLevel, InfoLevel, DebugLevel}, lvls)
_, err = ParseLevelString("blah")
assert.Error(t, err)
_, err = ParseLevelString("")
assert.Error(t, err)
}
func TestPathSanitizer(t *testing.T) {
assert.Equal(t, "somebad/path/log.bat.log", SanitizeLogPath("\t somebad/path/log.bat\n\n"))
assert.Equal(t, "proper/path/cloudflared.log", SanitizeLogPath("proper/path/cloudflared.log"))
assert.Equal(t, "proper/path/", SanitizeLogPath("proper/path/"))
assert.Equal(t, "proper/path/cloudflared.log", SanitizeLogPath("\tproper/path/cloudflared\n\n"))
}
+42 -7
View File
@@ -8,17 +8,17 @@ import (
"github.com/acmacalister/skittles"
)
// Level of logging
// Level of logging, lower number means more verbose logging, higher more terse
type Level int
const (
// InfoLevel is for standard log messages
InfoLevel Level = iota
// DebugLevel is for messages that are intended for purposes debugging only
DebugLevel
DebugLevel Level = iota
// ErrorLevel is for error message to indicte something has gone wrong
// InfoLevel is for standard log messages
InfoLevel
// ErrorLevel is for error message to indicate something has gone wrong
ErrorLevel
// FatalLevel is for error message that log and kill the program with an os.exit(1)
@@ -63,6 +63,12 @@ type TerminalFormatter struct {
supportsColor bool
}
// UIFormatter is used for streaming logs to UI
type UIFormatter struct {
format string
supportsColor bool
}
// NewTerminalFormatter creates a Terminal formatter for colored output
// format is the time format to use for timestamp formatting
func NewTerminalFormatter(format string) Formatter {
@@ -73,10 +79,39 @@ func NewTerminalFormatter(format string) Formatter {
}
}
func NewUIFormatter(format string) Formatter {
supportsColor := (runtime.GOOS != "windows")
return &UIFormatter{
format: format,
supportsColor: supportsColor,
}
}
// Timestamp uses formatting that is tview-specific for UI
func (f *UIFormatter) Timestamp(l Level, d time.Time) string {
t := ""
dateStr := "[" + d.Format(f.format) + "] "
switch l {
case InfoLevel:
t = "[#00ffff]INFO[white]"
case ErrorLevel:
t = "[red]ERROR[white]"
case DebugLevel:
t = "[yellow]DEBUG[white]"
case FatalLevel:
t = "[red]FATAL[white]"
}
return t + dateStr
}
func (f *UIFormatter) Content(l Level, c string) string {
return c
}
// Timestamp returns the log level with a matching color to the log type
func (f *TerminalFormatter) Timestamp(l Level, d time.Time) string {
t := ""
dateStr := "[" + d.Format(f.format) + "] "
dateStr := "[" + d.Format(f.format) + "] "
switch l {
case InfoLevel:
t = f.output("INFO", skittles.Cyan)
+5 -5
View File
@@ -7,8 +7,8 @@ import "sync"
var SharedWriteManager = NewWriteManager()
type writeData struct {
writeFunc func([]byte)
data []byte
target LogOutput
data []byte
}
// WriteManager is a logging service that handles managing multiple writing streams
@@ -31,9 +31,9 @@ func NewWriteManager() OutputManager {
}
// Append adds a message to the writer runloop
func (m *WriteManager) Append(data []byte, callback func([]byte)) {
func (m *WriteManager) Append(data []byte, target LogOutput) {
m.wg.Add(1)
m.writeChan <- writeData{data: data, writeFunc: callback}
m.writeChan <- writeData{data: data, target: target}
}
// Shutdown stops the sync manager service
@@ -49,7 +49,7 @@ func (m *WriteManager) run() {
select {
case event, ok := <-m.writeChan:
if ok {
event.writeFunc(event.data)
event.target.WriteLogLine(event.data)
m.wg.Done()
}
case <-m.shutdown:
+8 -2
View File
@@ -6,13 +6,19 @@ import (
"github.com/stretchr/testify/assert"
)
type outputFunc func(b []byte)
func (f outputFunc) WriteLogLine(data []byte) {
f(data)
}
func TestWriteManger(t *testing.T) {
testData := []byte(string("hello Austin, how are you doing?"))
waitChan := make(chan []byte)
m := NewWriteManager()
m.Append(testData, func(b []byte) {
m.Append(testData, outputFunc(func(b []byte) {
waitChan <- b
})
}))
resp := <-waitChan
assert.Equal(t, testData, resp)
}
+1 -1
View File
@@ -10,7 +10,7 @@ func NewMockWriteManager() OutputManager {
}
// Append is a mock stub
func (m *MockWriteManager) Append(data []byte, callback func([]byte)) {
func (m *MockWriteManager) Append(data []byte, target LogOutput) {
}
// Shutdown is a mock stub
+58 -33
View File
@@ -10,9 +10,13 @@ import (
// provided for testing
var osExit = os.Exit
type LogOutput interface {
WriteLogLine([]byte)
}
// OutputManager is used to sync data of Output
type OutputManager interface {
Append([]byte, func([]byte))
Append([]byte, LogOutput)
Shutdown()
}
@@ -27,6 +31,8 @@ type Service interface {
Infof(format string, args ...interface{})
Debugf(format string, args ...interface{})
Fatalf(format string, args ...interface{})
Add(writer io.Writer, formatter Formatter, levels ...Level)
}
type sourceGroup struct {
@@ -35,38 +41,66 @@ type sourceGroup struct {
levelsSupported []Level
}
// OutputWriter is the standard logging implementation
type OutputWriter struct {
groups []sourceGroup
syncWriter OutputManager
func (s *sourceGroup) WriteLogLine(data []byte) {
_, _ = s.writer.Write(data)
}
// NewOutputWriter create a new logger
func (s *sourceGroup) supportsLevel(l Level) bool {
for _, level := range s.levelsSupported {
if l == level {
return true
}
}
return false
}
// OutputWriter is the standard logging implementation
type OutputWriter struct {
groups []*sourceGroup
syncWriter OutputManager
minLevel Level
}
// NewOutputWriter creates a new logger
func NewOutputWriter(syncWriter OutputManager) *OutputWriter {
return &OutputWriter{
syncWriter: syncWriter,
groups: make([]sourceGroup, 0),
groups: nil,
minLevel: FatalLevel,
}
}
// Add a writer and formatter to output to
func (s *OutputWriter) Add(writer io.Writer, formatter Formatter, levels ...Level) {
s.groups = append(s.groups, sourceGroup{writer: writer, formatter: formatter, levelsSupported: levels})
s.groups = append(s.groups, &sourceGroup{writer: writer, formatter: formatter, levelsSupported: levels})
// track most verbose (lowest) level we need to output
for _, level := range levels {
if level < s.minLevel {
s.minLevel = level
}
}
}
// Error writes an error to the logging sources
func (s *OutputWriter) Error(message string) {
s.output(ErrorLevel, message)
if s.minLevel <= ErrorLevel {
s.output(ErrorLevel, message)
}
}
// Info writes an info string to the logging sources
func (s *OutputWriter) Info(message string) {
s.output(InfoLevel, message)
if s.minLevel <= InfoLevel {
s.output(InfoLevel, message)
}
}
// Debug writes a debug string to the logging sources
func (s *OutputWriter) Debug(message string) {
s.output(DebugLevel, message)
if s.minLevel <= DebugLevel {
s.output(DebugLevel, message)
}
}
// Fatal writes a error string to the logging sources and runs does an os.exit()
@@ -78,17 +112,23 @@ func (s *OutputWriter) Fatal(message string) {
// Errorf writes a formatted error to the logging sources
func (s *OutputWriter) Errorf(format string, args ...interface{}) {
s.output(ErrorLevel, fmt.Sprintf(format, args...))
if s.minLevel <= ErrorLevel {
s.output(ErrorLevel, fmt.Sprintf(format, args...))
}
}
// Infof writes a formatted info statement to the logging sources
func (s *OutputWriter) Infof(format string, args ...interface{}) {
s.output(InfoLevel, fmt.Sprintf(format, args...))
if s.minLevel <= InfoLevel {
s.output(InfoLevel, fmt.Sprintf(format, args...))
}
}
// Debugf writes a formatted debug statement to the logging sources
func (s *OutputWriter) Debugf(format string, args ...interface{}) {
s.output(DebugLevel, fmt.Sprintf(format, args...))
if s.minLevel <= DebugLevel {
s.output(DebugLevel, fmt.Sprintf(format, args...))
}
}
// Fatalf writes a writes a formatted error statement and runs does an os.exit()
@@ -100,31 +140,16 @@ func (s *OutputWriter) Fatalf(format string, args ...interface{}) {
// output does the actual write to the sync manager
func (s *OutputWriter) output(l Level, content string) {
now := time.Now()
for _, group := range s.groups {
if isSupported(group, l) {
logLine := fmt.Sprintf("%s%s\n", group.formatter.Timestamp(l, time.Now()),
if group.supportsLevel(l) {
logLine := fmt.Sprintf("%s%s\n", group.formatter.Timestamp(l, now),
group.formatter.Content(l, content))
s.append(group, []byte(logLine))
s.syncWriter.Append([]byte(logLine), group)
}
}
}
func (s *OutputWriter) append(group sourceGroup, logLine []byte) {
s.syncWriter.Append(logLine, func(b []byte) {
group.writer.Write(b)
})
}
// isSupported checks if the log level is supported
func isSupported(group sourceGroup, l Level) bool {
for _, level := range group.levelsSupported {
if l == level {
return true
}
}
return false
}
// Write implements io.Writer to support SetOutput of the log package
func (s *OutputWriter) Write(p []byte) (n int, err error) {
s.Info(string(p))
+2
View File
@@ -55,6 +55,8 @@ func TestOutputWrite(t *testing.T) {
logger := NewOutputWriter(m)
logger.Add(&testBuffer, f, InfoLevel)
logger.Debugf("debug message not logged here")
testData := "hello Bob Bork, how are you doing?"
logger.Info(testData)
testTime := f.Timestamp(InfoLevel, time.Now())
-31
View File
@@ -1,31 +0,0 @@
#!/bin/bash
TARGET_DIRECTORY=".build"
BINARY_NAME="cloudflared"
VERSION=$(git describe --tags --always --dirty="-dev")
PRODUCT="cloudflared"
echo "building cloudflared"
make cloudflared
echo "creating build directory"
mkdir ${TARGET_DIRECTORY}
mkdir ${TARGET_DIRECTORY}/contents
cp -r .mac_resources/scripts ${TARGET_DIRECTORY}/scripts
echo "move cloudflared into the build directory"
mv $BINARY_NAME {$TARGET_DIRECTORY}/contents/${PRODUCT}
echo "build the installer package"
pkgbuild --identifier com.cloudflare.${PRODUCT} \
--version ${VERSION} \
--scripts ${TARGET_DIRECTORY}/scripts \
--root ${TARGET_DIRECTORY}/contents \
--install-location /usr/local/bin \
${PRODUCT}.pkg
# TODO: our iOS/Mac account doesn't have this installer certificate type.
# need to find how we can get it --sign "Developer ID Installer: Cloudflare" \
echo "cleaning up the build directory"
rm -rf $TARGET_DIRECTORY
+3 -28
View File
@@ -58,11 +58,9 @@ type TunnelMetrics struct {
// oldServerLocations stores the last server the tunnel was connected to
oldServerLocations map[string]string
regSuccess *prometheus.CounterVec
regFail *prometheus.CounterVec
authSuccess prometheus.Counter
authFail *prometheus.CounterVec
rpcFail *prometheus.CounterVec
regSuccess *prometheus.CounterVec
regFail *prometheus.CounterVec
rpcFail *prometheus.CounterVec
muxerMetrics *muxerMetrics
tunnelsHA tunnelsForHA
@@ -456,27 +454,6 @@ func NewTunnelMetrics() *TunnelMetrics {
)
prometheus.MustRegister(registerSuccess)
authSuccess := prometheus.NewCounter(
prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: tunnelSubsystem,
Name: "tunnel_authenticate_success",
Help: "Count of successful tunnel authenticate",
},
)
prometheus.MustRegister(authSuccess)
authFail := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: tunnelSubsystem,
Name: "tunnel_authenticate_fail",
Help: "Count of tunnel authenticate errors by type",
},
[]string{"error"},
)
prometheus.MustRegister(authFail)
return &TunnelMetrics{
haConnections: haConnections,
activeStreams: activeStreams,
@@ -497,8 +474,6 @@ func NewTunnelMetrics() *TunnelMetrics {
regFail: registerFail,
rpcFail: rpcFail,
userHostnamesCounts: userHostnamesCounts,
authSuccess: authSuccess,
authFail: authFail,
}
}
+191
View File
@@ -0,0 +1,191 @@
package origin
import (
"context"
"errors"
"fmt"
"sync"
"time"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tunnelrpc"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
)
var (
errJWTUnset = errors.New("JWT unset")
)
// reconnectTunnelCredentialManager is invoked by functions in tunnel.go to
// get/set parameters for ReconnectTunnel RPC calls.
type reconnectCredentialManager struct {
mu sync.RWMutex
jwt []byte
eventDigest map[uint8][]byte
connDigest map[uint8][]byte
authSuccess prometheus.Counter
authFail *prometheus.CounterVec
}
func newReconnectCredentialManager(namespace, subsystem string, haConnections int) *reconnectCredentialManager {
authSuccess := prometheus.NewCounter(
prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "tunnel_authenticate_success",
Help: "Count of successful tunnel authenticate",
},
)
authFail := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "tunnel_authenticate_fail",
Help: "Count of tunnel authenticate errors by type",
},
[]string{"error"},
)
prometheus.MustRegister(authSuccess, authFail)
return &reconnectCredentialManager{
eventDigest: make(map[uint8][]byte, haConnections),
connDigest: make(map[uint8][]byte, haConnections),
authSuccess: authSuccess,
authFail: authFail,
}
}
func (cm *reconnectCredentialManager) ReconnectToken() ([]byte, error) {
cm.mu.RLock()
defer cm.mu.RUnlock()
if cm.jwt == nil {
return nil, errJWTUnset
}
return cm.jwt, nil
}
func (cm *reconnectCredentialManager) SetReconnectToken(jwt []byte) {
cm.mu.Lock()
defer cm.mu.Unlock()
cm.jwt = jwt
}
func (cm *reconnectCredentialManager) EventDigest(connID uint8) ([]byte, error) {
cm.mu.RLock()
defer cm.mu.RUnlock()
digest, ok := cm.eventDigest[connID]
if !ok {
return nil, fmt.Errorf("no event digest for connection %v", connID)
}
return digest, nil
}
func (cm *reconnectCredentialManager) SetEventDigest(connID uint8, digest []byte) {
cm.mu.Lock()
defer cm.mu.Unlock()
cm.eventDigest[connID] = digest
}
func (cm *reconnectCredentialManager) ConnDigest(connID uint8) ([]byte, error) {
cm.mu.RLock()
defer cm.mu.RUnlock()
digest, ok := cm.connDigest[connID]
if !ok {
return nil, fmt.Errorf("no conneciton digest for connection %v", connID)
}
return digest, nil
}
func (cm *reconnectCredentialManager) SetConnDigest(connID uint8, digest []byte) {
cm.mu.Lock()
defer cm.mu.Unlock()
cm.connDigest[connID] = digest
}
func (cm *reconnectCredentialManager) RefreshAuth(
ctx context.Context,
backoff *BackoffHandler,
authenticate func(ctx context.Context, numPreviousAttempts int) (tunnelpogs.AuthOutcome, error),
) (retryTimer <-chan time.Time, err error) {
authOutcome, err := authenticate(ctx, backoff.Retries())
if err != nil {
cm.authFail.WithLabelValues(err.Error()).Inc()
if _, ok := backoff.GetBackoffDuration(ctx); ok {
return backoff.BackoffTimer(), nil
}
return nil, err
}
// clear backoff timer
backoff.SetGracePeriod()
switch outcome := authOutcome.(type) {
case tunnelpogs.AuthSuccess:
cm.SetReconnectToken(outcome.JWT())
cm.authSuccess.Inc()
return timeAfter(outcome.RefreshAfter()), nil
case tunnelpogs.AuthUnknown:
duration := outcome.RefreshAfter()
cm.authFail.WithLabelValues(outcome.Error()).Inc()
return timeAfter(duration), nil
case tunnelpogs.AuthFail:
cm.authFail.WithLabelValues(outcome.Error()).Inc()
return nil, outcome
default:
err := fmt.Errorf("refresh_auth: Unexpected outcome type %T", authOutcome)
cm.authFail.WithLabelValues(err.Error()).Inc()
return nil, err
}
}
func ReconnectTunnel(
ctx context.Context,
muxer *h2mux.Muxer,
config *TunnelConfig,
logger logger.Service,
connectionID uint8,
originLocalAddr string,
uuid uuid.UUID,
credentialManager *reconnectCredentialManager,
) error {
token, err := credentialManager.ReconnectToken()
if err != nil {
return err
}
eventDigest, err := credentialManager.EventDigest(connectionID)
if err != nil {
return err
}
connDigest, err := credentialManager.ConnDigest(connectionID)
if err != nil {
return err
}
config.TransportLogger.Debug("initiating RPC stream to reconnect")
tunnelServer, err := connection.NewRPCClient(ctx, muxer, config.TransportLogger, openStreamTimeout)
if err != nil {
// RPC stream open error
return newClientRegisterTunnelError(err, config.Metrics.rpcFail, reconnect)
}
defer tunnelServer.Close()
// Request server info without blocking tunnel registration; must use capnp library directly.
serverInfoPromise := tunnelrpc.TunnelServer{Client: tunnelServer.Client}.GetServerInfo(ctx, func(tunnelrpc.TunnelServer_getServerInfo_Params) error {
return nil
})
LogServerInfo(serverInfoPromise.Result(), connectionID, config.Metrics, logger, config.TunnelEventChan)
registration := tunnelServer.ReconnectTunnel(
ctx,
token,
eventDigest,
connDigest,
config.Hostname,
config.RegistrationOptions(connectionID, originLocalAddr, uuid),
)
if registrationErr := registration.DeserializeError(); registrationErr != nil {
// ReconnectTunnel RPC failure
return processRegisterTunnelError(registrationErr, config.Metrics, reconnect)
}
return processRegistrationSuccess(config, logger, connectionID, registration, reconnect, credentialManager)
}
@@ -7,51 +7,19 @@ import (
"testing"
"time"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"github.com/cloudflare/cloudflared/logger"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
func testConfig(logger logger.Service) *TunnelConfig {
metrics := TunnelMetrics{}
metrics.authSuccess = prometheus.NewCounter(
prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: tunnelSubsystem,
Name: "tunnel_authenticate_success",
Help: "Count of successful tunnel authenticate",
},
)
metrics.authFail = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: tunnelSubsystem,
Name: "tunnel_authenticate_fail",
Help: "Count of tunnel authenticate errors by type",
},
[]string{"error"},
)
return &TunnelConfig{Logger: logger, Metrics: &metrics}
}
func TestRefreshAuthBackoff(t *testing.T) {
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
rcm := newReconnectCredentialManager(t.Name(), t.Name(), 4)
var wait time.Duration
timeAfter = func(d time.Duration) <-chan time.Time {
wait = d
return time.After(d)
}
s, err := NewSupervisor(testConfig(logger), uuid.New())
if !assert.NoError(t, err) {
t.FailNow()
}
backoff := &BackoffHandler{MaxRetries: 3}
auth := func(ctx context.Context, n int) (tunnelpogs.AuthOutcome, error) {
return nil, fmt.Errorf("authentication failure")
@@ -59,17 +27,17 @@ func TestRefreshAuthBackoff(t *testing.T) {
// authentication failures should consume the backoff
for i := uint(0); i < backoff.MaxRetries; i++ {
retryChan, err := s.refreshAuth(context.Background(), backoff, auth)
retryChan, err := rcm.RefreshAuth(context.Background(), backoff, auth)
assert.NoError(t, err)
assert.NotNil(t, retryChan)
assert.Equal(t, (1<<i)*time.Second, wait)
}
retryChan, err := s.refreshAuth(context.Background(), backoff, auth)
retryChan, err := rcm.RefreshAuth(context.Background(), backoff, auth)
assert.Error(t, err)
assert.Nil(t, retryChan)
// now we actually make contact with the remote server
_, _ = s.refreshAuth(context.Background(), backoff, func(ctx context.Context, n int) (tunnelpogs.AuthOutcome, error) {
_, _ = rcm.RefreshAuth(context.Background(), backoff, func(ctx context.Context, n int) (tunnelpogs.AuthOutcome, error) {
return tunnelpogs.NewAuthUnknown(errors.New("auth unknown"), 19), nil
})
@@ -84,7 +52,7 @@ func TestRefreshAuthBackoff(t *testing.T) {
}
func TestRefreshAuthSuccess(t *testing.T) {
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
rcm := newReconnectCredentialManager(t.Name(), t.Name(), 4)
var wait time.Duration
timeAfter = func(d time.Duration) <-chan time.Time {
@@ -92,27 +60,23 @@ func TestRefreshAuthSuccess(t *testing.T) {
return time.After(d)
}
s, err := NewSupervisor(testConfig(logger), uuid.New())
if !assert.NoError(t, err) {
t.FailNow()
}
backoff := &BackoffHandler{MaxRetries: 3}
auth := func(ctx context.Context, n int) (tunnelpogs.AuthOutcome, error) {
return tunnelpogs.NewAuthSuccess([]byte("jwt"), 19), nil
}
retryChan, err := s.refreshAuth(context.Background(), backoff, auth)
retryChan, err := rcm.RefreshAuth(context.Background(), backoff, auth)
assert.NoError(t, err)
assert.NotNil(t, retryChan)
assert.Equal(t, 19*time.Hour, wait)
token, err := s.ReconnectToken()
token, err := rcm.ReconnectToken()
assert.NoError(t, err)
assert.Equal(t, []byte("jwt"), token)
}
func TestRefreshAuthUnknown(t *testing.T) {
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
rcm := newReconnectCredentialManager(t.Name(), t.Name(), 4)
var wait time.Duration
timeAfter = func(d time.Duration) <-chan time.Time {
@@ -120,42 +84,34 @@ func TestRefreshAuthUnknown(t *testing.T) {
return time.After(d)
}
s, err := NewSupervisor(testConfig(logger), uuid.New())
if !assert.NoError(t, err) {
t.FailNow()
}
backoff := &BackoffHandler{MaxRetries: 3}
auth := func(ctx context.Context, n int) (tunnelpogs.AuthOutcome, error) {
return tunnelpogs.NewAuthUnknown(errors.New("auth unknown"), 19), nil
}
retryChan, err := s.refreshAuth(context.Background(), backoff, auth)
retryChan, err := rcm.RefreshAuth(context.Background(), backoff, auth)
assert.NoError(t, err)
assert.NotNil(t, retryChan)
assert.Equal(t, 19*time.Hour, wait)
token, err := s.ReconnectToken()
token, err := rcm.ReconnectToken()
assert.Equal(t, errJWTUnset, err)
assert.Nil(t, token)
}
func TestRefreshAuthFail(t *testing.T) {
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
rcm := newReconnectCredentialManager(t.Name(), t.Name(), 4)
s, err := NewSupervisor(testConfig(logger), uuid.New())
if !assert.NoError(t, err) {
t.FailNow()
}
backoff := &BackoffHandler{MaxRetries: 3}
auth := func(ctx context.Context, n int) (tunnelpogs.AuthOutcome, error) {
return tunnelpogs.NewAuthFail(errors.New("auth fail")), nil
}
retryChan, err := s.refreshAuth(context.Background(), backoff, auth)
retryChan, err := rcm.RefreshAuth(context.Background(), backoff, auth)
assert.Error(t, err)
assert.Nil(t, retryChan)
token, err := s.ReconnectToken()
token, err := rcm.ReconnectToken()
assert.Equal(t, errJWTUnset, err)
assert.Nil(t, token)
}
+14 -108
View File
@@ -3,9 +3,7 @@ package origin
import (
"context"
"errors"
"fmt"
"net"
"sync"
"time"
"github.com/google/uuid"
@@ -37,7 +35,6 @@ const (
)
var (
errJWTUnset = errors.New("JWT unset")
errEventDigestUnset = errors.New("event digest unset")
)
@@ -58,14 +55,7 @@ type Supervisor struct {
logger logger.Service
jwtLock sync.RWMutex
jwt []byte
eventDigestLock sync.RWMutex
eventDigest []byte
connDigestLock sync.RWMutex
connDigest map[uint8][]byte
reconnectCredentialManager *reconnectCredentialManager
bufferPool *buffer.Pool
}
@@ -95,14 +85,14 @@ func NewSupervisor(config *TunnelConfig, cloudflaredUUID uuid.UUID) (*Supervisor
}
return &Supervisor{
cloudflaredUUID: cloudflaredUUID,
config: config,
edgeIPs: edgeIPs,
tunnelErrors: make(chan tunnelError),
tunnelsConnecting: map[int]chan struct{}{},
logger: config.Logger,
connDigest: make(map[uint8][]byte),
bufferPool: buffer.NewPool(512 * 1024),
cloudflaredUUID: cloudflaredUUID,
config: config,
edgeIPs: edgeIPs,
tunnelErrors: make(chan tunnelError),
tunnelsConnecting: map[int]chan struct{}{},
logger: config.Logger,
reconnectCredentialManager: newReconnectCredentialManager(metricsNamespace, tunnelSubsystem, config.HAConnections),
bufferPool: buffer.NewPool(512 * 1024),
}, nil
}
@@ -121,7 +111,7 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal, re
var refreshAuthBackoffTimer <-chan time.Time
if s.config.UseReconnectToken {
if timer, err := s.refreshAuth(ctx, refreshAuthBackoff, s.authenticate); err == nil {
if timer, err := s.reconnectCredentialManager.RefreshAuth(ctx, refreshAuthBackoff, s.authenticate); err == nil {
refreshAuthBackoffTimer = timer
} else {
logger.Errorf("supervisor: initial refreshAuth failed, retrying in %v: %s", refreshAuthRetryDuration, err)
@@ -164,7 +154,7 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal, re
tunnelsWaiting = nil
// Time to call Authenticate
case <-refreshAuthBackoffTimer:
newTimer, err := s.refreshAuth(ctx, refreshAuthBackoff, s.authenticate)
newTimer, err := s.reconnectCredentialManager.RefreshAuth(ctx, refreshAuthBackoff, s.authenticate)
if err != nil {
logger.Errorf("supervisor: Authentication failed: %s", err)
// Permanent failure. Leave the `select` without setting the
@@ -237,7 +227,7 @@ func (s *Supervisor) startFirstTunnel(ctx context.Context, connectedSignal *sign
return
}
err = ServeTunnelLoop(ctx, s, s.config, addr, firstConnIndex, connectedSignal, s.cloudflaredUUID, s.bufferPool, reconnectCh)
err = ServeTunnelLoop(ctx, s.reconnectCredentialManager, s.config, addr, firstConnIndex, connectedSignal, s.cloudflaredUUID, s.bufferPool, reconnectCh)
// If the first tunnel disconnects, keep restarting it.
edgeErrors := 0
for s.unusedIPs() {
@@ -260,7 +250,7 @@ func (s *Supervisor) startFirstTunnel(ctx context.Context, connectedSignal *sign
return
}
}
err = ServeTunnelLoop(ctx, s, s.config, addr, firstConnIndex, connectedSignal, s.cloudflaredUUID, s.bufferPool, reconnectCh)
err = ServeTunnelLoop(ctx, s.reconnectCredentialManager, s.config, addr, firstConnIndex, connectedSignal, s.cloudflaredUUID, s.bufferPool, reconnectCh)
}
}
@@ -279,7 +269,7 @@ func (s *Supervisor) startTunnel(ctx context.Context, index int, connectedSignal
if err != nil {
return
}
err = ServeTunnelLoop(ctx, s, s.config, addr, uint8(index), connectedSignal, s.cloudflaredUUID, s.bufferPool, reconnectCh)
err = ServeTunnelLoop(ctx, s.reconnectCredentialManager, s.config, addr, uint8(index), connectedSignal, s.cloudflaredUUID, s.bufferPool, reconnectCh)
}
func (s *Supervisor) newConnectedTunnelSignal(index int) *signal.Signal {
@@ -305,90 +295,6 @@ func (s *Supervisor) unusedIPs() bool {
return s.edgeIPs.AvailableAddrs() > s.config.HAConnections
}
func (s *Supervisor) ReconnectToken() ([]byte, error) {
s.jwtLock.RLock()
defer s.jwtLock.RUnlock()
if s.jwt == nil {
return nil, errJWTUnset
}
return s.jwt, nil
}
func (s *Supervisor) SetReconnectToken(jwt []byte) {
s.jwtLock.Lock()
defer s.jwtLock.Unlock()
s.jwt = jwt
}
func (s *Supervisor) EventDigest() ([]byte, error) {
s.eventDigestLock.RLock()
defer s.eventDigestLock.RUnlock()
if s.eventDigest == nil {
return nil, errEventDigestUnset
}
return s.eventDigest, nil
}
func (s *Supervisor) SetEventDigest(eventDigest []byte) {
s.eventDigestLock.Lock()
defer s.eventDigestLock.Unlock()
s.eventDigest = eventDigest
}
func (s *Supervisor) ConnDigest(connID uint8) ([]byte, error) {
s.connDigestLock.RLock()
defer s.connDigestLock.RUnlock()
digest, ok := s.connDigest[connID]
if !ok {
return nil, fmt.Errorf("no connection digest for connection %v", connID)
}
return digest, nil
}
func (s *Supervisor) SetConnDigest(connID uint8, connDigest []byte) {
s.connDigestLock.Lock()
defer s.connDigestLock.Unlock()
s.connDigest[connID] = connDigest
}
func (s *Supervisor) refreshAuth(
ctx context.Context,
backoff *BackoffHandler,
authenticate func(ctx context.Context, numPreviousAttempts int) (tunnelpogs.AuthOutcome, error),
) (retryTimer <-chan time.Time, err error) {
logger := s.config.Logger
authOutcome, err := authenticate(ctx, backoff.Retries())
if err != nil {
s.config.Metrics.authFail.WithLabelValues(err.Error()).Inc()
if duration, ok := backoff.GetBackoffDuration(ctx); ok {
logger.Debugf("refresh_auth: Retrying in %v: %s", duration, err)
return backoff.BackoffTimer(), nil
}
return nil, err
}
// clear backoff timer
backoff.SetGracePeriod()
switch outcome := authOutcome.(type) {
case tunnelpogs.AuthSuccess:
s.SetReconnectToken(outcome.JWT())
s.config.Metrics.authSuccess.Inc()
return timeAfter(outcome.RefreshAfter()), nil
case tunnelpogs.AuthUnknown:
duration := outcome.RefreshAfter()
s.config.Metrics.authFail.WithLabelValues(outcome.Error()).Inc()
logger.Debugf("refresh_auth: Retrying in %v: %s", duration, outcome)
return timeAfter(duration), nil
case tunnelpogs.AuthFail:
s.config.Metrics.authFail.WithLabelValues(outcome.Error()).Inc()
return nil, outcome
default:
err := fmt.Errorf("refresh_auth: Unexpected outcome type %T", authOutcome)
s.config.Metrics.authFail.WithLabelValues(err.Error()).Inc()
return nil, err
}
}
func (s *Supervisor) authenticate(ctx context.Context, numPreviousAttempts int) (tunnelpogs.AuthOutcome, error) {
arbitraryEdgeIP, err := s.edgeIPs.GetAddrForRPC()
if err != nil {
+63 -85
View File
@@ -21,6 +21,7 @@ import (
"github.com/cloudflare/cloudflared/buffer"
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
"github.com/cloudflare/cloudflared/cmd/cloudflared/ui"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/logger"
@@ -88,16 +89,7 @@ type TunnelConfig struct {
NamedTunnel *NamedTunnelConfig
ReplaceExisting bool
}
// ReconnectTunnelCredentialManager is invoked by functions in this file to
// get/set parameters for ReconnectTunnel RPC calls.
type ReconnectTunnelCredentialManager interface {
ReconnectToken() ([]byte, error)
EventDigest() ([]byte, error)
SetEventDigest(eventDigest []byte)
ConnDigest(connID uint8) ([]byte, error)
SetConnDigest(connID uint8, connDigest []byte)
TunnelEventChan chan<- ui.TunnelEvent
}
type dupConnRegisterTunnelError struct{}
@@ -172,16 +164,17 @@ func (c *TunnelConfig) RegistrationOptions(connectionID uint8, OriginLocalIP str
}
}
func (c *TunnelConfig) ConnectionOptions(originLocalAddr string) *tunnelpogs.ConnectionOptions {
func (c *TunnelConfig) ConnectionOptions(originLocalAddr string, numPreviousAttempts uint8) *tunnelpogs.ConnectionOptions {
// attempt to parse out origin IP, but don't fail since it's informational field
host, _, _ := net.SplitHostPort(originLocalAddr)
originIP := net.ParseIP(host)
return &tunnelpogs.ConnectionOptions{
Client: c.NamedTunnel.Client,
OriginLocalIP: originIP,
ReplaceExisting: c.ReplaceExisting,
CompressionQuality: uint8(c.CompressionQuality),
Client: c.NamedTunnel.Client,
OriginLocalIP: originIP,
ReplaceExisting: c.ReplaceExisting,
CompressionQuality: uint8(c.CompressionQuality),
NumPreviousAttempts: numPreviousAttempts,
}
}
@@ -193,6 +186,10 @@ func (c *TunnelConfig) SupportedFeatures() []string {
return features
}
func (c *TunnelConfig) IsTrialTunnel() bool {
return c.Hostname == ""
}
type NamedTunnelConfig struct {
Auth pogs.TunnelAuth
ID uuid.UUID
@@ -208,7 +205,7 @@ func StartTunnelDaemon(ctx context.Context, config *TunnelConfig, connectedSigna
}
func ServeTunnelLoop(ctx context.Context,
credentialManager ReconnectTunnelCredentialManager,
credentialManager *reconnectCredentialManager,
config *TunnelConfig,
addr *net.TCPAddr,
connectionIndex uint8,
@@ -243,6 +240,10 @@ func ServeTunnelLoop(ctx context.Context,
)
if recoverable {
if duration, ok := backoff.GetBackoffDuration(ctx); ok {
if config.TunnelEventChan != nil {
config.TunnelEventChan <- ui.TunnelEvent{Index: connectionIndex, EventType: ui.Reconnecting}
}
config.Logger.Infof("Retrying connection %d in %s seconds", connectionIndex, duration)
backoff.Backoff(ctx)
continue
@@ -254,7 +255,7 @@ func ServeTunnelLoop(ctx context.Context,
func ServeTunnel(
ctx context.Context,
credentialManager ReconnectTunnelCredentialManager,
credentialManager *reconnectCredentialManager,
config *TunnelConfig,
logger logger.Service,
addr *net.TCPAddr,
@@ -277,6 +278,13 @@ func ServeTunnel(
}
}()
// If launch-ui flag is set, send disconnect msg
if config.TunnelEventChan != nil {
defer func() {
config.TunnelEventChan <- ui.TunnelEvent{Index: connectionIndex, EventType: ui.Disconnected}
}()
}
connectionTag := uint8ToString(connectionIndex)
// Returns error from parsing the origin URL or handshake errors
@@ -305,28 +313,16 @@ func ServeTunnel(
}()
if config.NamedTunnel != nil {
return RegisterConnection(ctx, handler.muxer, config, connectionIndex, originLocalAddr)
return RegisterConnection(ctx, handler.muxer, config, connectionIndex, originLocalAddr, uint8(backoff.retries))
}
if config.UseReconnectToken && connectedFuse.Value() {
token, tokenErr := credentialManager.ReconnectToken()
eventDigest, eventDigestErr := credentialManager.EventDigest()
// if we have both credentials, we can reconnect
if tokenErr == nil && eventDigestErr == nil {
var connDigest []byte
if digest, connDigestErr := credentialManager.ConnDigest(connectionIndex); connDigestErr == nil {
connDigest = digest
}
return ReconnectTunnel(serveCtx, token, eventDigest, connDigest, handler.muxer, config, logger, connectionIndex, originLocalAddr, cloudflaredUUID, credentialManager)
err := ReconnectTunnel(serveCtx, handler.muxer, config, logger, connectionIndex, originLocalAddr, cloudflaredUUID, credentialManager)
if err == nil {
return nil
}
// log errors and proceed to RegisterTunnel
if tokenErr != nil {
logger.Errorf("Couldn't get reconnect token: %s", tokenErr)
}
if eventDigestErr != nil {
logger.Errorf("Couldn't get event digest: %s", eventDigestErr)
}
logger.Errorf("Couldn't reconnect connection %d. Reregistering it instead. Error was: %v", connectionIndex, err)
}
return RegisterTunnel(serveCtx, credentialManager, handler.muxer, config, logger, connectionIndex, originLocalAddr, cloudflaredUUID)
})
@@ -416,6 +412,7 @@ func RegisterConnection(
config *TunnelConfig,
connectionIndex uint8,
originLocalAddr string,
numPreviousAttempts uint8,
) error {
const registerConnection = "registerConnection"
@@ -432,7 +429,7 @@ func RegisterConnection(
config.NamedTunnel.Auth,
config.NamedTunnel.ID,
connectionIndex,
config.ConnectionOptions(originLocalAddr),
config.ConnectionOptions(originLocalAddr, numPreviousAttempts),
)
if err != nil {
if err.Error() == DuplicateConnectionError {
@@ -446,6 +443,11 @@ func RegisterConnection(
config.Metrics.regSuccess.WithLabelValues(registerConnection).Inc()
config.Logger.Infof("Connection %d registered with %s using ID %s", connectionIndex, conn.Location, conn.UUID)
// If launch-ui flag is set, send connect msg
if config.TunnelEventChan != nil {
config.TunnelEventChan <- ui.TunnelEvent{Index: connectionIndex, EventType: ui.Connected, Location: conn.Location}
}
return nil
}
@@ -480,7 +482,7 @@ func UnregisterConnection(
func RegisterTunnel(
ctx context.Context,
credentialManager ReconnectTunnelCredentialManager,
credentialManager *reconnectCredentialManager,
muxer *h2mux.Muxer,
config *TunnelConfig,
logger logger.Service,
@@ -489,6 +491,9 @@ func RegisterTunnel(
uuid uuid.UUID,
) error {
config.TransportLogger.Debug("initiating RPC stream to register")
if config.TunnelEventChan != nil {
config.TunnelEventChan <- ui.TunnelEvent{EventType: ui.RegisteringTunnel}
}
tunnelServer, err := connection.NewRPCClient(ctx, muxer, config.TransportLogger, openStreamTimeout)
if err != nil {
// RPC stream open error
@@ -499,7 +504,7 @@ func RegisterTunnel(
serverInfoPromise := tunnelrpc.TunnelServer{Client: tunnelServer.Client}.GetServerInfo(ctx, func(tunnelrpc.TunnelServer_getServerInfo_Params) error {
return nil
})
LogServerInfo(serverInfoPromise.Result(), connectionID, config.Metrics, logger)
LogServerInfo(serverInfoPromise.Result(), connectionID, config.Metrics, logger, config.TunnelEventChan)
registration := tunnelServer.RegisterTunnel(
ctx,
config.OriginCert,
@@ -510,47 +515,13 @@ func RegisterTunnel(
// RegisterTunnel RPC failure
return processRegisterTunnelError(registrationErr, config.Metrics, register)
}
credentialManager.SetEventDigest(registration.EventDigest)
return processRegistrationSuccess(config, logger, connectionID, registration, register, credentialManager)
}
func ReconnectTunnel(
ctx context.Context,
token []byte,
eventDigest, connDigest []byte,
muxer *h2mux.Muxer,
config *TunnelConfig,
logger logger.Service,
connectionID uint8,
originLocalAddr string,
uuid uuid.UUID,
credentialManager ReconnectTunnelCredentialManager,
) error {
config.TransportLogger.Debug("initiating RPC stream to reconnect")
tunnelServer, err := connection.NewRPCClient(ctx, muxer, config.TransportLogger, openStreamTimeout)
if err != nil {
// RPC stream open error
return newClientRegisterTunnelError(err, config.Metrics.rpcFail, reconnect)
// Send free tunnel URL to UI
if config.TunnelEventChan != nil {
config.TunnelEventChan <- ui.TunnelEvent{EventType: ui.SetUrl, Url: registration.Url}
}
defer tunnelServer.Close()
// Request server info without blocking tunnel registration; must use capnp library directly.
serverInfoPromise := tunnelrpc.TunnelServer{Client: tunnelServer.Client}.GetServerInfo(ctx, func(tunnelrpc.TunnelServer_getServerInfo_Params) error {
return nil
})
LogServerInfo(serverInfoPromise.Result(), connectionID, config.Metrics, logger)
registration := tunnelServer.ReconnectTunnel(
ctx,
token,
eventDigest,
connDigest,
config.Hostname,
config.RegistrationOptions(connectionID, originLocalAddr, uuid),
)
if registrationErr := registration.DeserializeError(); registrationErr != nil {
// ReconnectTunnel RPC failure
return processRegisterTunnelError(registrationErr, config.Metrics, reconnect)
}
return processRegistrationSuccess(config, logger, connectionID, registration, reconnect, credentialManager)
credentialManager.SetEventDigest(connectionID, registration.EventDigest)
return processRegistrationSuccess(config, logger, connectionID, registration, register, credentialManager)
}
func processRegistrationSuccess(
@@ -559,7 +530,7 @@ func processRegistrationSuccess(
connectionID uint8,
registration *tunnelpogs.TunnelRegistration,
name registerRPCName,
credentialManager ReconnectTunnelCredentialManager,
credentialManager *reconnectCredentialManager,
) error {
for _, logLine := range registration.LogLines {
logger.Info(logLine)
@@ -570,15 +541,17 @@ func processRegistrationSuccess(
logger.Infof("Each HA connection's tunnel IDs: %v", config.Metrics.tunnelsHA.String())
}
// Print out the user's trial zone URL in a nice box (if they requested and got one)
if isTrialTunnel := config.Hostname == ""; isTrialTunnel {
if url, err := url.Parse(registration.Url); err == nil {
for _, line := range asciiBox(trialZoneMsg(url.String()), 2) {
logger.Info(line)
// 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 config.TunnelEventChan == nil {
if config.IsTrialTunnel() {
if registrationURL, err := url.Parse(registration.Url); err == nil {
for _, line := range asciiBox(trialZoneMsg(registrationURL.String()), 2) {
logger.Info(line)
}
} else {
logger.Error("Failed to connect tunnel, please try again.")
return fmt.Errorf("empty URL in response from Cloudflare edge")
}
} else {
logger.Error("Failed to connect tunnel, please try again.")
return fmt.Errorf("empty URL in response from Cloudflare edge")
}
}
@@ -621,6 +594,7 @@ func LogServerInfo(
connectionID uint8,
metrics *TunnelMetrics,
logger logger.Service,
tunnelEventChan chan<- ui.TunnelEvent,
) {
serverInfoMessage, err := promise.Struct()
if err != nil {
@@ -632,6 +606,10 @@ func LogServerInfo(
logger.Errorf("Failed to retrieve server information: %s", err)
return
}
// If launch-ui flag is set, send connect msg
if tunnelEventChan != nil {
tunnelEventChan <- ui.TunnelEvent{Index: connectionID, EventType: ui.Connected, Location: serverInfo.LocationName}
}
logger.Infof("Connected to %s", serverInfo.LocationName)
metrics.registerServerLocation(uint8ToString(connectionID), serverInfo.LocationName)
}
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"github.com/cloudflare/cloudflared/logger"
"github.com/getsentry/raven-go"
"github.com/pkg/errors"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
)
const (
+3 -2
View File
@@ -5,6 +5,7 @@ import (
"sync"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/metrics"
"github.com/coredns/coredns/plugin/metrics/vars"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/pkg/rcode"
@@ -27,7 +28,6 @@ func NewMetricsPlugin(next plugin.Handler) *MetricsPlugin {
prometheus.MustRegister(vars.RequestDuration)
prometheus.MustRegister(vars.RequestSize)
prometheus.MustRegister(vars.RequestDo)
prometheus.MustRegister(vars.RequestType)
prometheus.MustRegister(vars.ResponseSize)
prometheus.MustRegister(vars.ResponseRcode)
})
@@ -42,7 +42,8 @@ func (p MetricsPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dn
status, err := plugin.NextOrFailure(p.Name(), p.Next, ctx, rw, r)
// Update built-in metrics
vars.Report(ctx, state, ".", rcode.ToString(rw.Rcode), rw.Len, rw.Start)
server := metrics.WithServer(ctx)
vars.Report(server, state, ".", rcode.ToString(rw.Rcode), rw.Len, rw.Start)
return status, err
}
+1 -1
View File
@@ -17,7 +17,7 @@ import (
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/cache"
"github.com/pkg/errors"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
)
// Listener is an adapter between CoreDNS server and Warp runnable
+5 -4
View File
@@ -26,10 +26,11 @@ type ClientInfo struct {
}
type ConnectionOptions struct {
Client ClientInfo
OriginLocalIP net.IP `capnp:"originLocalIp"`
ReplaceExisting bool
CompressionQuality uint8
Client ClientInfo
OriginLocalIP net.IP `capnp:"originLocalIp"`
ReplaceExisting bool
CompressionQuality uint8
NumPreviousAttempts uint8
}
type TunnelAuth struct {
+2
View File
@@ -98,6 +98,8 @@ struct ConnectionOptions {
replaceExisting @2 :Bool;
# cross stream compression setting, 0 - off, 3 - high
compressionQuality @3 :UInt8;
# number of previous attempts to send RegisterConnection
numPreviousAttempts @4 :UInt8;
}
struct ConnectionResponse {
+184 -175
View File
@@ -1109,6 +1109,14 @@ func (s ConnectionOptions) SetCompressionQuality(v uint8) {
s.Struct.SetUint8(1, v)
}
func (s ConnectionOptions) NumPreviousAttempts() uint8 {
return s.Struct.Uint8(2)
}
func (s ConnectionOptions) SetNumPreviousAttempts(v uint8) {
s.Struct.SetUint8(2, v)
}
// ConnectionOptions_List is a list of ConnectionOptions.
type ConnectionOptions_List struct{ capnp.List }
@@ -3363,181 +3371,182 @@ func (p TunnelServer_reconnectTunnel_Results_Promise) Result() TunnelRegistratio
return TunnelRegistration_Promise{Pipeline: p.Pipeline.GetPipeline(0)}
}
const schema_db8274f9144abc7e = "x\xda\xccYkl\x1c\xd5\xf5?g\xee\xaeg\xfdb" +
"=\x99M\x9cX\xf0\xf7\xbfQ\"\x8a!\x80\xe3R\x85" +
"\x14j;\xd8)6yx\xbcq\xc5#A\x8cwo" +
"\xecqwg63\xb3\xc6N\x13\x92\x98\xa4\x10Dy" +
"\x84\x84G\x0a-\x04\xa9UiiI\xa1j\xa9\xa8J" +
"\xfa\x82\x8agP\xa8\x02\xa4jKH[\xa2P\xda@" +
"\x85\xa8\x80\xa9\xce\xcc\xce\xc3kc;\xd0\x0f\xfd\xb6:" +
"s\xee\xbd\xe7}~\xe7\xec\xf9\xcb\xc56\xa19.V" +
"\x03([\xe3\x15\x0eozi\xe3\xfd\x0b\x7f5\x06J" +
"\x03\xa2s\xdd\x13\xdd\xa9\xf7\xed\xb1\xd7 \xceD\x80\x16" +
"-\xbe\x11\xe5\xcdq\x11@\x1e\x8d\xff\x15\xd0\xb9L:" +
"\xef\xaa\xd4\x8b\xcf_\x0fRC\x949F\xcc\xeb*\x9a" +
"P\xceW\x10\xb3VA\xcc\x17\xe5_\xd8\xf7\xf9=\xcf" +
"l\x07\xa9A\x08\x99\x01[\xfa\xc4\x8d(k\"qr" +
"q5\xa0\xf3\xce\xee\xb9\xdf{\xe0\xf9\xa7w\x80t&" +
"B\xe9\xed\xcd\xe2\xab\x08(\xdf.\xfe\x10\xd0i]\xfe" +
"\xdc\xe3\x0d-w\xee.{W \xc6\xe6D\x13\xca\xed" +
"\x09\xba\xed\xe2\xc4\xb5\x80\xce\xfc\xe1\x85\xd7\xfc\xf27\x8f" +
"\xde\x05\xca\"D\xe7H\xff\xd9/\xb3\xfb\x1ez\x0d\xfa" +
"PD\x01\xa0\xe5\x81\xc4>\xbax\xbf\xcb\xfb\xc29O" +
"\xfc\xec\xb6Go\xf8\x06(g\"\x02\xb8\x8a\xcc\xae\xfc" +
"71\x9cU\xd9\x0a\xe8\xec>\xfc\xf3U\xf9\xdb\xf7\xee" +
"\xf3Ds\xbf+\x95\x82\x001g{\xd7{\xf9\xbe\x07" +
"\xd3\x0f\x96\x84&+\xb5tV\x9eD\xc0\x96+*\x1b" +
"\x11\xd0\xb9\xe0\xd5c\xabW\xfeh\xfdw\"g\x8bU" +
"\x1b\xe9\xec\xc3\xffwY\xe5\xc8\xb1\xe5\x8f\x80\xb4\xc8\xff" +
"\xb2\xa1\xaa\x97\xbe\xc4\xd6\xb1\x8f\xd4\xbb\x7f\xf1X\xb9?" +
"\\U\xf3U\xfd(o\xab\"U7W\x91\x89o:" +
"\xb0\xf7\xec\xc4\xb7\xde\xf9\xf1dvQ\xab\xfbQ.V" +
"\x13\xf3\x86j\xd2uv\x17\x1ey\xb29\xf6\x93\xa8\x95" +
"\x0fV\x1f']\x8fU\x93\x95\xcfxkY\xad\xfe\xf6" +
"\xd8\x93e\xb7\xb9\x8c;j\xbaQ\xbe\xa7\x86n\xdbS" +
"C\xcc\xddW\xdd\xb1+~\xec\x8e\xa7H\xd0\x88{\xe3" +
"\x09\xd7'\xb5&\xca\x9d\xb5\xf4\xb3\xbd\xb6\x9e\x01:\x0d" +
"\x8f|\xe1\x07\xcb\xb2\xaf<3\x89\xa4\xf2\x81\xba\x93\xf2" +
"su\xf4\xebwu$\xe8\xd1E\xfb\xbf\xfa\xe6\xd7\x0f" +
"\x1e*\x09\x8at\xcdB\xc9u\xca\x85\x129%\xf0i" +
"\x99\x91\\\xce+\xa4!\x94\xf3\x92\x1b\x87.\xb7pL" +
"\x9d\xb7\xf5\xf7_<\x12qC^z\x1d!\xe6\xac\xfa" +
"\xf2UC\x95\x9b\x8f\x1e\x8d>\xa4J\xaeE\x8a\xee\xd1" +
"\xbf\x7f\xfb\xf8\xad'\xf2\xd9\xbf\xb8\xa1\xe4\xdbl\x8f\xb4" +
"T\x00\x94\x1f\x96\xc8\x03\xf5\x8d\xb5\x9d\xf3\x0f\xf7\x1c\xf7" +
"<\xe9]\xb1s\xd62bx`\x16]q\xc15\xed" +
"|\xed\x92\xcb\x8f\x83\xd4\xc0\xc6e\xc1\x81YKQ>" +
"8\x8b\x0e<7\xeb\x06\x94\x8f\xc9\xf5\x00\xce-\xd7u" +
"\xac\xbep\xfe\x81\x93Q\x91^\x96)\xaa\xe47e\xba" +
"o\xfd\x92\x13_Zx\xcboO\x96\x19\xd2e\xacL" +
"5\xa1</E\xaa\xcfN\xb5\x02\xbe\xbd\xfc\x9b\x87\x1a" +
"\x92\x0d\xef\x96\x99\x89r\xb4\xe5\xc2\xd4\x10\xca+\x89\xb7" +
"\xa5+\xf5\x14\x85\xeb\xd7^\xbbz\xe4\xa5\xeb\xdf\xf9W" +
"\xb9G\xdd\xab/\x9e\xd3\x8b\xb22\x87\xae^9\x87\xfc" +
"\x7f\xd7\x9a\xbfm9\xb1g\xce{\x13\xf4zk\xce\x10" +
"\xcaXO\x9c\x1f\xce\xb9A^G\xbf\x9c\x17\xc5\x07\x9b" +
";\xb6<\xf3~$\xe0;\xeb\xbb)\xe0\xef\x14\xef=" +
"\xba\xf5\x8fW\x7f\x10U\xb8\xbd\xfeuR\xb8\xaf\x9e\x14" +
"\xde\xf4\xf6=\x97\xde\xba\xf6\xfb\x1fE\xb3\xa8~\x8c\x8e" +
"\xdaE]\xe79\xb3\x10\xcb\x9c\xe7\xff\xcc\x9c\x9bQ\x0b" +
"zai{\xd1\x1e\xe4\xba\xadeT\x9b\xf7\xf2V\xab" +
"`\xe8\x16\xefAT\xeaX\x0c \x86\x00\x92:\x04\xa0" +
"\\\xc3P\xc9\x09(!\xa6\xc8\xc1\x92F\xc4A\x86\x8a" +
"-\xa0$\x08)\xaa\x18\xd2\x86\xf9\x00J\x8e\xa12\"" +
" \xb2\x142\x00\xa9\xb8\x0b@\x19a\xa8l\x17\xd0)" +
"p3\xaf\xea\\\x87\xa4\xddi\x9aX\x03\x02\xd6\x00:" +
"&\xb7\xcdQ\xb5?\x07I\x1e!\x8bC\xd7\xdaX\x0b" +
"\x02\xd6\x02:\x83F\xd1\xb4\xfat\x1b\xb5\\/_o" +
"r\x0b\x07\xb1\x02\x04\xac\x00\x0c\xd4c\x13\xd5\xbb$\xa7" +
"q\xddNv\xe9\xeb\x8d2\xa5\xba'S\xaa\xbb\xa4\xd4" +
"\xf6\x88R\xdb\x96\x01(\x9b\x18*7\x0a(\xb1\x92V" +
";\x9a\xa8-0Tn\x16\xd0\xc9\xb8\x8fte\x01 " +
"\x90w=W\xed\xa2\xc9-\xa2\x9d\x06\xd8\xc3\xd0U\xeb" +
"4\xc0-\xc3\xdc\xb44C\xf7\xd5L\xaaff00" +
"\xc5\x14\xae\xea\x1c\xd1,[\xd3\x07\xd6\xb8\xf4\xd6\x1e#" +
"\xa7eFI\xab\x1aW\xce3\x96\x02 J\xb3\xaf\x04" +
"@A\x92\x96\x01\xb4j\x03\xbaar'\xabY\x19C" +
"\xd79\xb0\x8c\xbd\xa5_\xcd\xa9z\x86\x07\x0fUL|" +
"\xc8{ \xcd\xcdan\x9e\xabF\x02dA\x8fj\xaa" +
",o)5\x81\x1d;\xaf\x04P:\x18*=\x11;" +
"\xae$;\xae`\xa8\\\x1e\xb1c\x1f\xd9\xb1\x87\xa1\xb2" +
"V@\xc70\xb5\x01M\xbf\x84\x033\xa3>\xb6l]" +
"\xcds\xb2Y\xc9\x1e[\x8c\x82\xad\x19\xba\x85ua\x1d" +
"\x05\xc4\xba\xa9\xbd\xee)\x90\xa4\xd8&\xfb$\x02i\xcf" +
"\"i?\xcbP\xf9\\D\xdaf\x0a\xe5\xf3\x19*\x17" +
"\x09\xe8\xa8\x99\x8cQ\xd4\xed5\xc0\xd4\x812\xa7\xa49" +
"$3&\x0f\xe5\xf5\x9f\x8dO\x12wd\xef\x0c\x89\xde" +
"\xcb\xbd\x9c:\xd7\xe4\x96X\xcc\xd9$M\x8d\xe3x\xe2" +
",\x06P\x160T\xce\x17\xb0\x16?r<y\x16\xed" +
"\x0a\xe5i\xe4\xa6i\x98X\x17\xd6\x9c\x92\xf6\x99\xd2\x03" +
"h\xe8\x1d\xdcV\xb5\x1c\x92\x8d\x826Wf\xa3\xe9\x9c" +
"\\\xd4M>\xa0Y67=\xf2\x82V\xf2t\xdeR" +
"b\x81\xe9j\xf7\x02(u\x0c\x95\xd3\x05t\x06L5" +
"\xc3{\xb8\x89\x9a\x91]\xa5\xeaF\x9a\xf1\x0c\xc6A\xc0" +
"x\xe4\xd1\xd3N\xf5\xd1^n\x15s\xb6\x05\xc1\xa9\xa9" +
"\xcf\x9b\xbcd\x84\xd2\xf1\x9eFO\xe6T \xf3\xe6\xf9" +
"a\xed\x09\xdc\xbd\xad?L\xdd 8wR`\xdc\xc8" +
"P\xd9\x1dI\xf2\xdb)\x8coc\xa8\xdc+\xa0\x14\x8b" +
"\xa50\x06 \xddCa\xbc\x9b\xa1r\xbf0\xbeB\xf1" +
"a\xae\xdb\x1d\xda\x00\x88\xdc\x0a\xa9$b\x876\xc0\x81" +
"Y\x9f6\xd0\x13\xd3\xd8\xc3\xe8\xb7\x8c\x1c\xb7y\x07\xcf" +
"\xe4TS\xb5\xb5a\xee}/\x05\xa3\xef\xd4\xa9.\xec" +
"u=B\x87\x0d}\x82\x9b\xc2\xa0.\xb9\x0a\xad\xa9\xea" +
"U\xc8\xbe\xba`k\xa2\xa1[e%x)\x80\xb2\x96" +
"\xa12\x18\xf1\x0e7\x01\x94,C\xa5 \x96\x9c\x93" +
"\x1f\x0b\xdb\x8a\xc4\xb0\xd4W\xf6\x86\xbem\xf5*0\xd6" +
"\x85\xb8\xbbd;\xaf\xce\xac0\xa01\xa3\xe6\xba\x0a\x81" +
"\x07L^\xc8\xa9\x19\xde\x89\xa5\x9a\x0a\x88 \xba\x0e" +
"\xcb\x17LnY\xa8\x19\xbaRTs\x1a\xb3G't" +
"\x9a)\xb5\xa5|\x14\xb5\x9cUVz\x9a\xc2\\\x0f\xb4" +
"]D\xa5\xe7\x1c\x86\xca\x12\x01\x93\xc5\xa2\x96\x0d\x04\xcc" +
"\x19\x19\xd7\x09\x90\\\xa5\xe6\xf9\x84\xf6P1mn\x8c" +
"\xcb\xac\x1e5\xe9\xa6\xc6\xffR\xdd\x9e\x1a\x8c\x90\xea\xe0" +
"6\xb7Pd\xca\xe66\x86\xca\x8a\x88\xc8]\x8b#z" +
"\xf8\"\xaf\xec\x0f\xf5\x10\xbf\xc2G}\xa9\x1ay\x9e*" +
"\xa5o\xcc\x922\xed ^\x16\xf2L%_4;V" +
"\x17\x1a]\x0dI\xc6%\xbe\x8c\xf2(v\x03\xa4G\x90" +
"az;\x86b\xca\xdbp\x19@z\x13\xd1o\xc4P" +
"Ry\x076\x00\xa4\xb7\x12\xfdf\x0c@\x93\xbc\x13\x1f" +
"\x02H\xdfL\xe4\xbb\x89=\xc6\xdc\xe2#\xefq\xaf\xdf" +
"M\xf4\xfb\x89\x1e\x8f\xa50\x0e \xdf\x87M\x00\xe9\xbb" +
"\x89\xfe\x18\xd1+\x84\x14V\x00\xc8\xfbq\x08 \xfd\x08" +
"\xd1\x9f \xba\x18O\x11n\x94\x1fG\x13 \xfdS\xa2" +
"\xff\x9a\xe8\x89\xb9)L\xd0\x80\xe1\xd2\x9f$\xfa\xb3D" +
"\xaf\x9c\x97\xc2J\x1a7p\x0c \xfd4\xd1\x0f\x11\xbd" +
"\x0aSX\x05 \x1f\xc4\xbd\x00\xe9CD\xff\x13\xd1\xab" +
"+RX\x0d \xff\xc1\x95\xe70\xd1\xdf zM," +
"\x855\x00\xf2\x9fq\x1f@\xfa\x0d\xa2\xff\x83\xe8\xb5b" +
"\x0ak\x01\xe4\xb7\\\xbdN\x10=!\x94!*?\xa2" +
"\xca`\x133\xac\xc0e\xbc\x94\xc5\xe8\x85{\x8f\x91$" +
"h\x84\xc9p\xb2\x06\xc4$\xa0S0\x8c\xdc\xaa\xf1\x91" +
"\x9a\xb4\xd5\x01\xcb\x87hu\xe1\xf8\x05H\xc4\xa0\xcfB" +
"\xd2\xd0\xbb\xb2A!(\xaf+\xbe$\x9a\xd5^\xb4\x8d" +
"b\x01\x1a\xb3\xaa\xcd\xb3AU1\x8b\xfar\xd3\xc8\xaf" +
"An\xe65]\xcdMSo*A\xc0J(\x95\x04" +
"\xffn\xbd\x98\xef1\xf9\xb0\x86F\xd1j\xb7m\x9e\x17" +
"\x0b\xb6\x15H\xf4\xf1\x803\x88h\xa1<\xa2\x1b\x0bK" +
"\xd7\xa8\x033\xa9S\x8bC\xdc\x94\xd4#\x05\xa9qX" +
"\xcd\x15?Iy\x1a\xdf\xba{[\xbd\xd6\x1f\xc5\x1b\xd4" +
"\x1d\x12\x0c\x95\x94\x80\xad\xa6\xd7n\xea\xfc9m\xfaR" +
"2\x1e\x80%\xfd\xa9\xc6\xbd\x1f#\xab\x0fzG(\xdd" +
"?c\xf1\x07\xb8\xed\xfd\xa2\xb9\x82@\xb1\x18m\xab\xa7" +
"v\xba\x97[\xc9\x99\xa8\x1e\xce\xb3e\xca\x8b3\xea\xe2" +
"\x93\xf4p\x1f\xe3E:r\xd3d\x1d\xb9{\x92\x8e\xdc" +
"\x1b\xed\xc8B\xa9#S\xa3(0T6\x09\x98\xa4\xb1" +
"\x01\xeb\xc2}\xd48\xa1\xc7\x8fJ\x14\x0a]z\x96\x03" +
"\x8e\xf8\xd1\x1ci\x1f\xc1\x9egz443\xb5}\x94" +
"9\xad\xc1\x83\xe5\xc9L\x07\x8eV\xefQ\x8a\xb3\xb9," +
"\x0e\x10,\x92\xd0_VH\xfb7\x82 }W\xc4p" +
"\xd9\x82\xfenE\xba\xcf\x04A\xda#\xa2\x10,\xdb\xd0" +
"_\xaaI;o\x02A\xda!\"\x0bve\xe8\x8f\xfb" +
"\xcd\xa3U\x08\x82\xb4Y\xc4X\xb0\x1fD\x7fY m" +
"\x18\x02A\xd2D\x8c\x07k8\xf4\xb7F\xd2\xba1\x10" +
"\xa4>\xd1\xf1\x8d\x04\xad\x9e\x1em\xe8\xf81\x0a\x8dn" +
"\x94\xb6\xa1\xe3\xc3@\xf4A\x05@\x1b:>\xe6d\x1f" +
"\x07:].\x7f\x86\x84$M\x91m\x04\xbf\xbc\xfc\xc7" +
"R\x01\x806Tb\x18\xd9\x95\x00|R\xa8\xd3\xcb\x1b" +
"?M)\x99\xc4\xb9\xde;\xc1\x0e!r/\xa1\xb7\x1a" +
"\x86\xca\\aZ\xc0\x16\xfb8-\xfc\xa0M\xd2a\xba" +
"\xff\xff\x83\xfb\x0f\x12\xe0y\x96\xa1r8\x92\x8e/\x13" +
"\xf1E\x86\xca\x91\x08\xe0y\x85r\xf40C\xe5\xddp" +
"\xf1\xf2\xcf\x9b\x00\x94w\x19\xf6F\x00\x84\xf4!1~" +
"@m\xd6\x85\x0f\xe8\xc1\x878\xee\x02H'\xa8\xfd\xa6" +
"\\\xf8\x10\xf3\xe0\x83\x84\xfd\x00\xe9:\xa2\x9f\x1e\x85\x0f" +
"\xf3\xf0J\x80\xf4\\\xa2/@\x01E\x1eY\xda\x14\xcd" +
"\x10`\xe5\x8c\x81\x15\x9a>iO\xf27Ah/W" +
"\xb5\\\xd1\xe4\x10\xb6\xc4R\x91\xe8\x88tioE\xd4" +
"\xbe\x9e\xc2/M\xc1\x93E\x0bE\x10P<\xb5\xc9k" +
"F\x1d\xa3\xd34\x0d4\xcb\xc0\xe7\xe2\x10|\x06\xd8\x93" +
"0\xf4\xa5\x0c\x955\xe4\x8a6\xcf\x15J\x7f\x08\x97\x1b" +
"3j\xd1\xe2\x13t\x00\xc6\xcd`Z\xb6\x06\x8db." +
"\xdb\xcbA\xb4\xcd\xd12\x13L\x0bB\xd3<\xe9W\x9c" +
"\x84[q\xfc=,\xfa\xebV\xa9y/\x08\xd2\"\xaa" +
"8\xfej\x11\xfd\xa5\xba\xf4\x99\x87@\x90\xce\x08\x0b\x00" +
"\xfa6`\x86>>\xe5\xbd\x0fn\x8c\xb6a\x0f\xe2\x7f" +
"c\x86\xf4\xda\xcf)d\xfa\xb8U\x14\x95qq&}" +
"3\xf8\xe3\xa6,\xd3+?\xed8\xed7\x92\xff\x04\x00" +
"\x00\xff\xff\xed\xc3%H"
const schema_db8274f9144abc7e = "x\xda\xccY{l\x1c\xd5\xd5?g\xee\xaeg\xed\xd8" +
"\xac\x87\xd9\xc4\x89\x05\x9f\xbf/\x0a\xe2\xc3`\xc0q\xa9" +
"\xd2\x94\xd6v\xb0Sl\xf2\xf0x\x93\x96G\x82\x18\xef" +
"\xde\xd8\xe3\xee\xce,3\xb3\xc6N\x13\x92\x98\xa4\x10\xc4" +
"+!\xe1\x91\x926\x04\xd1\xaa\x14ZR@m*\xaa" +
"B_\x80x\xa3P%\x10\xd4\x96\x90>\xa2PJ\xa0" +
"BT\x94\xa9\xce\xcc\xce\xc3kc;\xd0?\xfa\xdf\xea" +
"\xcc\xb9\xf7\x9es~\xe7\x9e\xf3\xbbg\xcf\xff\x9a\xd8&" +
"4\xc7\xc5\x19\x00\xca\xc6x\x85\xc3\x1b_Y\xbb\xe7\x8c" +
"_\x8d\x82R\x8f\xe8\\\xfbxw\xeaC{\xf4u\x88" +
"3\x11\xa0E\x8b\xafEy}\\\x04\x90G\xe2\x7f\x06" +
"t.\x91\xce\xbb\"\xf5\xd2\x0b\xd7\x81T\x1fU\x8e\x91" +
"\xf2\xea\x8aF\x94\xf3\x15\xa4\xacU\x90\xf2\x85\xf9\x17\xf7" +
"~~\xe7\xb3\x9bA\xaa\x17Be\xc0\x96\x95\xe2Z\x94" +
"5\x914\xb9\xb8\x1c\xd0yo\xc7\xec\x1f\xdc\xfb\xc2\xd3" +
"[@:\x13\xa1t\xf6z\xf15\x04\x94\xb7\x89?\x02" +
"tZ\x17?\xbf\xbf\xbe\xe5\x8e\x1de\xe7\x0a\xa4\xd8\x9c" +
"hD\xb9=A\xbb})q\x0d\xa03w\xe8\x8c\xab" +
"~\xf9\x9bG\xee\x04\xa5\x09\xd19\xdcw\xf6\xabl\xf7" +
"\x03\xaf\xc3J\x14Q\x00h\xb97\xb1\x976\xde\xe7\xea" +
"\xbex\xce\xe3?\xbb\xed\x91\xeb\xbf\x05\xca\x99\x88\x00\xae" +
"#3+\xffI\x0agU\xb6\x02:;\x0e\xfe|Y" +
"~\xdb\xae\xbd\x9ei\xeew\xa5R\x10 \xe6l\xee\xfa" +
" \xbf\xf2\xbe\xf4}%\xa3)J-\x9d\x95'\x10\xb0" +
"\xe5\xb2\xca\x06\x04t.x\xed\xe8\xf2\xa5?^\xf3\xbd" +
"\xc8\xdab\xd5ZZ\xfb\xd0\xff\\R9|t\xf1\xc3" +
" 5\xf9_\xae\xae\xea\xa5/\xb1\xd5\xecc\xf5\xae_" +
"<Z\x8e\x87\xebj\xbe\xaa\x0f\xe5MUnx\xaa\xdc" +
"#n|r\xd7\xd9\x89\xef\xbc\xf7\xd8D\x81\xb9\x7fF" +
"\x1f\xca\xfbgP`\x1e\x9bA\xce\xce\xec\xc2\xc3O4" +
"\xc7~\x12\x0d\xf3\xcc\xeac\xae\xb3\xd5\x14\xe6\xd3\xdf^" +
"T\xa3\xbf3\xfaD\xd9n\xae\xe23\xd5\xdd(\xbfQ" +
"M\xbb\x1dr\x95\xbb\xaf\xb8}{\xfc\xe8\xedO\x91\xa5" +
"\x11|\xe3\x09\xd7\xcf\x1a\x13\xe5\xad5\xf4sKM\x1d" +
"\x03t\xea\x1f\xfe\xe2\x0f\x17e\x0f=;\x81\xa5r\\" +
":!K\x12\xfd\xaa\x91\xc8\xd0#M\xfb\xbe\xf1\xd7\x9b" +
"_>P2\x14\xdd\\\x94\\T\xd6K\x84J\x00j" +
"Y\x94\\\xcd\xdd\xd2 \xca\xfb\xdc\xed\x1er\xb5\x85\xa3" +
"\xea\x9c\x8d\xbf\xfb\xf2\xe1\x08\x0e\xfb\xa47\x11b\xce\xb2" +
"\xaf^1X\xb9\xfe\xc8\x91\xe8A\xf7KnD\xf6\xbb" +
"K\xff\xf6\xddc\xb7\x1e\xcfg\xff\xe4\xe6\x92\x1f\xb3C" +
"\xd2B\x01P~W\xa2,\xafk\xa8\xe9\x9c{\xb0\xe7" +
"\x98\x07\xa5\xb7\xc5\xf3\xa7.\"\x85\xa3\xa7\xd2\x16\x17\\" +
"\xd5\xceW-\xb8\xf4\x18H\xf5l\xcc5\x88\xcb\x0bQ" +
"\x9e)\xd3\x02I\xbe\x1e\xe5\xb3Ru\x00\xce-\xd7v" +
",\xff\xc2\xdc'ODM\x9a\x93\xa2\xb4\x92\x9bR\xb4" +
"\xdf\x9a\x05\xc7\xbfr\xc6-\xbf=Q\x16HWqi" +
"\xaa\x11\xe5\xd5)r\xfd2R~g\xf1\xb7\x0f\xd4'" +
"\xeb\xdf/\x0bS\x85\x9bA\xa9A\x94\xb7\x91n\xcb\xcd" +
"\xa9\xa7(\x99\xbe\xf9\xfa\x95\xc3\xaf\\\xf7\xde?\xca\x11" +
"u\xb7\xde4\xab\x17\xe5\x9d\xb3h\xebm\xb3\x08\xff;" +
"W\xfce\xc3\xf1\x9d\xb3>\x18\xe7Ws\xdd \xca\x9d" +
"u\xa4\xd9^w\xbd|/\xfdr^\x12\xefk\xee\xd8" +
"\xf0\xec\x87\x91\x8c\xdfZ\xd7M\x19\x7f\x87x\xcf\x91\x8d" +
"\xbf\xbf\xf2\xa3\xa8\xc3[\xea\xde$\x87\xef\xae#\x87\xd7" +
"\xbds\xf7\xc5\xb7\xaez\xf0\xe3\x08|\xfb\xebFi\xa9" +
"]\xd4u\x9e3\x0b\xb1\xccy\xfe\xcf\xcc\xb9\x19\xb5\xa0" +
"\x17\x16\xb6\x17\xed\x01\xae\xdbZF\xb5y/o\xb5\x0a" +
"\x86n\xf1\x1eD\xa5\x96\xc5\x00b\x08 \xa9\x83\x00\xca" +
"U\x0c\x95\x9c\x80\x12b\x8a\x00\x964\x12\x0e0Tl" +
"\x01%AHQ\xc9\x90\xae\x9e\x0b\xa0\xe4\x18*\xc3\x02" +
"\"K!\x03\x90\x8a\xdb\x01\x94a\x86\xcaf\x01\x9d\x02" +
"7\xf3\xaa\xceuH\xda\x9d\xa6\x89\xd5 `5\xa0c" +
"r\xdb\x1cQ\xfbr\x90\xe4\x11\xb18x\x8d\x8d5 " +
"`\x0d\xa03`\x14Mk\xa5n\xa3\x96\xeb\xe5kL" +
"n\xe1\x00V\x80\x80\x15\x80\x81{l\xbc{\x17\xe54" +
"\xae\xdb\xc9.}\x8dQ\xe6T\xf7DNu\x97\x9c\xda" +
"\x1cqj\xd3\"\x00e\x1dC\xe5\x06\x01%V\xf2j" +
"K#\xf5\x05\x86\xcaM\x02:\x19\xf7\x90\xae,\x00\x04" +
"\xf6\xae\xe1\xaa]4\xb9E\xb2S\x00{\x18\xban\x9d" +
"\x02\xb8a\x88\x9b\x96f\xe8\xbe\x9bI\xd5\xcc\x0c\x04\xa1" +
"\x98\x04\xaa\xcea\xcd\xb25\xbd\x7f\x85+o\xed1r" +
"Zf\x84\xbc\xaav\xed<}!\x00\xa24\xf3r\x00" +
"\x14$i\x11@\xab\xd6\xaf\x1b&w\xb2\x9a\x951t" +
"\x9d\x03\xcb\xd8\x1b\xfa\xd4\x9c\xaagxpP\xc5\xf8\x83" +
"\xbc\x03\xd2\xdc\x1c\xe2\xe6\xb9j$A\xe6\xf5\xa8\xa6\xca" +
"\xf2\x96R\x1d\xc4\xb1\xf3r\x00\xa5\x83\xa1\xd2\x13\x89\xe3" +
"R\x8a\xe3\x12\x86\xca\xa5\x918\xae\xa48\xf60TV" +
"\x09\xe8\x18\xa6\xd6\xaf\xe9\x17q`f\x14c\xcb\xd6\xd5" +
"<\xa7\x98\x95\xe2\xb1\xc1(\xd8\x9a\xa1[X\x1b\xd6Q" +
"@\xac\x9d\x1cu\xcf\x81$\xe56\xc5'\x11X{\x16" +
"Y\xfb\xff\x0c\x95\xcfE\xacm\xa6T>\x9f\xa1r\xa1" +
"\x80\x8e\x9a\xc9\x18E\xdd^\x01L\xed/\x03%\xcd!" +
"\x991yh\xaf\x7fl|\x82\xbc\xa3xg\xc8\xf4^" +
"\xee\xdd\xa9sMn\x89\xc5\x9cM\xd6T;\x8eg\xce" +
"|\x00e\x1eC\xe5|\x01k\xf0c\xc7\xb3\xa7i{" +
"hO\x037M\xc3\xc4\xda\xb0\xe6\x94\xbc\xcf\x94\x0e@" +
"C\xef\xe0\xb6\xaa\xe5\x90b\x14\xb4\xb9\xb2\x18M\x05r" +
"Q7y\xbff\xd9\xdc\xf4\xc4\xf3Z\x09\xe9\xbc\xa5\xc4" +
"\x82\xd0\xd5\xec\x02Pj\x19*\xa7\x09\xe8\xf4\x9bj\x86" +
"\xf7p\x135#\xbbL\xd5\x8d4\xe3\x19\x8c\x83\x80\xf1" +
"\xc8\xa1\xa7\x9c\xec\xa1\xbd\xdc*\xe6l\x0b\x82U\x93\xaf" +
"7y)\x08\xa5\xe5=\x0d\x9e\xcd\xa9\xc0\xe6\xf5s\xc3" +
"\xda\x13\xc0\xbd\xa9/\xbc\xbaArn\xa5\xc4\xb8\x81\xa1" +
"\xb2#r\xc9\xb7Q\x1a\xdf\xc6P\xb9G@)\x16K" +
"a\x0c@\xba\x9b\xd2x\x07Ce\x8f0\xb6B\xf1!" +
"\xae\xdb\x1dZ?\x88\xdc\x0a\xa5db\x87\xd6\xcf\x81Y" +
"\x9f5\xd1\x13S\xc4\xc3\xe8\xb3\x8c\x1c\xb7y\x07\xcf\xe4" +
"TS\xb5\xb5!\xee}/%\xa3\x0f\xead\x1b\xf6\xba" +
"\x88\xd0bC\x1f\x07S\x98\xd4%\xa8\xd0\x9a\xac^\x85" +
"\xea\xcb\x0b\xb6&\x1a\xbaE\x971\x82\xce\xc2\x89\xd01" +
"Ct\xd0\x07g4\x0a\x0e\x96\xc0\xd9\x15\xe2 \xc5\x04" +
"\x0f\x9c\xdd{\x01\x94=\x0c\x95\x07\x05l\xf5\xca2\xd6" +
"\x86l\xbc\x14P\xaf\xf8,1\xa0!\xa3\xe6\xba\x0a\x01" +
",&/\xe4\xd4\x0c\xef\xc4R\xa1\x05D\x10\x10]\x14" +
"\xf3\x05\x93[\x16j\x86\xae\x14\xd5\x9c\xc6\xec\x91\xa0\xfd" +
"\xe8\xc5|\x8f\xc9\x8744\x8aV\xbbm\xf3\xbcX\xb0" +
"\xadq\xcdi\xd2\x00\xd1\x15\x16\xb5\x9cUV\xad\x1a\xc3" +
"\xf2\x10\x04\xa8\x89\xaa\xd59\x0c\x95\x05\x02&\x8bE-" +
"\x1b\x98\x9f32.n\x90\\\xa6\xe6\xf9\xb8\x8eR1" +
"\xe5u\x1as\x19{\xd4\xa4{\x9b\xfe\x9bJ\xfd\xe4\xfc" +
"\x85\\\x07\xb7\x1f\x86&S\x01hc\xa8,\x89\x98\xdc" +
"5?\xe2\x87o\xf2\xd2\xbe\xd0\x0f\xf1\xeb|\xc4\xb7\xaa" +
"\x81\xe7\xa9\xb8\xfa\xc1,9\xd3\x0e\xe2%\xa1\xced\xf6" +
"E/\xd4\xf2B\x83\xeb!\xd9\xb8\xc0\xb7Q\x1e\xc1n" +
"\x80\xf402Lo\xc6\xd0Ly\x13.\x02H\xaf#" +
"\xf9\x0d\x18Z*o\xc1z\x80\xf4F\x92\xdf\x84\x01\xcf" +
"\x92\xb7\xe2\x03\x00\xe9\x9bH|\x17\xa9\xc7\x98{%\xe4" +
"\x9d\xee\xf6;H\xbe\x87\xe4\xf1X\x0a\xe3\x00\xf2nl" +
"\x04H\xdfE\xf2GI^!\xa4\xb0\x02@\xde\x87\x83" +
"\x00\xe9\x87I\xfe8\xc9\xc5x\x8a\xa8\xa6\xbc\x1fM\x80" +
"\xf4OI\xfek\x92'f\xa70\x01 ?\xe9\xca\x9f" +
" \xf9s$\xaf\x9c\x93\xc2J\x00\xf9\x19\x1c\x05H?" +
"M\xf2\x03$\xaf\xc2\x14V\x01\xc8/\xe3.\x80\xf4\x01" +
"\x92\xff\x81\xe43*R8\x03@~\xc3\xb5\xe7 \xc9" +
"\xdf\"yu,\x85\xd5\x00\xf2\x1fq/@\xfa-\x92" +
"\xff\x9d\xe45b\x0ak\x00\xe4\xb7]\xbf\x8e\x93<!" +
"\x94\x910?\xa3\xca\x98\x163\xac\x002^\xba\xe3\xe8" +
"\xa5{\x8f\x91$6\x85\xc9\xf05\x0e\x88I@\xa7`" +
"\x18\xb9ec35i\xab\xfd\x96\xcf\xeaj\xc3\x17\x1b" +
" \x09\x83\xd6\x0cIC\xef\xca\x06\x85\xa0\xbc\xea\xf8\x96" +
"hV{\xd16\x8a\x05h\xc8\xaa6\xcf\x065\xc7," +
"\xea\x8bM#\xbf\x02\xb9\x99\xd7t57E5\xaa\x04" +
"\x01+\xa1T\x12\xfc\xbd'/M\x9f\xccQ\x83\x8c\x16" +
"\xca3\xba\xa1\xb0p\x85\xda?\x9d:5?\xa4ZI" +
"=R\x90\x1a\x86\xd4\\\xf1\xd3\x94\xa7\xb1\xdd\xbe\xb7\xd5" +
"c\x0bQ\x8aB\x0d%\xc1PI\x09\xd8jz\x1d\xaa" +
"\xd6\x7f\xdaM]J\xc6r\xb6\xa4\xff\x10r\xf7\xc7\xc8" +
"\xb8\x84\xce\x11J\xfbO\xdb\xfc~n{\xbf\xe8)B" +
"<Z\x8cv\xe2\x93[\xdd\xcb\xad\xe4t\\\x0f\x9f\xc0" +
"e\xce\x8b\xd3j\xfc\x13\xb4}\x9f\x16F\xdeQ\x84\xfd" +
"*\x86\xca@\x04{NM!\xcbP)\x84M<\xdf" +
"\x1b\xbe\x0d%&\x94\x1e\x87\xd4(\x0a\x0c\x95u\x02&" +
"\xe9\xa5\x81\xb5\xe1\x0ck\x8c\xd1c_W\x94\x0a]z" +
"\x96\x03\x0e\xfb\xd9\x1ci\x1f\xc1lhj\x025=\xb7" +
"}b:e\xc0\x83y\xcbt\xdf(\xad\xde\xa1\x94g" +
"\xb3Y\x1c \x98=\xa1?\xdf\x90\xf6\xad\x05A\xfa\xbe" +
"\x88\xe1|\x06\xfdq\x8c\xb4\xdb\x04A\xda)\xa2\x10\x0c" +
"\xe8\xd0\x1f\xc4I[o\x04A\xda\"\"\x0b\xe6k\xe8" +
"O\x08\x9aG\xaa\x10\x04i\xbd\x88\xb1`\xa6\x88\xfe|" +
"A\xbaz\x10\x04I\x131\x1e\x8c\xee\xd0\x1f4I\xab" +
"GA\x90V\x8a\x8e\x1f$h\xf5\xfchC\xc7\xcfQ" +
"hp\xb3\xb4\x0d\x1d\x9f9\xa2O*\x00\xda\xd0\xf1i" +
"*\xfb$\x9e\xeaj\xf9\xcfNH\xd2\xc3\xb3\x8d\xc8\x99" +
"w\xff\xb1T\x00\xa0\x0d\x95\x18F\xc6+\x00\x9f\x96\xea" +
"\xf4\xf2\x86\xcfRJ&\x00\xd7;'\x18;D\xf6%" +
"\xf6V\xcdP\x99-LI\xd8b\x9f\xe4\x85\x9f\xb4I" +
"ZL\xfb\xffo\xb0\xff\xcbDx\x9ec\xa8\x1c\x8c\\" +
"\xc7WI\xf8\x12C\xe5p\x84\xf0\x1c\xa2;z\x90\xa1" +
"\xf2~8\xaby\xf7F\x00\xe5}\x86\xbd\x11\x02!\xfd" +
"\x8b\x14?\xa26\xeb\xd2\x07\xf4\xe8C\x1c\xb7\x03\xa4\x13" +
"\xd4~S.}\x88y\xf4A\xc2>\x80t-\xc9O" +
"\x8b\xd2\x879x9@z6\xc9\xe7\xa1\x80\"\x8f\xcc" +
"y\x8afH\xb0rF\xff\x12M\x9f\xb0'\xf9\xc3#" +
"\xb4\x17\xabZ\xaehr\x08[b\xa9HtD\xba\xb4" +
"7Uj_C\xe9\x97\xa6\xe4\xc9\xa2\x85\"\x08(\x9e" +
"\xdccmZ\x1d\xa3\xd34\x0d4\xcb\xc8\xe7\xfc\x90|" +
"\x06\xdc\x938\xf4\xc5\x0c\x95\x15\x04E\x9b\x07\x85\xd2\x17" +
"\xd2\xe5\x86\x8cZ\xb4\xf88\x1f\x80q3x`[\x03" +
"F1\x97\xed\xe5 \xda\xe6HY\x08\xa6$\xa1i\x9e" +
"\xf4+N\xc2\xad8\xfe\xe8\x16\xfd\x09\xad\xd4\xbc\x0b\x04" +
"\xa9\x89*\x8e?\x8dD\x7f\x10/\xfd\xdf\x03 H\xa7" +
"\x87\x05\x00\xfd\x180C\x1f{\xe5\xbd\x0fn\x8e\xb6a" +
"\x0f\xe2\x7f\xe2\xd9\xe9\xb5\x9f\x93\xb8\xe9c\xa6WT\xc6" +
"\xc5\xe9\xf4\xcd\xe0\xcf\x9e\xb2\x9b^\xf9Y_\xe0~#" +
"\xf9w\x00\x00\x00\xff\xffH\xa22\xa3"
func init() {
schemas.Register(schema_db8274f9144abc7e,
+286 -53
View File
@@ -6,12 +6,15 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"path"
"strings"
"time"
"github.com/cloudflare/cloudflared/logger"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/cloudflare/cloudflared/logger"
)
const (
@@ -24,10 +27,11 @@ var (
ErrUnauthorized = errors.New("unauthorized")
ErrBadRequest = errors.New("incorrect request parameters")
ErrNotFound = errors.New("not found")
ErrAPINoSuccess = errors.New("API call failed")
)
type Tunnel struct {
ID string `json:"id"`
ID uuid.UUID `json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"created_at"`
DeletedAt time.Time `json:"deleted_at"`
@@ -35,35 +39,195 @@ type Tunnel struct {
}
type Connection struct {
ColoName string `json:"colo_name"`
ID uuid.UUID `json:"uuid"`
ColoName string `json:"colo_name"`
ID uuid.UUID `json:"id"`
IsPendingReconnect bool `json:"is_pending_reconnect"`
}
type Change = string
const (
ChangeNew = "new"
ChangeUpdated = "updated"
ChangeUnchanged = "unchanged"
)
// Route represents a record type that can route to a tunnel
type Route interface {
json.Marshaler
RecordType() string
UnmarshalResult(body io.Reader) (RouteResult, error)
}
type RouteResult interface {
// SuccessSummary explains what will route to this tunnel when it's provisioned successfully
SuccessSummary() string
}
type DNSRoute struct {
userHostname string
}
type DNSRouteResult struct {
route *DNSRoute
CName Change `json:"cname"`
}
func NewDNSRoute(userHostname string) Route {
return &DNSRoute{
userHostname: userHostname,
}
}
func (dr *DNSRoute) MarshalJSON() ([]byte, error) {
s := struct {
Type string `json:"type"`
UserHostname string `json:"user_hostname"`
}{
Type: dr.RecordType(),
UserHostname: dr.userHostname,
}
return json.Marshal(&s)
}
func (dr *DNSRoute) UnmarshalResult(body io.Reader) (RouteResult, error) {
var result DNSRouteResult
err := parseResponse(body, &result)
result.route = dr
return &result, err
}
func (dr *DNSRoute) RecordType() string {
return "dns"
}
func (res *DNSRouteResult) SuccessSummary() string {
var msgFmt string
switch res.CName {
case ChangeNew:
msgFmt = "Added CNAME %s which will route to this tunnel"
case ChangeUpdated: // this is not currently returned by tunnelsore
msgFmt = "%s updated to route to your tunnel"
case ChangeUnchanged:
msgFmt = "%s is already configured to route to your tunnel"
}
return fmt.Sprintf(msgFmt, res.route.userHostname)
}
type LBRoute struct {
lbName string
lbPool string
}
type LBRouteResult struct {
route *LBRoute
LoadBalancer Change `json:"load_balancer"`
Pool Change `json:"pool"`
}
func NewLBRoute(lbName, lbPool string) Route {
return &LBRoute{
lbName: lbName,
lbPool: lbPool,
}
}
func (lr *LBRoute) MarshalJSON() ([]byte, error) {
s := struct {
Type string `json:"type"`
LBName string `json:"lb_name"`
LBPool string `json:"lb_pool"`
}{
Type: lr.RecordType(),
LBName: lr.lbName,
LBPool: lr.lbPool,
}
return json.Marshal(&s)
}
func (lr *LBRoute) RecordType() string {
return "lb"
}
func (lr *LBRoute) UnmarshalResult(body io.Reader) (RouteResult, error) {
var result LBRouteResult
err := parseResponse(body, &result)
result.route = lr
return &result, err
}
func (res *LBRouteResult) SuccessSummary() string {
var msg string
switch res.LoadBalancer + "," + res.Pool {
case "new,new":
msg = "Created load balancer %s and added a new pool %s with this tunnel as an origin"
case "new,updated":
msg = "Created load balancer %s with an existing pool %s which was updated to use this tunnel as an origin"
case "new,unchanged":
msg = "Created load balancer %s with an existing pool %s which already has this tunnel as an origin"
case "updated,new":
msg = "Added new pool %[2]s with this tunnel as an origin to load balancer %[1]s"
case "updated,updated":
msg = "Updated pool %[2]s to use this tunnel as an origin and added it to load balancer %[1]s"
case "updated,unchanged":
msg = "Added pool %[2]s, which already has this tunnel as an origin, to load balancer %[1]s"
case "unchanged,updated":
msg = "Added this tunnel as an origin in pool %[2]s which is already used by load balancer %[1]s"
case "unchanged,unchanged":
msg = "Load balancer %s already uses pool %s which has this tunnel as an origin"
case "unchanged,new":
// this state is not possible
fallthrough
default:
msg = "Something went wrong: failed to modify load balancer %s with pool %s; please check traffic manager configuration in the dashboard"
}
return fmt.Sprintf(msg, res.route.lbName, res.route.lbPool)
}
type Client interface {
CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error)
GetTunnel(tunnelID string) (*Tunnel, error)
DeleteTunnel(tunnelID string) error
ListTunnels() ([]Tunnel, error)
CleanupConnections(tunnelID string) error
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
DeleteTunnel(tunnelID uuid.UUID) error
ListTunnels(filter *Filter) ([]*Tunnel, error)
CleanupConnections(tunnelID uuid.UUID) error
RouteTunnel(tunnelID uuid.UUID, route Route) (RouteResult, error)
}
type RESTClient struct {
baseURL string
authToken string
client http.Client
logger logger.Service
baseEndpoints *baseEndpoints
authToken string
userAgent string
client http.Client
logger logger.Service
}
type baseEndpoints struct {
accountLevel url.URL
zoneLevel url.URL
}
var _ Client = (*RESTClient)(nil)
func NewRESTClient(baseURL string, accountTag string, authToken string, logger logger.Service) *RESTClient {
func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, logger logger.Service) (*RESTClient, error) {
if strings.HasSuffix(baseURL, "/") {
baseURL = baseURL[:len(baseURL)-1]
}
url := fmt.Sprintf("%s/accounts/%s/tunnels", baseURL, accountTag)
accountLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/tunnels", baseURL, accountTag))
if err != nil {
return nil, errors.Wrap(err, "failed to create account level endpoint")
}
zoneLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/zones/%s/tunnels", baseURL, zoneTag))
if err != nil {
return nil, errors.Wrap(err, "failed to create account level endpoint")
}
return &RESTClient{
baseURL: url,
baseEndpoints: &baseEndpoints{
accountLevel: *accountLevelEndpoint,
zoneLevel: *zoneLevelEndpoint,
},
authToken: authToken,
userAgent: userAgent,
client: http.Client{
Transport: &http.Transport{
TLSHandshakeTimeout: defaultTimeout,
@@ -72,7 +236,7 @@ func NewRESTClient(baseURL string, accountTag string, authToken string, logger l
Timeout: defaultTimeout,
},
logger: logger,
}
}, nil
}
type newTunnel struct {
@@ -84,15 +248,15 @@ func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, er
if name == "" {
return nil, errors.New("tunnel name required")
}
body, err := json.Marshal(&newTunnel{
if _, err := uuid.Parse(name); err == nil {
return nil, errors.New("you cannot use UUIDs as tunnel names")
}
body := &newTunnel{
Name: name,
TunnelSecret: tunnelSecret,
})
if err != nil {
return nil, errors.Wrap(err, "Failed to serialize new tunnel request")
}
resp, err := r.sendRequest("POST", "", bytes.NewBuffer(body))
resp, err := r.sendRequest("POST", r.baseEndpoints.accountLevel, body)
if err != nil {
return nil, errors.Wrap(err, "REST request failed")
}
@@ -105,11 +269,13 @@ func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, er
return nil, ErrTunnelNameConflict
}
return nil, statusCodeToError("create tunnel", resp)
return nil, r.statusCodeToError("create tunnel", resp)
}
func (r *RESTClient) GetTunnel(tunnelID string) (*Tunnel, error) {
resp, err := r.sendRequest("GET", tunnelID, nil)
func (r *RESTClient) GetTunnel(tunnelID uuid.UUID) (*Tunnel, error) {
endpoint := r.baseEndpoints.accountLevel
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
resp, err := r.sendRequest("GET", endpoint, nil)
if err != nil {
return nil, errors.Wrap(err, "REST request failed")
}
@@ -119,77 +285,144 @@ func (r *RESTClient) GetTunnel(tunnelID string) (*Tunnel, error) {
return unmarshalTunnel(resp.Body)
}
return nil, statusCodeToError("get tunnel", resp)
return nil, r.statusCodeToError("get tunnel", resp)
}
func (r *RESTClient) DeleteTunnel(tunnelID string) error {
resp, err := r.sendRequest("DELETE", tunnelID, nil)
func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID) error {
endpoint := r.baseEndpoints.accountLevel
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
resp, err := r.sendRequest("DELETE", endpoint, nil)
if err != nil {
return errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
return statusCodeToError("delete tunnel", resp)
return r.statusCodeToError("delete tunnel", resp)
}
func (r *RESTClient) ListTunnels() ([]Tunnel, error) {
resp, err := r.sendRequest("GET", "", nil)
func (r *RESTClient) ListTunnels(filter *Filter) ([]*Tunnel, error) {
endpoint := r.baseEndpoints.accountLevel
endpoint.RawQuery = filter.encode()
resp, err := r.sendRequest("GET", endpoint, nil)
if err != nil {
return nil, errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
var tunnels []Tunnel
if err := json.NewDecoder(resp.Body).Decode(&tunnels); err != nil {
return nil, errors.Wrap(err, "failed to decode response")
}
return tunnels, nil
return parseListTunnels(resp.Body)
}
return nil, statusCodeToError("list tunnels", resp)
return nil, r.statusCodeToError("list tunnels", resp)
}
func (r *RESTClient) CleanupConnections(tunnelID string) error {
resp, err := r.sendRequest("DELETE", fmt.Sprintf("%s/connections", tunnelID), nil)
func parseListTunnels(body io.ReadCloser) ([]*Tunnel, error) {
var tunnels []*Tunnel
err := parseResponse(body, &tunnels)
return tunnels, err
}
func (r *RESTClient) CleanupConnections(tunnelID uuid.UUID) error {
endpoint := r.baseEndpoints.accountLevel
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
resp, err := r.sendRequest("DELETE", endpoint, nil)
if err != nil {
return errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
return statusCodeToError("cleanup connections", resp)
return r.statusCodeToError("cleanup connections", resp)
}
func (r *RESTClient) resolve(target string) string {
if target != "" {
return r.baseURL + "/" + target
func (r *RESTClient) RouteTunnel(tunnelID uuid.UUID, route Route) (RouteResult, error) {
endpoint := r.baseEndpoints.zoneLevel
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/routes", tunnelID))
resp, err := r.sendRequest("PUT", endpoint, route)
if err != nil {
return nil, errors.Wrap(err, "REST request failed")
}
return r.baseURL
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return route.UnmarshalResult(resp.Body)
}
return nil, r.statusCodeToError("add route", resp)
}
func (r *RESTClient) sendRequest(method string, target string, body io.Reader) (*http.Response, error) {
url := r.resolve(target)
r.logger.Debugf("%s %s", method, url)
req, err := http.NewRequest(method, url, body)
func (r *RESTClient) sendRequest(method string, url url.URL, body interface{}) (*http.Response, error) {
var bodyReader io.Reader
if body != nil {
if bodyBytes, err := json.Marshal(body); err != nil {
return nil, errors.Wrap(err, "failed to serialize json body")
} else {
bodyReader = bytes.NewBuffer(bodyBytes)
}
}
req, err := http.NewRequest(method, url.String(), bodyReader)
if err != nil {
return nil, errors.Wrapf(err, "can't create %s request", method)
}
if body != nil {
req.Header.Set("User-Agent", r.userAgent)
if bodyReader != nil {
req.Header.Set("Content-Type", jsonContentType)
}
req.Header.Add("X-Auth-User-Service-Key", r.authToken)
req.Header.Add("Accept", "application/json;version=1")
return r.client.Do(req)
}
func parseResponse(reader io.Reader, data interface{}) error {
// Schema for Tunnelstore responses in the v1 API.
// Roughly, it's a wrapper around a particular result that adds failures/errors/etc
var result struct {
Result json.RawMessage `json:"result"`
Success bool `json:"success"`
Errors []string `json:"errors"`
}
// First, parse the wrapper and check the API call succeeded
if err := json.NewDecoder(reader).Decode(&result); err != nil {
return errors.Wrap(err, "failed to decode response")
}
if err := checkErrors(result.Errors); err != nil {
return err
}
if !result.Success {
return ErrAPINoSuccess
}
// At this point we know the API call succeeded, so, parse out the inner
// result into the datatype provided as a parameter.
return json.Unmarshal(result.Result, &data)
}
func unmarshalTunnel(reader io.Reader) (*Tunnel, error) {
var tunnel Tunnel
if err := json.NewDecoder(reader).Decode(&tunnel); err != nil {
return nil, errors.Wrap(err, "failed to decode response")
}
return &tunnel, nil
err := parseResponse(reader, &tunnel)
return &tunnel, err
}
func statusCodeToError(op string, resp *http.Response) error {
func checkErrors(errs []string) error {
if len(errs) == 0 {
return nil
}
if len(errs) == 1 {
return fmt.Errorf("API error: %s", errs[0])
}
allErrs := strings.Join(errs, "; ")
return fmt.Errorf("API errors: %s", allErrs)
}
func (r *RESTClient) statusCodeToError(op string, resp *http.Response) error {
if resp.Header.Get("Content-Type") == "application/json" {
var errorsResp struct {
Error string `json:"error"`
}
if json.NewDecoder(resp.Body).Decode(&errorsResp) == nil && errorsResp.Error != "" {
return errors.Errorf("Failed to %s: %s", op, errorsResp.Error)
}
}
switch resp.StatusCode {
case http.StatusOK:
return nil
+207
View File
@@ -0,0 +1,207 @@
package tunnelstore
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"reflect"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func TestDNSRouteUnmarshalResult(t *testing.T) {
route := &DNSRoute{
userHostname: "example.com",
}
result, err := route.UnmarshalResult(strings.NewReader(`{"success": true, "result": {"cname": "new"}}`))
assert.NoError(t, err)
assert.Equal(t, &DNSRouteResult{
route: route,
CName: ChangeNew,
}, result)
badJSON := []string{
`abc`,
`{"success": false, "result": {"cname": "new"}}`,
`{"errors": ["foo"], "result": {"cname": "new"}}`,
`{"errors": ["foo", "bar"], "result": {"cname": "new"}}`,
`{"result": {"cname": "new"}}`,
`{"result": {"cname": "new"}}`,
}
for _, j := range badJSON {
_, err = route.UnmarshalResult(strings.NewReader(j))
assert.NotNil(t, err)
}
}
func TestLBRouteUnmarshalResult(t *testing.T) {
route := &LBRoute{
lbName: "lb.example.com",
lbPool: "pool",
}
result, err := route.UnmarshalResult(strings.NewReader(`{"success": true, "result": {"pool": "unchanged", "load_balancer": "updated"}}`))
assert.NoError(t, err)
assert.Equal(t, &LBRouteResult{
route: route,
LoadBalancer: ChangeUpdated,
Pool: ChangeUnchanged,
}, result)
badJSON := []string{
`abc`,
`{"success": false, "result": {"pool": "unchanged", "load_balancer": "updated"}}`,
`{"errors": ["foo"], "result": {"pool": "unchanged", "load_balancer": "updated"}}`,
`{"errors": ["foo", "bar"], "result": {"pool": "unchanged", "load_balancer": "updated"}}`,
`{"result": {"pool": "unchanged", "load_balancer": "updated"}}`,
}
for _, j := range badJSON {
_, err = route.UnmarshalResult(strings.NewReader(j))
assert.NotNil(t, err)
}
}
func TestLBRouteResultSuccessSummary(t *testing.T) {
route := &LBRoute{
lbName: "lb.example.com",
lbPool: "POOL",
}
tests := []struct {
lb Change
pool Change
expected string
}{
{ChangeNew, ChangeNew, "Created load balancer lb.example.com and added a new pool POOL with this tunnel as an origin"},
{ChangeNew, ChangeUpdated, "Created load balancer lb.example.com with an existing pool POOL which was updated to use this tunnel as an origin"},
{ChangeNew, ChangeUnchanged, "Created load balancer lb.example.com with an existing pool POOL which already has this tunnel as an origin"},
{ChangeUpdated, ChangeNew, "Added new pool POOL with this tunnel as an origin to load balancer lb.example.com"},
{ChangeUpdated, ChangeUpdated, "Updated pool POOL to use this tunnel as an origin and added it to load balancer lb.example.com"},
{ChangeUpdated, ChangeUnchanged, "Added pool POOL, which already has this tunnel as an origin, to load balancer lb.example.com"},
{ChangeUnchanged, ChangeNew, "Something went wrong: failed to modify load balancer lb.example.com with pool POOL; please check traffic manager configuration in the dashboard"},
{ChangeUnchanged, ChangeUpdated, "Added this tunnel as an origin in pool POOL which is already used by load balancer lb.example.com"},
{ChangeUnchanged, ChangeUnchanged, "Load balancer lb.example.com already uses pool POOL which has this tunnel as an origin"},
{"", "", "Something went wrong: failed to modify load balancer lb.example.com with pool POOL; please check traffic manager configuration in the dashboard"},
{"a", "b", "Something went wrong: failed to modify load balancer lb.example.com with pool POOL; please check traffic manager configuration in the dashboard"},
}
for i, tt := range tests {
res := &LBRouteResult{
route: route,
LoadBalancer: tt.lb,
Pool: tt.pool,
}
actual := res.SuccessSummary()
assert.Equal(t, tt.expected, actual, "case %d", i+1)
}
}
func Test_parseListTunnels(t *testing.T) {
type args struct {
body string
}
tests := []struct {
name string
args args
want []*Tunnel
wantErr bool
}{
{
name: "empty list",
args: args{body: `{"success": true, "result": []}`},
want: []*Tunnel{},
},
{
name: "success is false",
args: args{body: `{"success": false, "result": []}`},
wantErr: true,
},
{
name: "errors are present",
args: args{body: `{"errors": ["foo"], "result": []}`},
wantErr: true,
},
{
name: "invalid response",
args: args{body: `abc`},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
body := ioutil.NopCloser(bytes.NewReader([]byte(tt.args.body)))
got, err := parseListTunnels(body)
if (err != nil) != tt.wantErr {
t.Errorf("parseListTunnels() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseListTunnels() = %v, want %v", got, tt.want)
}
})
}
}
func Test_unmarshalTunnel(t *testing.T) {
type args struct {
reader io.Reader
}
tests := []struct {
name string
args args
want *Tunnel
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := unmarshalTunnel(tt.args.reader)
if (err != nil) != tt.wantErr {
t.Errorf("unmarshalTunnel() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("unmarshalTunnel() = %v, want %v", got, tt.want)
}
})
}
}
func TestUnmarshalTunnelOk(t *testing.T) {
jsonBody := `{"success": true, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}`
expected := Tunnel{
ID: uuid.Nil,
Name: "test",
CreatedAt: time.Time{},
Connections: []Connection{},
}
actual, err := unmarshalTunnel(bytes.NewReader([]byte(jsonBody)))
assert.NoError(t, err)
assert.Equal(t, &expected, actual)
}
func TestUnmarshalTunnelErr(t *testing.T) {
tests := []string{
`abc`,
`{"success": true, "result": abc}`,
`{"success": false, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}}`,
`{"errors": ["foo"], "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}}`,
}
for i, test := range tests {
_, err := unmarshalTunnel(bytes.NewReader([]byte(test)))
assert.Error(t, err, fmt.Sprintf("Test #%v failed", i))
}
}
+42
View File
@@ -0,0 +1,42 @@
package tunnelstore
import (
"net/url"
"time"
"github.com/google/uuid"
)
const (
TimeLayout = time.RFC3339
)
type Filter struct {
queryParams url.Values
}
func NewFilter() *Filter {
return &Filter{
queryParams: url.Values{},
}
}
func (f *Filter) ByName(name string) {
f.queryParams.Set("name", name)
}
func (f *Filter) NoDeleted() {
f.queryParams.Set("is_deleted", "false")
}
func (f *Filter) ByExistedAt(existedAt time.Time) {
f.queryParams.Set("existed_at", existedAt.Format(TimeLayout))
}
func (f *Filter) ByTunnelID(tunnelID uuid.UUID) {
f.queryParams.Set("uuid", tunnelID.String())
}
func (f Filter) encode() string {
return f.queryParams.Encode()
}
+3 -1
View File
@@ -27,6 +27,7 @@ var (
// ParseBase2Bytes supports both iB and B in base-2 multipliers. That is, KB
// and KiB are both 1024.
// However "kB", which is the correct SI spelling of 1000 Bytes, is rejected.
func ParseBase2Bytes(s string) (Base2Bytes, error) {
n, err := ParseUnit(s, bytesUnitMap)
if err != nil {
@@ -68,12 +69,13 @@ func ParseMetricBytes(s string) (MetricBytes, error) {
return MetricBytes(n), err
}
// TODO: represents 1000B as uppercase "KB", while SI standard requires "kB".
func (m MetricBytes) String() string {
return ToString(int64(m), 1000, "B", "B")
}
// ParseStrictBytes supports both iB and B suffixes for base 2 and metric,
// respectively. That is, KiB represents 1024 and KB represents 1000.
// respectively. That is, KiB represents 1024 and kB, KB represent 1000.
func ParseStrictBytes(s string) (int64, error) {
n, err := ParseUnit(s, bytesUnitMap)
if err != nil {
+2
View File
@@ -1 +1,3 @@
module github.com/alecthomas/units
require github.com/stretchr/testify v1.4.0
+11
View File
@@ -0,0 +1,11 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+27 -3
View File
@@ -14,13 +14,37 @@ const (
)
func MakeUnitMap(suffix, shortSuffix string, scale int64) map[string]float64 {
return map[string]float64{
shortSuffix: 1,
"K" + suffix: float64(scale),
res := map[string]float64{
shortSuffix: 1,
// see below for "k" / "K"
"M" + suffix: float64(scale * scale),
"G" + suffix: float64(scale * scale * scale),
"T" + suffix: float64(scale * scale * scale * scale),
"P" + suffix: float64(scale * scale * scale * scale * scale),
"E" + suffix: float64(scale * scale * scale * scale * scale * scale),
}
// Standard SI prefixes use lowercase "k" for kilo = 1000.
// For compatibility, and to be fool-proof, we accept both "k" and "K" in metric mode.
//
// However, official binary prefixes are always capitalized - "KiB" -
// and we specifically never parse "kB" as 1024B because:
//
// (1) people pedantic enough to use lowercase according to SI unlikely to abuse "k" to mean 1024 :-)
//
// (2) Use of capital K for 1024 was an informal tradition predating IEC prefixes:
// "The binary meaning of the kilobyte for 1024 bytes typically uses the symbol KB, with an
// uppercase letter K."
// -- https://en.wikipedia.org/wiki/Kilobyte#Base_2_(1024_bytes)
// "Capitalization of the letter K became the de facto standard for binary notation, although this
// could not be extended to higher powers, and use of the lowercase k did persist.[13][14][15]"
// -- https://en.wikipedia.org/wiki/Binary_prefix#History
// See also the extensive https://en.wikipedia.org/wiki/Timeline_of_binary_prefixes.
if scale == 1024 {
res["K"+suffix] = float64(scale)
} else {
res["k"+suffix] = float64(scale)
res["K"+suffix] = float64(scale)
}
return res
}
+93
View File
@@ -0,0 +1,93 @@
// Package arn provides a parser for interacting with Amazon Resource Names.
package arn
import (
"errors"
"strings"
)
const (
arnDelimiter = ":"
arnSections = 6
arnPrefix = "arn:"
// zero-indexed
sectionPartition = 1
sectionService = 2
sectionRegion = 3
sectionAccountID = 4
sectionResource = 5
// errors
invalidPrefix = "arn: invalid prefix"
invalidSections = "arn: not enough sections"
)
// ARN captures the individual fields of an Amazon Resource Name.
// See http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html for more information.
type ARN struct {
// The partition that the resource is in. For standard AWS regions, the partition is "aws". If you have resources in
// other partitions, the partition is "aws-partitionname". For example, the partition for resources in the China
// (Beijing) region is "aws-cn".
Partition string
// The service namespace that identifies the AWS product (for example, Amazon S3, IAM, or Amazon RDS). For a list of
// namespaces, see
// http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#genref-aws-service-namespaces.
Service string
// The region the resource resides in. Note that the ARNs for some resources do not require a region, so this
// component might be omitted.
Region string
// The ID of the AWS account that owns the resource, without the hyphens. For example, 123456789012. Note that the
// ARNs for some resources don't require an account number, so this component might be omitted.
AccountID string
// The content of this part of the ARN varies by service. It often includes an indicator of the type of resource —
// for example, an IAM user or Amazon RDS database - followed by a slash (/) or a colon (:), followed by the
// resource name itself. Some services allows paths for resource names, as described in
// http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#arns-paths.
Resource string
}
// Parse parses an ARN into its constituent parts.
//
// Some example ARNs:
// arn:aws:elasticbeanstalk:us-east-1:123456789012:environment/My App/MyEnvironment
// arn:aws:iam::123456789012:user/David
// arn:aws:rds:eu-west-1:123456789012:db:mysql-db
// arn:aws:s3:::my_corporate_bucket/exampleobject.png
func Parse(arn string) (ARN, error) {
if !strings.HasPrefix(arn, arnPrefix) {
return ARN{}, errors.New(invalidPrefix)
}
sections := strings.SplitN(arn, arnDelimiter, arnSections)
if len(sections) != arnSections {
return ARN{}, errors.New(invalidSections)
}
return ARN{
Partition: sections[sectionPartition],
Service: sections[sectionService],
Region: sections[sectionRegion],
AccountID: sections[sectionAccountID],
Resource: sections[sectionResource],
}, nil
}
// IsARN returns whether the given string is an ARN by looking for
// whether the string starts with "arn:" and contains the correct number
// of sections delimited by colons(:).
func IsARN(arn string) bool {
return strings.HasPrefix(arn, arnPrefix) && strings.Count(arn, ":") >= arnSections-1
}
// String returns the canonical representation of the ARN
func (arn ARN) String() string {
return arnPrefix +
arn.Partition + arnDelimiter +
arn.Service + arnDelimiter +
arn.Region + arnDelimiter +
arn.AccountID + arnDelimiter +
arn.Resource
}
+1 -1
View File
@@ -70,7 +70,7 @@ func rValuesAtPath(v interface{}, path string, createPath, caseSensitive, nilTer
value = value.FieldByNameFunc(func(name string) bool {
if c == name {
return true
} else if !caseSensitive && strings.ToLower(name) == strings.ToLower(c) {
} else if !caseSensitive && strings.EqualFold(name, c) {
return true
}
return false
+1
View File
@@ -12,6 +12,7 @@ import (
type Config struct {
Config *aws.Config
Handlers request.Handlers
PartitionID string
Endpoint string
SigningRegion string
SigningName string
+3 -3
View File
@@ -16,11 +16,11 @@ import (
type DefaultRetryer struct {
// Num max Retries is the number of max retries that will be performed.
// By default, this is zero.
NumMaxRetries int
NumMaxRetries int
// MinRetryDelay is the minimum retry delay after which retry will be performed.
// If not set, the value is 0ns.
MinRetryDelay time.Duration
MinRetryDelay time.Duration
// MinThrottleRetryDelay is the minimum retry delay when throttled.
// If not set, the value is 0ns.
@@ -28,7 +28,7 @@ type DefaultRetryer struct {
// MaxRetryDelay is the maximum retry delay before which retry must be performed.
// If not set, the value is 0ns.
MaxRetryDelay time.Duration
MaxRetryDelay time.Duration
// MaxThrottleDelay is the maximum retry delay when throttled.
// If not set, the value is 0ns.
+1
View File
@@ -5,6 +5,7 @@ type ClientInfo struct {
ServiceName string
ServiceID string
APIVersion string
PartitionID string
Endpoint string
SigningName string
SigningRegion string
+55 -4
View File
@@ -43,7 +43,7 @@ type Config struct {
// An optional endpoint URL (hostname only or fully qualified URI)
// that overrides the default generated endpoint for a client. Set this
// to `""` to use the default generated endpoint.
// to `nil` or the value to `""` to use the default generated endpoint.
//
// Note: You must still provide a `Region` value when specifying an
// endpoint for a client.
@@ -138,7 +138,7 @@ type Config struct {
// `ExpectContinueTimeout` for information on adjusting the continue wait
// timeout. https://golang.org/pkg/net/http/#Transport
//
// You should use this flag to disble 100-Continue if you experience issues
// You should use this flag to disable 100-Continue if you experience issues
// with proxies or third party S3 compatible services.
S3Disable100Continue *bool
@@ -161,6 +161,17 @@ type Config struct {
// on GetObject API calls.
S3DisableContentMD5Validation *bool
// Set this to `true` to have the S3 service client to use the region specified
// in the ARN, when an ARN is provided as an argument to a bucket parameter.
S3UseARNRegion *bool
// Set this to `true` to enable the SDK to unmarshal API response header maps to
// normalized lower case map keys.
//
// For example S3's X-Amz-Meta prefixed header will be unmarshaled to lower case
// Metadata member's map keys. The value of the header in the map is unaffected.
LowerCaseHeaderMaps *bool
// Set this to `true` to disable the EC2Metadata client from overriding the
// default http.Client's Timeout. This is helpful if you do not want the
// EC2Metadata client to create a new http.Client. This options is only
@@ -172,7 +183,7 @@ type Config struct {
//
// Example:
// sess := session.Must(session.NewSession(aws.NewConfig()
// .WithEC2MetadataDiableTimeoutOverride(true)))
// .WithEC2MetadataDisableTimeoutOverride(true)))
//
// svc := s3.New(sess)
//
@@ -183,7 +194,7 @@ type Config struct {
// both IPv4 and IPv6 addressing.
//
// Setting this for a service which does not support dual stack will fail
// to make requets. It is not recommended to set this value on the session
// to make requests. It is not recommended to set this value on the session
// as it will apply to all service clients created with the session. Even
// services which don't support dual stack endpoints.
//
@@ -227,6 +238,7 @@ type Config struct {
// EnableEndpointDiscovery will allow for endpoint discovery on operations that
// have the definition in its model. By default, endpoint discovery is off.
// To use EndpointDiscovery, Endpoint should be unset or set to an empty string.
//
// Example:
// sess := session.Must(session.NewSession(&aws.Config{
@@ -246,6 +258,12 @@ type Config struct {
// Disabling this feature is useful when you want to use local endpoints
// for testing that do not support the modeled host prefix pattern.
DisableEndpointHostPrefix *bool
// STSRegionalEndpoint will enable regional or legacy endpoint resolving
STSRegionalEndpoint endpoints.STSRegionalEndpoint
// S3UsEast1RegionalEndpoint will enable regional or legacy endpoint resolving
S3UsEast1RegionalEndpoint endpoints.S3UsEast1RegionalEndpoint
}
// NewConfig returns a new Config pointer that can be chained with builder
@@ -379,6 +397,13 @@ func (c *Config) WithS3DisableContentMD5Validation(enable bool) *Config {
}
// WithS3UseARNRegion sets a config S3UseARNRegion value and
// returning a Config pointer for chaining
func (c *Config) WithS3UseARNRegion(enable bool) *Config {
c.S3UseARNRegion = &enable
return c
}
// WithUseDualStack sets a config UseDualStack value returning a Config
// pointer for chaining.
func (c *Config) WithUseDualStack(enable bool) *Config {
@@ -420,6 +445,20 @@ func (c *Config) MergeIn(cfgs ...*Config) {
}
}
// WithSTSRegionalEndpoint will set whether or not to use regional endpoint flag
// when resolving the endpoint for a service
func (c *Config) WithSTSRegionalEndpoint(sre endpoints.STSRegionalEndpoint) *Config {
c.STSRegionalEndpoint = sre
return c
}
// WithS3UsEast1RegionalEndpoint will set whether or not to use regional endpoint flag
// when resolving the endpoint for a service
func (c *Config) WithS3UsEast1RegionalEndpoint(sre endpoints.S3UsEast1RegionalEndpoint) *Config {
c.S3UsEast1RegionalEndpoint = sre
return c
}
func mergeInConfig(dst *Config, other *Config) {
if other == nil {
return
@@ -493,6 +532,10 @@ func mergeInConfig(dst *Config, other *Config) {
dst.S3DisableContentMD5Validation = other.S3DisableContentMD5Validation
}
if other.S3UseARNRegion != nil {
dst.S3UseARNRegion = other.S3UseARNRegion
}
if other.UseDualStack != nil {
dst.UseDualStack = other.UseDualStack
}
@@ -520,6 +563,14 @@ func mergeInConfig(dst *Config, other *Config) {
if other.DisableEndpointHostPrefix != nil {
dst.DisableEndpointHostPrefix = other.DisableEndpointHostPrefix
}
if other.STSRegionalEndpoint != endpoints.UnsetSTSEndpoint {
dst.STSRegionalEndpoint = other.STSRegionalEndpoint
}
if other.S3UsEast1RegionalEndpoint != endpoints.UnsetS3UsEast1Endpoint {
dst.S3UsEast1RegionalEndpoint = other.S3UsEast1RegionalEndpoint
}
}
// Copy will return a shallow copy of the Config object. If any additional
+3 -37
View File
@@ -2,42 +2,8 @@
package aws
import "time"
// An emptyCtx is a copy of the Go 1.7 context.emptyCtx type. This is copied to
// provide a 1.6 and 1.5 safe version of context that is compatible with Go
// 1.7's Context.
//
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}
func (e *emptyCtx) String() string {
switch e {
case backgroundCtx:
return "aws.BackgroundContext"
}
return "unknown empty Context"
}
var (
backgroundCtx = new(emptyCtx)
import (
"github.com/aws/aws-sdk-go/internal/context"
)
// BackgroundContext returns a context that will never be canceled, has no
@@ -52,5 +18,5 @@ var (
//
// See https://golang.org/pkg/context for more information on Contexts.
func BackgroundContext() Context {
return backgroundCtx
return context.BackgroundCtx
}
+3 -1
View File
@@ -161,7 +161,7 @@ func handleSendError(r *request.Request, err error) {
}
// Catch all request errors, and let the default retrier determine
// if the error is retryable.
r.Error = awserr.New("RequestError", "send request failed", err)
r.Error = awserr.New(request.ErrCodeRequestError, "send request failed", err)
// Override the error with a context canceled error, if that was canceled.
ctx := r.Context()
@@ -225,6 +225,8 @@ var ValidateEndpointHandler = request.NamedHandler{Name: "core.ValidateEndpointH
if r.ClientInfo.SigningRegion == "" && aws.StringValue(r.Config.Region) == "" {
r.Error = aws.ErrMissingRegion
} else if r.ClientInfo.Endpoint == "" {
// Was any endpoint provided by the user, or one was derived by the
// SDK's endpoint resolver?
r.Error = aws.ErrMissingEndpoint
}
}}
@@ -0,0 +1,22 @@
// +build !go1.7
package credentials
import (
"github.com/aws/aws-sdk-go/internal/context"
)
// backgroundContext returns a context that will never be canceled, has no
// values, and no deadline. This context is used by the SDK to provide
// backwards compatibility with non-context API operations and functionality.
//
// Go 1.6 and before:
// This context function is equivalent to context.Background in the Go stdlib.
//
// Go 1.7 and later:
// The context returned will be the value returned by context.Background()
//
// See https://golang.org/pkg/context for more information on Contexts.
func backgroundContext() Context {
return context.BackgroundCtx
}
@@ -0,0 +1,20 @@
// +build go1.7
package credentials
import "context"
// backgroundContext returns a context that will never be canceled, has no
// values, and no deadline. This context is used by the SDK to provide
// backwards compatibility with non-context API operations and functionality.
//
// Go 1.6 and before:
// This context function is equivalent to context.Background in the Go stdlib.
//
// Go 1.7 and later:
// The context returned will be the value returned by context.Background()
//
// See https://golang.org/pkg/context for more information on Contexts.
func backgroundContext() Context {
return context.Background()
}
+39
View File
@@ -0,0 +1,39 @@
// +build !go1.9
package credentials
import "time"
// Context is an copy of the Go v1.7 stdlib's context.Context interface.
// It is represented as a SDK interface to enable you to use the "WithContext"
// API methods with Go v1.6 and a Context type such as golang.org/x/net/context.
//
// This type, aws.Context, and context.Context are equivalent.
//
// See https://golang.org/pkg/context on how to use contexts.
type Context interface {
// Deadline returns the time when work done on behalf of this context
// should be canceled. Deadline returns ok==false when no deadline is
// set. Successive calls to Deadline return the same results.
Deadline() (deadline time.Time, ok bool)
// Done returns a channel that's closed when work done on behalf of this
// context should be canceled. Done may return nil if this context can
// never be canceled. Successive calls to Done return the same value.
Done() <-chan struct{}
// Err returns a non-nil error value after Done is closed. Err returns
// Canceled if the context was canceled or DeadlineExceeded if the
// context's deadline passed. No other values for Err are defined.
// After Done is closed, successive calls to Err return the same value.
Err() error
// Value returns the value associated with this context for key, or nil
// if no value is associated with key. Successive calls to Value with
// the same key returns the same result.
//
// Use context values only for request-scoped data that transits
// processes and API boundaries, not for passing optional parameters to
// functions.
Value(key interface{}) interface{}
}
+13
View File
@@ -0,0 +1,13 @@
// +build go1.9
package credentials
import "context"
// Context is an alias of the Go stdlib's context.Context interface.
// It can be used within the SDK's API operation "WithContext" methods.
//
// This type, aws.Context, and context.Context are equivalent.
//
// See https://golang.org/pkg/context on how to use contexts.
type Context = context.Context
+87 -47
View File
@@ -50,10 +50,11 @@ package credentials
import (
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/internal/sync/singleflight"
)
// AnonymousCredentials is an empty Credential object that can be used as
@@ -106,6 +107,13 @@ type Provider interface {
IsExpired() bool
}
// ProviderWithContext is a Provider that can retrieve credentials with a Context
type ProviderWithContext interface {
Provider
RetrieveWithContext(Context) (Value, error)
}
// An Expirer is an interface that Providers can implement to expose the expiration
// time, if known. If the Provider cannot accurately provide this info,
// it should not implement this interface.
@@ -197,20 +205,68 @@ func (e *Expiry) ExpiresAt() time.Time {
// first instance of the credentials Value. All calls to Get() after that
// will return the cached credentials Value until IsExpired() returns true.
type Credentials struct {
creds Value
forceRefresh bool
m sync.RWMutex
creds atomic.Value
sf singleflight.Group
provider Provider
}
// NewCredentials returns a pointer to a new Credentials with the provider set.
func NewCredentials(provider Provider) *Credentials {
return &Credentials{
provider: provider,
forceRefresh: true,
c := &Credentials{
provider: provider,
}
c.creds.Store(Value{})
return c
}
// GetWithContext returns the credentials value, or error if the credentials
// Value failed to be retrieved. Will return early if the passed in context is
// canceled.
//
// Will return the cached credentials Value if it has not expired. If the
// credentials Value has expired the Provider's Retrieve() will be called
// to refresh the credentials.
//
// If Credentials.Expire() was called the credentials Value will be force
// expired, and the next call to Get() will cause them to be refreshed.
//
// Passed in Context is equivalent to aws.Context, and context.Context.
func (c *Credentials) GetWithContext(ctx Context) (Value, error) {
if curCreds := c.creds.Load(); !c.isExpired(curCreds) {
return curCreds.(Value), nil
}
// Cannot pass context down to the actual retrieve, because the first
// context would cancel the whole group when there is not direct
// association of items in the group.
resCh := c.sf.DoChan("", func() (interface{}, error) {
return c.singleRetrieve(&suppressedContext{ctx})
})
select {
case res := <-resCh:
return res.Val.(Value), res.Err
case <-ctx.Done():
return Value{}, awserr.New("RequestCanceled",
"request context canceled", ctx.Err())
}
}
func (c *Credentials) singleRetrieve(ctx Context) (creds interface{}, err error) {
if curCreds := c.creds.Load(); !c.isExpired(curCreds) {
return curCreds.(Value), nil
}
if p, ok := c.provider.(ProviderWithContext); ok {
creds, err = p.RetrieveWithContext(ctx)
} else {
creds, err = c.provider.Retrieve()
}
if err == nil {
c.creds.Store(creds)
}
return creds, err
}
// Get returns the credentials value, or error if the credentials Value failed
@@ -223,30 +279,7 @@ func NewCredentials(provider Provider) *Credentials {
// If Credentials.Expire() was called the credentials Value will be force
// expired, and the next call to Get() will cause them to be refreshed.
func (c *Credentials) Get() (Value, error) {
// Check the cached credentials first with just the read lock.
c.m.RLock()
if !c.isExpired() {
creds := c.creds
c.m.RUnlock()
return creds, nil
}
c.m.RUnlock()
// Credentials are expired need to retrieve the credentials taking the full
// lock.
c.m.Lock()
defer c.m.Unlock()
if c.isExpired() {
creds, err := c.provider.Retrieve()
if err != nil {
return Value{}, err
}
c.creds = creds
c.forceRefresh = false
}
return c.creds, nil
return c.GetWithContext(backgroundContext())
}
// Expire expires the credentials and forces them to be retrieved on the
@@ -255,10 +288,7 @@ func (c *Credentials) Get() (Value, error) {
// This will override the Provider's expired state, and force Credentials
// to call the Provider's Retrieve().
func (c *Credentials) Expire() {
c.m.Lock()
defer c.m.Unlock()
c.forceRefresh = true
c.creds.Store(Value{})
}
// IsExpired returns if the credentials are no longer valid, and need
@@ -267,33 +297,43 @@ func (c *Credentials) Expire() {
// If the Credentials were forced to be expired with Expire() this will
// reflect that override.
func (c *Credentials) IsExpired() bool {
c.m.RLock()
defer c.m.RUnlock()
return c.isExpired()
return c.isExpired(c.creds.Load())
}
// isExpired helper method wrapping the definition of expired credentials.
func (c *Credentials) isExpired() bool {
return c.forceRefresh || c.provider.IsExpired()
func (c *Credentials) isExpired(creds interface{}) bool {
return creds == nil || creds.(Value) == Value{} || c.provider.IsExpired()
}
// ExpiresAt provides access to the functionality of the Expirer interface of
// the underlying Provider, if it supports that interface. Otherwise, it returns
// an error.
func (c *Credentials) ExpiresAt() (time.Time, error) {
c.m.RLock()
defer c.m.RUnlock()
expirer, ok := c.provider.(Expirer)
if !ok {
return time.Time{}, awserr.New("ProviderNotExpirer",
fmt.Sprintf("provider %s does not support ExpiresAt()", c.creds.ProviderName),
fmt.Sprintf("provider %s does not support ExpiresAt()", c.creds.Load().(Value).ProviderName),
nil)
}
if c.forceRefresh {
if c.creds.Load().(Value) == (Value{}) {
// set expiration time to the distant past
return time.Time{}, nil
}
return expirer.ExpiresAt(), nil
}
type suppressedContext struct {
Context
}
func (s *suppressedContext) Deadline() (deadline time.Time, ok bool) {
return time.Time{}, false
}
func (s *suppressedContext) Done() <-chan struct{} {
return nil
}
func (s *suppressedContext) Err() error {
return nil
}
@@ -7,6 +7,7 @@ import (
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/client"
"github.com/aws/aws-sdk-go/aws/credentials"
@@ -87,7 +88,14 @@ func NewCredentialsWithClient(client *ec2metadata.EC2Metadata, options ...func(*
// Error will be returned if the request fails, or unable to extract
// the desired credentials.
func (m *EC2RoleProvider) Retrieve() (credentials.Value, error) {
credsList, err := requestCredList(m.Client)
return m.RetrieveWithContext(aws.BackgroundContext())
}
// RetrieveWithContext retrieves credentials from the EC2 service.
// Error will be returned if the request fails, or unable to extract
// the desired credentials.
func (m *EC2RoleProvider) RetrieveWithContext(ctx credentials.Context) (credentials.Value, error) {
credsList, err := requestCredList(ctx, m.Client)
if err != nil {
return credentials.Value{ProviderName: ProviderName}, err
}
@@ -97,7 +105,7 @@ func (m *EC2RoleProvider) Retrieve() (credentials.Value, error) {
}
credsName := credsList[0]
roleCreds, err := requestCred(m.Client, credsName)
roleCreds, err := requestCred(ctx, m.Client, credsName)
if err != nil {
return credentials.Value{ProviderName: ProviderName}, err
}
@@ -130,8 +138,8 @@ const iamSecurityCredsPath = "iam/security-credentials/"
// requestCredList requests a list of credentials from the EC2 service.
// If there are no credentials, or there is an error making or receiving the request
func requestCredList(client *ec2metadata.EC2Metadata) ([]string, error) {
resp, err := client.GetMetadata(iamSecurityCredsPath)
func requestCredList(ctx aws.Context, client *ec2metadata.EC2Metadata) ([]string, error) {
resp, err := client.GetMetadataWithContext(ctx, iamSecurityCredsPath)
if err != nil {
return nil, awserr.New("EC2RoleRequestError", "no EC2 instance role found", err)
}
@@ -154,8 +162,8 @@ func requestCredList(client *ec2metadata.EC2Metadata) ([]string, error) {
//
// If the credentials cannot be found, or there is an error reading the response
// and error will be returned.
func requestCred(client *ec2metadata.EC2Metadata, credsName string) (ec2RoleCredRespBody, error) {
resp, err := client.GetMetadata(sdkuri.PathJoin(iamSecurityCredsPath, credsName))
func requestCred(ctx aws.Context, client *ec2metadata.EC2Metadata, credsName string) (ec2RoleCredRespBody, error) {
resp, err := client.GetMetadataWithContext(ctx, sdkuri.PathJoin(iamSecurityCredsPath, credsName))
if err != nil {
return ec2RoleCredRespBody{},
awserr.New("EC2RoleRequestError",
@@ -116,7 +116,13 @@ func (p *Provider) IsExpired() bool {
// Retrieve will attempt to request the credentials from the endpoint the Provider
// was configured for. And error will be returned if the retrieval fails.
func (p *Provider) Retrieve() (credentials.Value, error) {
resp, err := p.getCredentials()
return p.RetrieveWithContext(aws.BackgroundContext())
}
// RetrieveWithContext will attempt to request the credentials from the endpoint the Provider
// was configured for. And error will be returned if the retrieval fails.
func (p *Provider) RetrieveWithContext(ctx credentials.Context) (credentials.Value, error) {
resp, err := p.getCredentials(ctx)
if err != nil {
return credentials.Value{ProviderName: ProviderName},
awserr.New("CredentialsEndpointError", "failed to load credentials", err)
@@ -148,7 +154,7 @@ type errorOutput struct {
Message string `json:"message"`
}
func (p *Provider) getCredentials() (*getCredentialsOutput, error) {
func (p *Provider) getCredentials(ctx aws.Context) (*getCredentialsOutput, error) {
op := &request.Operation{
Name: "GetCredentials",
HTTPMethod: "GET",
@@ -156,6 +162,7 @@ func (p *Provider) getCredentials() (*getCredentialsOutput, error) {
out := &getCredentialsOutput{}
req := p.Client.NewRequest(op, nil, out)
req.SetContext(ctx)
req.HTTPRequest.Header.Set("Accept", "application/json")
if authToken := p.AuthorizationToken; len(authToken) != 0 {
req.HTTPRequest.Header.Set("Authorization", authToken)
+2 -1
View File
@@ -90,6 +90,7 @@ import (
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/internal/sdkio"
)
const (
@@ -142,7 +143,7 @@ const (
// DefaultBufSize limits buffer size from growing to an enormous
// amount due to a faulty process.
DefaultBufSize = 1024
DefaultBufSize = int(8 * sdkio.KibiByte)
// DefaultTimeout default limit on time a process can run.
DefaultTimeout = time.Duration(1) * time.Minute
@@ -17,8 +17,9 @@ var (
ErrSharedCredentialsHomeNotFound = awserr.New("UserHomeNotFound", "user home directory not found.", nil)
)
// A SharedCredentialsProvider retrieves credentials from the current user's home
// directory, and keeps track if those credentials are expired.
// A SharedCredentialsProvider retrieves access key pair (access key ID,
// secret access key, and session token if present) credentials from the current
// user's home directory, and keeps track if those credentials are expired.
//
// Profile ini file example: $HOME/.aws/credentials
type SharedCredentialsProvider struct {
+3 -1
View File
@@ -19,7 +19,9 @@ type StaticProvider struct {
}
// NewStaticCredentials returns a pointer to a new Credentials object
// wrapping a static credentials value provider.
// wrapping a static credentials value provider. Token is only required
// for temporary security credentials retrieved via STS, otherwise an empty
// string can be passed for this parameter.
func NewStaticCredentials(id, secret, token string) *Credentials {
return NewCredentials(&StaticProvider{Value: Value{
AccessKeyID: id,
@@ -87,6 +87,7 @@ import (
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/client"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/internal/sdkrand"
"github.com/aws/aws-sdk-go/service/sts"
)
@@ -118,6 +119,10 @@ type AssumeRoler interface {
AssumeRole(input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error)
}
type assumeRolerWithContext interface {
AssumeRoleWithContext(aws.Context, *sts.AssumeRoleInput, ...request.Option) (*sts.AssumeRoleOutput, error)
}
// DefaultDuration is the default amount of time in minutes that the credentials
// will be valid for.
var DefaultDuration = time.Duration(15) * time.Minute
@@ -144,6 +149,13 @@ type AssumeRoleProvider struct {
// Session name, if you wish to reuse the credentials elsewhere.
RoleSessionName string
// Optional, you can pass tag key-value pairs to your session. These tags are called session tags.
Tags []*sts.Tag
// A list of keys for session tags that you want to set as transitive.
// If you set a tag key as transitive, the corresponding key and value passes to subsequent sessions in a role chain.
TransitiveTagKeys []*string
// Expiry duration of the STS credentials. Defaults to 15 minutes if not set.
Duration time.Duration
@@ -157,6 +169,29 @@ type AssumeRoleProvider struct {
// size.
Policy *string
// The ARNs of IAM managed policies you want to use as managed session policies.
// The policies must exist in the same account as the role.
//
// This parameter is optional. You can provide up to 10 managed policy ARNs.
// However, the plain text that you use for both inline and managed session
// policies can't exceed 2,048 characters.
//
// An AWS conversion compresses the passed session policies and session tags
// into a packed binary format that has a separate limit. Your request can fail
// for this limit even if your plain text meets the other requirements. The
// PackedPolicySize response element indicates by percentage how close the policies
// and tags for your request are to the upper size limit.
//
// Passing policies to this operation returns new temporary credentials. The
// resulting session's permissions are the intersection of the role's identity-based
// policy and the session policies. You can use the role's temporary credentials
// in subsequent AWS API calls to access resources in the account that owns
// the role. You cannot use session policies to grant more permissions than
// those allowed by the identity-based policy of the role that is being assumed.
// For more information, see Session Policies (https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html#policies_session)
// in the IAM User Guide.
PolicyArns []*sts.PolicyDescriptorType
// The identification number of the MFA device that is associated with the user
// who is making the AssumeRole call. Specify this value if the trust policy
// of the role being assumed includes a condition that requires MFA authentication.
@@ -258,6 +293,11 @@ func NewCredentialsWithClient(svc AssumeRoler, roleARN string, options ...func(*
// Retrieve generates a new set of temporary credentials using STS.
func (p *AssumeRoleProvider) Retrieve() (credentials.Value, error) {
return p.RetrieveWithContext(aws.BackgroundContext())
}
// RetrieveWithContext generates a new set of temporary credentials using STS.
func (p *AssumeRoleProvider) RetrieveWithContext(ctx credentials.Context) (credentials.Value, error) {
// Apply defaults where parameters are not set.
if p.RoleSessionName == "" {
// Try to work out a role name that will hopefully end up unique.
@@ -269,10 +309,13 @@ func (p *AssumeRoleProvider) Retrieve() (credentials.Value, error) {
}
jitter := time.Duration(sdkrand.SeededRand.Float64() * p.MaxJitterFrac * float64(p.Duration))
input := &sts.AssumeRoleInput{
DurationSeconds: aws.Int64(int64((p.Duration - jitter) / time.Second)),
RoleArn: aws.String(p.RoleARN),
RoleSessionName: aws.String(p.RoleSessionName),
ExternalId: p.ExternalID,
DurationSeconds: aws.Int64(int64((p.Duration - jitter) / time.Second)),
RoleArn: aws.String(p.RoleARN),
RoleSessionName: aws.String(p.RoleSessionName),
ExternalId: p.ExternalID,
Tags: p.Tags,
PolicyArns: p.PolicyArns,
TransitiveTagKeys: p.TransitiveTagKeys,
}
if p.Policy != nil {
input.Policy = p.Policy
@@ -295,7 +338,15 @@ func (p *AssumeRoleProvider) Retrieve() (credentials.Value, error) {
}
}
roleOutput, err := p.Client.AssumeRole(input)
var roleOutput *sts.AssumeRoleOutput
var err error
if c, ok := p.Client.(assumeRolerWithContext); ok {
roleOutput, err = c.AssumeRoleWithContext(ctx, input)
} else {
roleOutput, err = p.Client.AssumeRole(input)
}
if err != nil {
return credentials.Value{ProviderName: ProviderName}, err
}
@@ -28,15 +28,46 @@ const (
// compare test values.
var now = time.Now
// TokenFetcher shuold return WebIdentity token bytes or an error
type TokenFetcher interface {
FetchToken(credentials.Context) ([]byte, error)
}
// FetchTokenPath is a path to a WebIdentity token file
type FetchTokenPath string
// FetchToken returns a token by reading from the filesystem
func (f FetchTokenPath) FetchToken(ctx credentials.Context) ([]byte, error) {
data, err := ioutil.ReadFile(string(f))
if err != nil {
errMsg := fmt.Sprintf("unable to read file at %s", f)
return nil, awserr.New(ErrCodeWebIdentity, errMsg, err)
}
return data, nil
}
// WebIdentityRoleProvider is used to retrieve credentials using
// an OIDC token.
type WebIdentityRoleProvider struct {
credentials.Expiry
PolicyArns []*sts.PolicyDescriptorType
client stsiface.STSAPI
// Duration the STS credentials will be valid for. Truncated to seconds.
// If unset, the assumed role will use AssumeRoleWithWebIdentity's default
// expiry duration. See
// https://docs.aws.amazon.com/sdk-for-go/api/service/sts/#STS.AssumeRoleWithWebIdentity
// for more information.
Duration time.Duration
// The amount of time the credentials will be refreshed before they expire.
// This is useful refresh credentials before they expire to reduce risk of
// using credentials as they expire. If unset, will default to no expiry
// window.
ExpiryWindow time.Duration
tokenFilePath string
client stsiface.STSAPI
tokenFetcher TokenFetcher
roleARN string
roleSessionName string
}
@@ -52,9 +83,15 @@ func NewWebIdentityCredentials(c client.ConfigProvider, roleARN, roleSessionName
// NewWebIdentityRoleProvider will return a new WebIdentityRoleProvider with the
// provided stsiface.STSAPI
func NewWebIdentityRoleProvider(svc stsiface.STSAPI, roleARN, roleSessionName, path string) *WebIdentityRoleProvider {
return NewWebIdentityRoleProviderWithToken(svc, roleARN, roleSessionName, FetchTokenPath(path))
}
// NewWebIdentityRoleProviderWithToken will return a new WebIdentityRoleProvider with the
// provided stsiface.STSAPI and a TokenFetcher
func NewWebIdentityRoleProviderWithToken(svc stsiface.STSAPI, roleARN, roleSessionName string, tokenFetcher TokenFetcher) *WebIdentityRoleProvider {
return &WebIdentityRoleProvider{
client: svc,
tokenFilePath: path,
tokenFetcher: tokenFetcher,
roleARN: roleARN,
roleSessionName: roleSessionName,
}
@@ -64,10 +101,16 @@ func NewWebIdentityRoleProvider(svc stsiface.STSAPI, roleARN, roleSessionName, p
// 'WebIdentityTokenFilePath' specified destination and if that is empty an
// error will be returned.
func (p *WebIdentityRoleProvider) Retrieve() (credentials.Value, error) {
b, err := ioutil.ReadFile(p.tokenFilePath)
return p.RetrieveWithContext(aws.BackgroundContext())
}
// RetrieveWithContext will attempt to assume a role from a token which is located at
// 'WebIdentityTokenFilePath' specified destination and if that is empty an
// error will be returned.
func (p *WebIdentityRoleProvider) RetrieveWithContext(ctx credentials.Context) (credentials.Value, error) {
b, err := p.tokenFetcher.FetchToken(ctx)
if err != nil {
errMsg := fmt.Sprintf("unable to read file at %s", p.tokenFilePath)
return credentials.Value{}, awserr.New(ErrCodeWebIdentity, errMsg, err)
return credentials.Value{}, awserr.New(ErrCodeWebIdentity, "failed fetching WebIdentity token: ", err)
}
sessionName := p.roleSessionName
@@ -76,11 +119,22 @@ func (p *WebIdentityRoleProvider) Retrieve() (credentials.Value, error) {
// uses unix time in nanoseconds to uniquely identify sessions.
sessionName = strconv.FormatInt(now().UnixNano(), 10)
}
var duration *int64
if p.Duration != 0 {
duration = aws.Int64(int64(p.Duration / time.Second))
}
req, resp := p.client.AssumeRoleWithWebIdentityRequest(&sts.AssumeRoleWithWebIdentityInput{
PolicyArns: p.PolicyArns,
RoleArn: &p.roleARN,
RoleSessionName: &sessionName,
WebIdentityToken: aws.String(string(b)),
DurationSeconds: duration,
})
req.SetContext(ctx)
// InvalidIdentityToken error is a temporary error that can occur
// when assuming an Role with a JWT web identity token.
req.RetryErrorCodes = append(req.RetryErrorCodes, sts.ErrCodeInvalidIdentityTokenException)
+1 -2
View File
@@ -66,7 +66,6 @@ func (rep *Reporter) sendAPICallAttemptMetric(r *request.Request) {
XAmzRequestID: aws.String(r.RequestID),
AttemptCount: aws.Int(r.RetryCount + 1),
AttemptLatency: aws.Int(int(now.Sub(r.AttemptTime).Nanoseconds() / int64(time.Millisecond))),
AccessKey: aws.String(creds.AccessKeyID),
}
@@ -90,7 +89,7 @@ func getMetricException(err awserr.Error) metricException {
code := err.Code()
switch code {
case "RequestError",
case request.ErrCodeRequestError,
request.ErrCodeSerialization,
request.CanceledErrorCode:
return sdkException{
+104 -24
View File
@@ -4,28 +4,73 @@ import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/internal/sdkuri"
)
// getToken uses the duration to return a token for EC2 metadata service,
// or an error if the request failed.
func (c *EC2Metadata) getToken(ctx aws.Context, duration time.Duration) (tokenOutput, error) {
op := &request.Operation{
Name: "GetToken",
HTTPMethod: "PUT",
HTTPPath: "/latest/api/token",
}
var output tokenOutput
req := c.NewRequest(op, nil, &output)
req.SetContext(ctx)
// remove the fetch token handler from the request handlers to avoid infinite recursion
req.Handlers.Sign.RemoveByName(fetchTokenHandlerName)
// Swap the unmarshalMetadataHandler with unmarshalTokenHandler on this request.
req.Handlers.Unmarshal.Swap(unmarshalMetadataHandlerName, unmarshalTokenHandler)
ttl := strconv.FormatInt(int64(duration/time.Second), 10)
req.HTTPRequest.Header.Set(ttlHeader, ttl)
err := req.Send()
// Errors with bad request status should be returned.
if err != nil {
err = awserr.NewRequestFailure(
awserr.New(req.HTTPResponse.Status, http.StatusText(req.HTTPResponse.StatusCode), err),
req.HTTPResponse.StatusCode, req.RequestID)
}
return output, err
}
// GetMetadata uses the path provided to request information from the EC2
// instance metdata service. The content will be returned as a string, or
// instance metadata service. The content will be returned as a string, or
// error if the request failed.
func (c *EC2Metadata) GetMetadata(p string) (string, error) {
return c.GetMetadataWithContext(aws.BackgroundContext(), p)
}
// GetMetadataWithContext uses the path provided to request information from the EC2
// instance metadata service. The content will be returned as a string, or
// error if the request failed.
func (c *EC2Metadata) GetMetadataWithContext(ctx aws.Context, p string) (string, error) {
op := &request.Operation{
Name: "GetMetadata",
HTTPMethod: "GET",
HTTPPath: sdkuri.PathJoin("/meta-data", p),
HTTPPath: sdkuri.PathJoin("/latest/meta-data", p),
}
output := &metadataOutput{}
req := c.NewRequest(op, nil, output)
err := req.Send()
req := c.NewRequest(op, nil, output)
req.SetContext(ctx)
err := req.Send()
return output.Content, err
}
@@ -33,21 +78,24 @@ func (c *EC2Metadata) GetMetadata(p string) (string, error) {
// there is no user-data setup for the EC2 instance a "NotFoundError" error
// code will be returned.
func (c *EC2Metadata) GetUserData() (string, error) {
return c.GetUserDataWithContext(aws.BackgroundContext())
}
// GetUserDataWithContext returns the userdata that was configured for the service. If
// there is no user-data setup for the EC2 instance a "NotFoundError" error
// code will be returned.
func (c *EC2Metadata) GetUserDataWithContext(ctx aws.Context) (string, error) {
op := &request.Operation{
Name: "GetUserData",
HTTPMethod: "GET",
HTTPPath: "/user-data",
HTTPPath: "/latest/user-data",
}
output := &metadataOutput{}
req := c.NewRequest(op, nil, output)
req.Handlers.UnmarshalError.PushBack(func(r *request.Request) {
if r.HTTPResponse.StatusCode == http.StatusNotFound {
r.Error = awserr.New("NotFoundError", "user-data not found", r.Error)
}
})
err := req.Send()
req.SetContext(ctx)
err := req.Send()
return output.Content, err
}
@@ -55,16 +103,24 @@ func (c *EC2Metadata) GetUserData() (string, error) {
// instance metadata service for dynamic data. The content will be returned
// as a string, or error if the request failed.
func (c *EC2Metadata) GetDynamicData(p string) (string, error) {
return c.GetDynamicDataWithContext(aws.BackgroundContext(), p)
}
// GetDynamicDataWithContext uses the path provided to request information from the EC2
// instance metadata service for dynamic data. The content will be returned
// as a string, or error if the request failed.
func (c *EC2Metadata) GetDynamicDataWithContext(ctx aws.Context, p string) (string, error) {
op := &request.Operation{
Name: "GetDynamicData",
HTTPMethod: "GET",
HTTPPath: sdkuri.PathJoin("/dynamic", p),
HTTPPath: sdkuri.PathJoin("/latest/dynamic", p),
}
output := &metadataOutput{}
req := c.NewRequest(op, nil, output)
err := req.Send()
req.SetContext(ctx)
err := req.Send()
return output.Content, err
}
@@ -72,7 +128,14 @@ func (c *EC2Metadata) GetDynamicData(p string) (string, error) {
// instance. Error is returned if the request fails or is unable to parse
// the response.
func (c *EC2Metadata) GetInstanceIdentityDocument() (EC2InstanceIdentityDocument, error) {
resp, err := c.GetDynamicData("instance-identity/document")
return c.GetInstanceIdentityDocumentWithContext(aws.BackgroundContext())
}
// GetInstanceIdentityDocumentWithContext retrieves an identity document describing an
// instance. Error is returned if the request fails or is unable to parse
// the response.
func (c *EC2Metadata) GetInstanceIdentityDocumentWithContext(ctx aws.Context) (EC2InstanceIdentityDocument, error) {
resp, err := c.GetDynamicDataWithContext(ctx, "instance-identity/document")
if err != nil {
return EC2InstanceIdentityDocument{},
awserr.New("EC2MetadataRequestError",
@@ -91,7 +154,12 @@ func (c *EC2Metadata) GetInstanceIdentityDocument() (EC2InstanceIdentityDocument
// IAMInfo retrieves IAM info from the metadata API
func (c *EC2Metadata) IAMInfo() (EC2IAMInfo, error) {
resp, err := c.GetMetadata("iam/info")
return c.IAMInfoWithContext(aws.BackgroundContext())
}
// IAMInfoWithContext retrieves IAM info from the metadata API
func (c *EC2Metadata) IAMInfoWithContext(ctx aws.Context) (EC2IAMInfo, error) {
resp, err := c.GetMetadataWithContext(ctx, "iam/info")
if err != nil {
return EC2IAMInfo{},
awserr.New("EC2MetadataRequestError",
@@ -116,24 +184,36 @@ func (c *EC2Metadata) IAMInfo() (EC2IAMInfo, error) {
// Region returns the region the instance is running in.
func (c *EC2Metadata) Region() (string, error) {
resp, err := c.GetMetadata("placement/availability-zone")
return c.RegionWithContext(aws.BackgroundContext())
}
// RegionWithContext returns the region the instance is running in.
func (c *EC2Metadata) RegionWithContext(ctx aws.Context) (string, error) {
ec2InstanceIdentityDocument, err := c.GetInstanceIdentityDocumentWithContext(ctx)
if err != nil {
return "", err
}
if len(resp) == 0 {
return "", awserr.New("EC2MetadataError", "invalid Region response", nil)
// extract region from the ec2InstanceIdentityDocument
region := ec2InstanceIdentityDocument.Region
if len(region) == 0 {
return "", awserr.New("EC2MetadataError", "invalid region received for ec2metadata instance", nil)
}
// returns region without the suffix. Eg: us-west-2a becomes us-west-2
return resp[:len(resp)-1], nil
// returns region
return region, nil
}
// Available returns if the application has access to the EC2 Metadata service.
// Can be used to determine if application is running within an EC2 Instance and
// the metadata service is available.
func (c *EC2Metadata) Available() bool {
if _, err := c.GetMetadata("instance-id"); err != nil {
return c.AvailableWithContext(aws.BackgroundContext())
}
// AvailableWithContext returns if the application has access to the EC2 Metadata service.
// Can be used to determine if application is running within an EC2 Instance and
// the metadata service is available.
func (c *EC2Metadata) AvailableWithContext(ctx aws.Context) bool {
if _, err := c.GetMetadataWithContext(ctx, "instance-id"); err != nil {
return false
}
+114 -21
View File
@@ -5,6 +5,10 @@
// variable "AWS_EC2_METADATA_DISABLED=true". This environment variable set to
// true instructs the SDK to disable the EC2 Metadata client. The client cannot
// be used while the environment variable is set to true, (case insensitive).
//
// The endpoint of the EC2 IMDS client can be configured via the environment
// variable, AWS_EC2_METADATA_SERVICE_ENDPOINT when creating the client with a
// Session. See aws/session#Options.EC2IMDSEndpoint for more details.
package ec2metadata
import (
@@ -12,7 +16,9 @@ import (
"errors"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
@@ -24,9 +30,25 @@ import (
"github.com/aws/aws-sdk-go/aws/request"
)
// ServiceName is the name of the service.
const ServiceName = "ec2metadata"
const disableServiceEnvVar = "AWS_EC2_METADATA_DISABLED"
const (
// ServiceName is the name of the service.
ServiceName = "ec2metadata"
disableServiceEnvVar = "AWS_EC2_METADATA_DISABLED"
// Headers for Token and TTL
ttlHeader = "x-aws-ec2-metadata-token-ttl-seconds"
tokenHeader = "x-aws-ec2-metadata-token"
// Named Handler constants
fetchTokenHandlerName = "FetchTokenHandler"
unmarshalMetadataHandlerName = "unmarshalMetadataHandler"
unmarshalTokenHandlerName = "unmarshalTokenHandler"
enableTokenProviderHandlerName = "enableTokenProviderHandler"
// TTL constants
defaultTTL = 21600 * time.Second
ttlExpirationWindow = 30 * time.Second
)
// A EC2Metadata is an EC2 Metadata service Client.
type EC2Metadata struct {
@@ -52,6 +74,9 @@ func New(p client.ConfigProvider, cfgs ...*aws.Config) *EC2Metadata {
// a client when not using a session. Generally using just New with a session
// is preferred.
//
// Will remove the URL path from the endpoint provided to ensure the EC2 IMDS
// client is able to communicate with the EC2 IMDS API.
//
// If an unmodified HTTP client is provided from the stdlib default, or no client
// the EC2RoleProvider's EC2Metadata HTTP client's timeout will be shortened.
// To disable this set Config.EC2MetadataDisableTimeoutOverride to false. Enabled by default.
@@ -63,8 +88,19 @@ func NewClient(cfg aws.Config, handlers request.Handlers, endpoint, signingRegio
// use a shorter timeout than default because the metadata
// service is local if it is running, and to fail faster
// if not running on an ec2 instance.
Timeout: 5 * time.Second,
Timeout: 1 * time.Second,
}
// max number of retries on the client operation
cfg.MaxRetries = aws.Int(2)
}
if u, err := url.Parse(endpoint); err == nil {
// Remove path from the endpoint since it will be added by requests.
// This is an artifact of the SDK adding `/latest` to the endpoint for
// EC2 IMDS, but this is now moved to the operation definition.
u.Path = ""
u.RawPath = ""
endpoint = u.String()
}
svc := &EC2Metadata{
@@ -80,13 +116,27 @@ func NewClient(cfg aws.Config, handlers request.Handlers, endpoint, signingRegio
),
}
svc.Handlers.Unmarshal.PushBack(unmarshalHandler)
// token provider instance
tp := newTokenProvider(svc, defaultTTL)
// NamedHandler for fetching token
svc.Handlers.Sign.PushBackNamed(request.NamedHandler{
Name: fetchTokenHandlerName,
Fn: tp.fetchTokenHandler,
})
// NamedHandler for enabling token provider
svc.Handlers.Complete.PushBackNamed(request.NamedHandler{
Name: enableTokenProviderHandlerName,
Fn: tp.enableTokenProviderHandler,
})
svc.Handlers.Unmarshal.PushBackNamed(unmarshalHandler)
svc.Handlers.UnmarshalError.PushBack(unmarshalError)
svc.Handlers.Validate.Clear()
svc.Handlers.Validate.PushBack(validateEndpointHandler)
// Disable the EC2 Metadata service if the environment variable is set.
// This shortcirctes the service's functionality to always fail to send
// This short-circuits the service's functionality to always fail to send
// requests.
if strings.ToLower(os.Getenv(disableServiceEnvVar)) == "true" {
svc.Handlers.Send.SwapNamed(request.NamedHandler{
@@ -107,7 +157,6 @@ func NewClient(cfg aws.Config, handlers request.Handlers, endpoint, signingRegio
for _, option := range opts {
option(svc.Client)
}
return svc
}
@@ -119,30 +168,74 @@ type metadataOutput struct {
Content string
}
func unmarshalHandler(r *request.Request) {
defer r.HTTPResponse.Body.Close()
b := &bytes.Buffer{}
if _, err := io.Copy(b, r.HTTPResponse.Body); err != nil {
r.Error = awserr.New(request.ErrCodeSerialization, "unable to unmarshal EC2 metadata response", err)
return
}
type tokenOutput struct {
Token string
TTL time.Duration
}
if data, ok := r.Data.(*metadataOutput); ok {
data.Content = b.String()
}
// unmarshal token handler is used to parse the response of a getToken operation
var unmarshalTokenHandler = request.NamedHandler{
Name: unmarshalTokenHandlerName,
Fn: func(r *request.Request) {
defer r.HTTPResponse.Body.Close()
var b bytes.Buffer
if _, err := io.Copy(&b, r.HTTPResponse.Body); err != nil {
r.Error = awserr.NewRequestFailure(awserr.New(request.ErrCodeSerialization,
"unable to unmarshal EC2 metadata response", err), r.HTTPResponse.StatusCode, r.RequestID)
return
}
v := r.HTTPResponse.Header.Get(ttlHeader)
data, ok := r.Data.(*tokenOutput)
if !ok {
return
}
data.Token = b.String()
// TTL is in seconds
i, err := strconv.ParseInt(v, 10, 64)
if err != nil {
r.Error = awserr.NewRequestFailure(awserr.New(request.ParamFormatErrCode,
"unable to parse EC2 token TTL response", err), r.HTTPResponse.StatusCode, r.RequestID)
return
}
t := time.Duration(i) * time.Second
data.TTL = t
},
}
var unmarshalHandler = request.NamedHandler{
Name: unmarshalMetadataHandlerName,
Fn: func(r *request.Request) {
defer r.HTTPResponse.Body.Close()
var b bytes.Buffer
if _, err := io.Copy(&b, r.HTTPResponse.Body); err != nil {
r.Error = awserr.NewRequestFailure(awserr.New(request.ErrCodeSerialization,
"unable to unmarshal EC2 metadata response", err), r.HTTPResponse.StatusCode, r.RequestID)
return
}
if data, ok := r.Data.(*metadataOutput); ok {
data.Content = b.String()
}
},
}
func unmarshalError(r *request.Request) {
defer r.HTTPResponse.Body.Close()
b := &bytes.Buffer{}
if _, err := io.Copy(b, r.HTTPResponse.Body); err != nil {
r.Error = awserr.New(request.ErrCodeSerialization, "unable to unmarshal EC2 metadata error response", err)
var b bytes.Buffer
if _, err := io.Copy(&b, r.HTTPResponse.Body); err != nil {
r.Error = awserr.NewRequestFailure(
awserr.New(request.ErrCodeSerialization, "unable to unmarshal EC2 metadata error response", err),
r.HTTPResponse.StatusCode, r.RequestID)
return
}
// Response body format is not consistent between metadata endpoints.
// Grab the error message as a string and include that as the source error
r.Error = awserr.New("EC2MetadataError", "failed to make EC2Metadata request", errors.New(b.String()))
r.Error = awserr.NewRequestFailure(awserr.New("EC2MetadataError", "failed to make EC2Metadata request", errors.New(b.String())),
r.HTTPResponse.StatusCode, r.RequestID)
}
func validateEndpointHandler(r *request.Request) {
+92
View File
@@ -0,0 +1,92 @@
package ec2metadata
import (
"net/http"
"sync/atomic"
"time"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/request"
)
// A tokenProvider struct provides access to EC2Metadata client
// and atomic instance of a token, along with configuredTTL for it.
// tokenProvider also provides an atomic flag to disable the
// fetch token operation.
// The disabled member will use 0 as false, and 1 as true.
type tokenProvider struct {
client *EC2Metadata
token atomic.Value
configuredTTL time.Duration
disabled uint32
}
// A ec2Token struct helps use of token in EC2 Metadata service ops
type ec2Token struct {
token string
credentials.Expiry
}
// newTokenProvider provides a pointer to a tokenProvider instance
func newTokenProvider(c *EC2Metadata, duration time.Duration) *tokenProvider {
return &tokenProvider{client: c, configuredTTL: duration}
}
// fetchTokenHandler fetches token for EC2Metadata service client by default.
func (t *tokenProvider) fetchTokenHandler(r *request.Request) {
// short-circuits to insecure data flow if tokenProvider is disabled.
if v := atomic.LoadUint32(&t.disabled); v == 1 {
return
}
if ec2Token, ok := t.token.Load().(ec2Token); ok && !ec2Token.IsExpired() {
r.HTTPRequest.Header.Set(tokenHeader, ec2Token.token)
return
}
output, err := t.client.getToken(r.Context(), t.configuredTTL)
if err != nil {
// change the disabled flag on token provider to true,
// when error is request timeout error.
if requestFailureError, ok := err.(awserr.RequestFailure); ok {
switch requestFailureError.StatusCode() {
case http.StatusForbidden, http.StatusNotFound, http.StatusMethodNotAllowed:
atomic.StoreUint32(&t.disabled, 1)
case http.StatusBadRequest:
r.Error = requestFailureError
}
// Check if request timed out while waiting for response
if e, ok := requestFailureError.OrigErr().(awserr.Error); ok {
if e.Code() == request.ErrCodeRequestError {
atomic.StoreUint32(&t.disabled, 1)
}
}
}
return
}
newToken := ec2Token{
token: output.Token,
}
newToken.SetExpiration(time.Now().Add(output.TTL), ttlExpirationWindow)
t.token.Store(newToken)
// Inject token header to the request.
if ec2Token, ok := t.token.Load().(ec2Token); ok {
r.HTTPRequest.Header.Set(tokenHeader, ec2Token.token)
}
}
// enableTokenProviderHandler enables the token provider
func (t *tokenProvider) enableTokenProviderHandler(r *request.Request) {
// If the error code status is 401, we enable the token provider
if e, ok := r.Error.(awserr.RequestFailure); ok && e != nil &&
e.StatusCode() == http.StatusUnauthorized {
atomic.StoreUint32(&t.disabled, 0)
}
}
+29 -1
View File
@@ -83,6 +83,7 @@ func decodeV3Endpoints(modelDef modelDefinition, opts DecodeModelOptions) (Resol
p := &ps[i]
custAddEC2Metadata(p)
custAddS3DualStack(p)
custRegionalS3(p)
custRmIotDataService(p)
custFixAppAutoscalingChina(p)
custFixAppAutoscalingUsGov(p)
@@ -92,7 +93,7 @@ func decodeV3Endpoints(modelDef modelDefinition, opts DecodeModelOptions) (Resol
}
func custAddS3DualStack(p *partition) {
if p.ID != "aws" {
if !(p.ID == "aws" || p.ID == "aws-cn" || p.ID == "aws-us-gov") {
return
}
@@ -100,6 +101,33 @@ func custAddS3DualStack(p *partition) {
custAddDualstack(p, "s3-control")
}
func custRegionalS3(p *partition) {
if p.ID != "aws" {
return
}
service, ok := p.Services["s3"]
if !ok {
return
}
// If global endpoint already exists no customization needed.
if _, ok := service.Endpoints["aws-global"]; ok {
return
}
service.PartitionEndpoint = "aws-global"
service.Endpoints["us-east-1"] = endpoint{}
service.Endpoints["aws-global"] = endpoint{
Hostname: "s3.amazonaws.com",
CredentialScope: credentialScope{
Region: "us-east-1",
},
}
p.Services["s3"] = service
}
func custAddDualstack(p *partition, svcName string) {
s, ok := p.Services[svcName]
if !ok {
File diff suppressed because it is too large Load Diff
+116 -4
View File
@@ -3,6 +3,7 @@ package endpoints
import (
"fmt"
"regexp"
"strings"
"github.com/aws/aws-sdk-go/aws/awserr"
)
@@ -46,6 +47,108 @@ type Options struct {
//
// This option is ignored if StrictMatching is enabled.
ResolveUnknownService bool
// STS Regional Endpoint flag helps with resolving the STS endpoint
STSRegionalEndpoint STSRegionalEndpoint
// S3 Regional Endpoint flag helps with resolving the S3 endpoint
S3UsEast1RegionalEndpoint S3UsEast1RegionalEndpoint
}
// STSRegionalEndpoint is an enum for the states of the STS Regional Endpoint
// options.
type STSRegionalEndpoint int
func (e STSRegionalEndpoint) String() string {
switch e {
case LegacySTSEndpoint:
return "legacy"
case RegionalSTSEndpoint:
return "regional"
case UnsetSTSEndpoint:
return ""
default:
return "unknown"
}
}
const (
// UnsetSTSEndpoint represents that STS Regional Endpoint flag is not specified.
UnsetSTSEndpoint STSRegionalEndpoint = iota
// LegacySTSEndpoint represents when STS Regional Endpoint flag is specified
// to use legacy endpoints.
LegacySTSEndpoint
// RegionalSTSEndpoint represents when STS Regional Endpoint flag is specified
// to use regional endpoints.
RegionalSTSEndpoint
)
// GetSTSRegionalEndpoint function returns the STSRegionalEndpointFlag based
// on the input string provided in env config or shared config by the user.
//
// `legacy`, `regional` are the only case-insensitive valid strings for
// resolving the STS regional Endpoint flag.
func GetSTSRegionalEndpoint(s string) (STSRegionalEndpoint, error) {
switch {
case strings.EqualFold(s, "legacy"):
return LegacySTSEndpoint, nil
case strings.EqualFold(s, "regional"):
return RegionalSTSEndpoint, nil
default:
return UnsetSTSEndpoint, fmt.Errorf("unable to resolve the value of STSRegionalEndpoint for %v", s)
}
}
// S3UsEast1RegionalEndpoint is an enum for the states of the S3 us-east-1
// Regional Endpoint options.
type S3UsEast1RegionalEndpoint int
func (e S3UsEast1RegionalEndpoint) String() string {
switch e {
case LegacyS3UsEast1Endpoint:
return "legacy"
case RegionalS3UsEast1Endpoint:
return "regional"
case UnsetS3UsEast1Endpoint:
return ""
default:
return "unknown"
}
}
const (
// UnsetS3UsEast1Endpoint represents that S3 Regional Endpoint flag is not
// specified.
UnsetS3UsEast1Endpoint S3UsEast1RegionalEndpoint = iota
// LegacyS3UsEast1Endpoint represents when S3 Regional Endpoint flag is
// specified to use legacy endpoints.
LegacyS3UsEast1Endpoint
// RegionalS3UsEast1Endpoint represents when S3 Regional Endpoint flag is
// specified to use regional endpoints.
RegionalS3UsEast1Endpoint
)
// GetS3UsEast1RegionalEndpoint function returns the S3UsEast1RegionalEndpointFlag based
// on the input string provided in env config or shared config by the user.
//
// `legacy`, `regional` are the only case-insensitive valid strings for
// resolving the S3 regional Endpoint flag.
func GetS3UsEast1RegionalEndpoint(s string) (S3UsEast1RegionalEndpoint, error) {
switch {
case strings.EqualFold(s, "legacy"):
return LegacyS3UsEast1Endpoint, nil
case strings.EqualFold(s, "regional"):
return RegionalS3UsEast1Endpoint, nil
default:
return UnsetS3UsEast1Endpoint,
fmt.Errorf("unable to resolve the value of S3UsEast1RegionalEndpoint for %v", s)
}
}
// Set combines all of the option functions together.
@@ -79,6 +182,12 @@ func ResolveUnknownServiceOption(o *Options) {
o.ResolveUnknownService = true
}
// STSRegionalEndpointOption enables the STS endpoint resolver behavior to resolve
// STS endpoint to their regional endpoint, instead of the global endpoint.
func STSRegionalEndpointOption(o *Options) {
o.STSRegionalEndpoint = RegionalSTSEndpoint
}
// A Resolver provides the interface for functionality to resolve endpoints.
// The build in Partition and DefaultResolver return value satisfy this interface.
type Resolver interface {
@@ -194,7 +303,7 @@ func (p Partition) ID() string { return p.id }
// require the provided service and region to be known by the partition.
// If the endpoint cannot be strictly resolved an error will be returned. This
// mode is useful to ensure the endpoint resolved is valid. Without
// StrictMatching enabled the endpoint returned my look valid but may not work.
// StrictMatching enabled the endpoint returned may look valid but may not work.
// StrictMatching requires the SDK to be updated if you want to take advantage
// of new regions and services expansions.
//
@@ -208,7 +317,7 @@ func (p Partition) EndpointFor(service, region string, opts ...func(*Options)) (
// Regions returns a map of Regions indexed by their ID. This is useful for
// enumerating over the regions in a partition.
func (p Partition) Regions() map[string]Region {
rs := map[string]Region{}
rs := make(map[string]Region, len(p.p.Regions))
for id, r := range p.p.Regions {
rs[id] = Region{
id: id,
@@ -223,7 +332,7 @@ func (p Partition) Regions() map[string]Region {
// Services returns a map of Service indexed by their ID. This is useful for
// enumerating over the services in a partition.
func (p Partition) Services() map[string]Service {
ss := map[string]Service{}
ss := make(map[string]Service, len(p.p.Services))
for id := range p.p.Services {
ss[id] = Service{
id: id,
@@ -310,7 +419,7 @@ func (s Service) Regions() map[string]Region {
// A region is the AWS region the service exists in. Whereas a Endpoint is
// an URL that can be resolved to a instance of a service.
func (s Service) Endpoints() map[string]Endpoint {
es := map[string]Endpoint{}
es := make(map[string]Endpoint, len(s.p.Services[s.id].Endpoints))
for id := range s.p.Services[s.id].Endpoints {
es[id] = Endpoint{
id: id,
@@ -350,6 +459,9 @@ type ResolvedEndpoint struct {
// The endpoint URL
URL string
// The endpoint partition
PartitionID string
// The region that should be used for signing requests.
SigningRegion string
+24
View File
@@ -0,0 +1,24 @@
package endpoints
var legacyGlobalRegions = map[string]map[string]struct{}{
"sts": {
"ap-northeast-1": {},
"ap-south-1": {},
"ap-southeast-1": {},
"ap-southeast-2": {},
"ca-central-1": {},
"eu-central-1": {},
"eu-north-1": {},
"eu-west-1": {},
"eu-west-2": {},
"eu-west-3": {},
"sa-east-1": {},
"us-east-1": {},
"us-east-2": {},
"us-west-1": {},
"us-west-2": {},
},
"s3": {
"us-east-1": {},
},
}
+62 -19
View File
@@ -7,6 +7,8 @@ import (
"strings"
)
var regionValidationRegex = regexp.MustCompile(`^[[:alnum:]]([[:alnum:]\-]*[[:alnum:]])?$`)
type partitions []partition
func (ps partitions) EndpointFor(service, region string, opts ...func(*Options)) (ResolvedEndpoint, error) {
@@ -75,24 +77,56 @@ func (p partition) canResolveEndpoint(service, region string, strictMatch bool)
return p.RegionRegex.MatchString(region)
}
func allowLegacyEmptyRegion(service string) bool {
legacy := map[string]struct{}{
"budgets": {},
"ce": {},
"chime": {},
"cloudfront": {},
"ec2metadata": {},
"iam": {},
"importexport": {},
"organizations": {},
"route53": {},
"sts": {},
"support": {},
"waf": {},
}
_, allowed := legacy[service]
return allowed
}
func (p partition) EndpointFor(service, region string, opts ...func(*Options)) (resolved ResolvedEndpoint, err error) {
var opt Options
opt.Set(opts...)
s, hasService := p.Services[service]
if !(hasService || opt.ResolveUnknownService) {
if len(service) == 0 || !(hasService || opt.ResolveUnknownService) {
// Only return error if the resolver will not fallback to creating
// endpoint based on service endpoint ID passed in.
return resolved, NewUnknownServiceError(p.ID, service, serviceList(p.Services))
}
if len(region) == 0 && allowLegacyEmptyRegion(service) && len(s.PartitionEndpoint) != 0 {
region = s.PartitionEndpoint
}
if (service == "sts" && opt.STSRegionalEndpoint != RegionalSTSEndpoint) ||
(service == "s3" && opt.S3UsEast1RegionalEndpoint != RegionalS3UsEast1Endpoint) {
if _, ok := legacyGlobalRegions[service][region]; ok {
region = "aws-global"
}
}
e, hasEndpoint := s.endpointForRegion(region)
if !hasEndpoint && opt.StrictMatching {
if len(region) == 0 || (!hasEndpoint && opt.StrictMatching) {
return resolved, NewUnknownEndpointError(p.ID, service, region, endpointList(s.Endpoints))
}
defs := []endpoint{p.Defaults, s.Defaults}
return e.resolve(service, region, p.DNSSuffix, defs, opt), nil
return e.resolve(service, p.ID, region, p.DNSSuffix, defs, opt)
}
func serviceList(ss services) []string {
@@ -201,7 +235,7 @@ func getByPriority(s []string, p []string, def string) string {
return s[0]
}
func (e endpoint) resolve(service, region, dnsSuffix string, defs []endpoint, opts Options) ResolvedEndpoint {
func (e endpoint) resolve(service, partitionID, region, dnsSuffix string, defs []endpoint, opts Options) (ResolvedEndpoint, error) {
var merged endpoint
for _, def := range defs {
merged.mergeIn(def)
@@ -209,20 +243,6 @@ func (e endpoint) resolve(service, region, dnsSuffix string, defs []endpoint, op
merged.mergeIn(e)
e = merged
hostname := e.Hostname
// Offset the hostname for dualstack if enabled
if opts.UseDualStack && e.HasDualStack == boxedTrue {
hostname = e.DualStackHostname
}
u := strings.Replace(hostname, "{service}", service, 1)
u = strings.Replace(u, "{region}", region, 1)
u = strings.Replace(u, "{dnsSuffix}", dnsSuffix, 1)
scheme := getEndpointScheme(e.Protocols, opts.DisableSSL)
u = fmt.Sprintf("%s://%s", scheme, u)
signingRegion := e.CredentialScope.Region
if len(signingRegion) == 0 {
signingRegion = region
@@ -235,13 +255,32 @@ func (e endpoint) resolve(service, region, dnsSuffix string, defs []endpoint, op
signingNameDerived = true
}
hostname := e.Hostname
// Offset the hostname for dualstack if enabled
if opts.UseDualStack && e.HasDualStack == boxedTrue {
hostname = e.DualStackHostname
region = signingRegion
}
if !validateInputRegion(region) {
return ResolvedEndpoint{}, fmt.Errorf("invalid region identifier format provided")
}
u := strings.Replace(hostname, "{service}", service, 1)
u = strings.Replace(u, "{region}", region, 1)
u = strings.Replace(u, "{dnsSuffix}", dnsSuffix, 1)
scheme := getEndpointScheme(e.Protocols, opts.DisableSSL)
u = fmt.Sprintf("%s://%s", scheme, u)
return ResolvedEndpoint{
URL: u,
PartitionID: partitionID,
SigningRegion: signingRegion,
SigningName: signingName,
SigningNameDerived: signingNameDerived,
SigningMethod: getByPriority(e.SignatureVersions, signerPriority, defaultSigner),
}
}, nil
}
func getEndpointScheme(protocols []string, disableSSL bool) string {
@@ -306,3 +345,7 @@ const (
boxedFalse
boxedTrue
)
func validateInputRegion(region string) bool {
return regionValidationRegex.MatchString(region)
}
+2 -1
View File
@@ -9,7 +9,8 @@ func isErrConnectionReset(err error) bool {
return false
}
if strings.Contains(err.Error(), "connection reset") ||
if strings.Contains(err.Error(), "use of closed network connection") ||
strings.Contains(err.Error(), "connection reset") ||
strings.Contains(err.Error(), "broken pipe") {
return true
}
+21
View File
@@ -10,6 +10,7 @@ import (
type Handlers struct {
Validate HandlerList
Build HandlerList
BuildStream HandlerList
Sign HandlerList
Send HandlerList
ValidateResponse HandlerList
@@ -28,6 +29,7 @@ func (h *Handlers) Copy() Handlers {
return Handlers{
Validate: h.Validate.copy(),
Build: h.Build.copy(),
BuildStream: h.BuildStream.copy(),
Sign: h.Sign.copy(),
Send: h.Send.copy(),
ValidateResponse: h.ValidateResponse.copy(),
@@ -46,6 +48,7 @@ func (h *Handlers) Copy() Handlers {
func (h *Handlers) Clear() {
h.Validate.Clear()
h.Build.Clear()
h.BuildStream.Clear()
h.Send.Clear()
h.Sign.Clear()
h.Unmarshal.Clear()
@@ -67,6 +70,9 @@ func (h *Handlers) IsEmpty() bool {
if h.Build.Len() != 0 {
return false
}
if h.BuildStream.Len() != 0 {
return false
}
if h.Send.Len() != 0 {
return false
}
@@ -320,3 +326,18 @@ func MakeAddToUserAgentFreeFormHandler(s string) func(*Request) {
AddToUserAgent(r, s)
}
}
// WithSetRequestHeaders updates the operation request's HTTP header to contain
// the header key value pairs provided. If the header key already exists in the
// request's HTTP header set, the existing value(s) will be replaced.
func WithSetRequestHeaders(h map[string]string) Option {
return withRequestHeader(h).SetRequestHeaders
}
type withRequestHeader map[string]string
func (h withRequestHeader) SetRequestHeaders(r *Request) {
for k, v := range h {
r.HTTPRequest.Header[k] = []string{v}
}
}

Some files were not shown because too many files have changed in this diff Show More