Compare commits

..

121 Commits

Author SHA1 Message Date
Adam Chalmers 3de3b2f734 Release 2020.11.0 2020-11-10 09:44:29 -06:00
Adam Chalmers 4698ec8dee TUN-3461: Show all origin services in the UI 2020-11-10 14:25:37 +00:00
Igor Postelnik 8c6181db9f TUN-3524: Don't ignore errors from app-level action handler (#248) 2020-11-10 13:06:49 +00:00
Adam Chalmers 64d3836645 TUN-3522: ingress validate checks that the config file exists 2020-11-09 12:31:50 -06:00
Adam Chalmers 87e2679744 TUN-3516: Better error message when parsing invalid YAML config 2020-11-09 10:35:28 -06:00
cthuang 61c814bd79 TUN-3506: OriginService needs to set request host and scheme for websocket requests 2020-11-05 16:34:02 +00:00
cthuang be9a558867 TUN-3503: Matching ingress rule should not take port into account 2020-11-05 15:36:12 +00:00
cthuang b00bfe8e0b TUN-3505: Response body for status code origin returns EOF on Read 2020-11-05 14:47:46 +00:00
Adam Chalmers bc015995d8 TUN-3484: OriginService that responds with configured HTTP status 2020-11-04 21:28:33 +00:00
Adam Chalmers d01770107e TUN-3492: Refactor OriginService, shrink its interface 2020-11-04 21:28:33 +00:00
cthuang 18c359cb86 TUN-3494: Proceed to create tunnel if at least one edge address can be resolved 2020-10-30 18:18:34 +00:00
Adam Chalmers e933ef9e1a TUN-2640: Users can configure per-origin config. Unify single-rule CLI
flow with multi-rule config file code.
2020-10-30 07:42:20 -05:00
cthuang ea71b78e6d TUN-3478: Increase download timeout to 60s 2020-10-22 10:38:18 +01:00
Igor Postelnik 04fa10ea7b Release 2020.10.2 2020-10-21 13:42:12 -05:00
Dalton 8e0be7cff1 AUTH-3185 fixed indention error 2020-10-21 12:14:39 -05:00
cthuang 3c13080fe1 Release 2020.10.1 2020-10-21 17:09:19 +01:00
Igor Postelnik b6cd54d854 TUN-3459: Make service install on linux use named tunnels 2020-10-21 10:46:29 -05:00
cthuang f0cfad8efa TUN-3476: Fix conversion to string and int slice 2020-10-21 16:03:25 +01:00
Igor Postelnik ed54d150fe Move raw ingress rules to config package 2020-10-20 12:00:34 -05:00
Igor Postelnik ca4887fb19 Split out typed config from legacy command-line switches; refactor ingress commands and fix tests 2020-10-20 10:10:19 -05:00
Igor Postelnik eaf03305bd TUN-3475: Unify config file handling with typed config for new fields 2020-10-20 08:55:30 -05:00
Igor Postelnik 051908aaef TUN-3463: Let users run a named tunnel via config file setting 2020-10-19 12:27:18 +00:00
Adam Chalmers acd03e36e6 TUN-3465: Use Go 1.15.3 2020-10-15 15:55:16 -05:00
Adam Chalmers c96b9e8d8f TUN-3464: Newtype to wrap []ingress.Rule 2020-10-15 12:48:14 -05:00
Adam Chalmers 4a4a1bb6b1 TUN-3441: Multiple-origin routing via ingress rules 2020-10-13 08:55:17 -05:00
Adam Chalmers 0eebc7cef9 TUN-3438: move ingress into own package, read into TunnelConfig 2020-10-12 16:33:22 +00:00
Igor Postelnik 53a1fa46a8 TUN-3452: Fix loading of flags from config file for tunnel run subcommand. This change also cleans up building of tunnel subcommand list, hides deprecated subcommands and improves help. 2020-10-09 12:07:17 -05:00
Adam Chalmers 86a7af3dc4 TUN-3451: Cloudflared tunnel ingress command 2020-10-08 22:06:40 +00:00
Adam Chalmers 407c9550d7 TUN-3440: 'tunnel rule' command to test ingress rules 2020-10-08 22:06:40 +00:00
Adam Chalmers 2319003e10 TUN-3439: 'tunnel validate' command to check ingress rules 2020-10-08 22:06:40 +00:00
cthuang 1e6399c2f0 TUN-3446: Use go 1.15.2 and add a step to build cloudflared in the dev Dockerfile 2020-10-07 21:28:54 +00:00
Adam Chalmers b05d826d22 TUN-3436, TUN-3437: Parse ingress from YAML, ensure last rule catches everything 2020-10-07 16:36:28 +00:00
Dalton d21989dba4 AUTH-3148 fixed cloudflared copy and match all the files in the checksum upload 2020-10-06 11:39:40 -05:00
Igor Postelnik a986355a61 Release 2020.10.0 2020-10-06 09:54:04 -05:00
cthuang 812244d79f TUN-3443: Decode as v4api response on non-200 status 2020-10-06 11:34:41 +00:00
Dalton be7b7c7149 AUTH-2993 cleaned up worker service tests 2020-10-02 13:01:05 -05:00
cthuang 03d7320a44 TUN-3430: Copy flags to configure proxy to run subcommand, print relevant tunnel flags in help 2020-10-01 21:44:27 +00:00
Dalton ba4c8d8849 AUTH-2993 added workers updater logic 2020-10-01 14:41:58 -05:00
cthuang 2c9b7361b7 TUN-3427: Define a struct that only implements RegistrationServer in tunnelpogs 2020-10-01 09:08:32 +01:00
Lee Valentine 8e8513e325 TRAFFIC-448: allow the user to specify the proxy address and port to bind to, falling back to 127.0.0.1 and random port if not specified 2020-09-25 09:54:40 -05:00
Adam Chalmers 607dcff697 Release 2020.9.3 2020-09-24 12:07:27 -05:00
Adam Chalmers 1d1cf6a827 Release 2020.9.2 2020-09-24 19:23:53 +04:00
Adam Chalmers 61f3fab757 TUN-3410: Request the v1 Tunnelstore API 2020-09-24 19:23:53 +04:00
Lee Valentine e2ff7f65fc TRAFFIC-448: build cloudflare for junos and publish to s3 2020-09-24 19:23:53 +04:00
Dalton da0792a321 AUTH-3109 upload the checksum to workers kv on github releases 2020-09-23 18:01:43 +04:00
Bojan Zelic fa061ab54e updater service exit code should be 11 2020-09-22 18:26:56 +04: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
1123 changed files with 157033 additions and 42088 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'
+1 -1
View File
@@ -1,7 +1,7 @@
# use a builder image for building cloudflare
ARG TARGET_GOOS
ARG TARGET_GOARCH
FROM golang:1.13.3 as builder
FROM golang:1.15.3 as builder
ENV GO111MODULE=on \
CGO_ENABLED=0 \
TARGET_GOOS=${TARGET_GOOS} \
+73 -3
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
@@ -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)
+140
View File
@@ -1,3 +1,143 @@
2020.11.0
- 2020-11-04 TUN-3484: OriginService that responds with configured HTTP status
- 2020-11-05 TUN-3505: Response body for status code origin returns EOF on Read
- 2020-11-04 TUN-3503: Matching ingress rule should not take port into account
- 2020-11-05 TUN-3506: OriginService needs to set request host and scheme for websocket requests
- 2020-11-09 TUN-3516: Better error message when parsing invalid YAML config
- 2020-11-09 TUN-3522: ingress validate checks that the config file exists
- 2020-11-09 TUN-3524: Don't ignore errors from app-level action handler (#248)
- 2020-11-09 TUN-3461: Show all origin services in the UI
- 2020-10-30 TUN-3494: Proceed to create tunnel if at least one edge address can be resolved
- 2020-10-30 TUN-3492: Refactor OriginService, shrink its interface
- 2020-10-22 TUN-3478: Increase download timeout to 60s
- 2020-10-15 TUN-2640: Users can configure per-origin config. Unify single-rule CLI flow with multi-rule config file code.
2020.10.2
- 2020-10-21 Release 2020.10.1
- 2020-10-21 AUTH-3185 fixed indention error
- 2020-10-19 TUN-3459: Make service install on linux use named tunnels
2020.10.1
- 2020-10-20 Split out typed config from legacy command-line switches; refactor ingress commands and fix tests
- 2020-10-20 Move raw ingress rules to config package
- 2020-10-21 TUN-3476: Fix conversion to string and int slice
- 2020-10-12 TUN-3441: Multiple-origin routing via ingress rules
- 2020-10-15 TUN-3464: Newtype to wrap []ingress.Rule
- 2020-10-15 TUN-3465: Use Go 1.15.3
- 2020-10-15 TUN-3463: Let users run a named tunnel via config file setting
- 2020-10-19 TUN-3475: Unify config file handling with typed config for new fields
- 2020-10-19 TUN-3459: Make service install on linux use named tunnels
- 2020-10-06 AUTH-3148 fixed cloudflared copy and match all the files in the checksum upload
- 2020-10-06 TUN-3436, TUN-3437: Parse ingress from YAML, ensure last rule catches everything
- 2020-10-06 TUN-3446: Use go 1.15.2 and add a step to build cloudflared in the dev Dockerfile
- 2020-10-07 TUN-3439: 'tunnel validate' command to check ingress rules
- 2020-10-07 TUN-3440: 'tunnel rule' command to test ingress rules
- 2020-10-08 TUN-3451: Cloudflared tunnel ingress command
- 2020-10-09 TUN-3452: Fix loading of flags from config file for tunnel run subcommand. This change also cleans up building of tunnel subcommand list, hides deprecated subcommands and improves help.
- 2020-10-08 TUN-3438: move ingress into own package, read into TunnelConfig
2020.10.0
- 2020-10-02 AUTH-2993 cleaned up worker service tests
- 2020-10-02 TUN-3443: Decode as v4api response on non-200 status
- 2020-09-24 TRAFFIC-448: allow the user to specify the proxy address and port to bind to, falling back to 127.0.0.1 and random port if not specified
- 2020-09-28 TUN-3427: Define a struct that only implements RegistrationServer in tunnelpogs
- 2020-09-29 TUN-3430: Copy flags to configure proxy to run subcommand, print relevant tunnel flags in help
- 2020-08-12 AUTH-2993 added workers updater logic
2020.9.3
- 2020-09-22 TRAFFIC-448: build cloudflare for junos and publish to s3
- 2020-09-22 TUN-3410: Request the v1 Tunnelstore API
- 2020-09-23 Release 2020.9.2
- 2020-09-17 updater service exit code should be 11
- 2020-09-18 AUTH-3109 upload the checksum to workers kv on github releases
2020.9.2
- 2020-09-22 TRAFFIC-448: build cloudflare for junos and publish to s3
- 2020-09-22 TUN-3410: Request the v1 Tunnelstore API
- 2020-09-17 AUTH-3103 CI build fixes
- 2020-09-18 AUTH-3110-use-cfsetup-precache
- 2020-09-17 TUN-3295: Show route command results
- 2020-09-16 TUN-3291: cloudflared tunnel run -h explains how to use flags from parent command
- 2020-09-18 AUTH-3109 upload the checksum to workers kv on github releases
- 2020-09-17 updater service exit code should be 11
- 2020-09-01 TUN-3216: UI improvements
- 2020-08-25 Rebased and passed TunnelEventChan to LogServerInfo in new ReconnectTunnel function
- 2020-08-25 TUN-3321: Add box around logs on UI
- 2020-08-26 TUN-3328: Filter out free tunnel has started log from UI
- 2020-08-27 TUN-3333: Add text to UI explaining how to exit
- 2020-08-27 TUN-3335: Dynamically set connection table size for UI
- 2020-08-10 TUN-3238: Update UI when connection re-connects
- 2020-08-17 TUN-3261: Display connections on UI for free classic tunnels
- 2020-07-24 TUN-3201: Create base cloudflared UI structure
- 2020-07-29 TUN-3200: Add connection information to UI
- 2020-07-24 TUN-3255: Update UI to display URL instead of hostname
- 2020-07-29 TUN-3198: Handle errors while running tunnel UI
2020.9.1
- 2020-09-14 TUN-3395: Unhide named tunnel subcommands, tweak help
- 2020-09-15 TUN-3395: Improve help for list command
- 2020-09-14 TUN-3294: Perform basic validation on arguments of route command; remove default pool name which wasn't valid
- 2020-09-15 TUN-3395: Improve help for list command
- 2020-09-16 Use Go 1.15.2
2020.9.0
- 2020-09-11 TUN-3293: Try to use error information from the body of a failed tunnelstore reresponse if available
- 2020-09-04 AUTH-2653 renabled signing
- 2020-09-04 TUN-3377: Tunnel route should check dns/lb before checking tunnel ID
- 2020-09-04 AUTH-2653 changed to proper file extension
- 2020-09-04 AUTH-2653 handle duplicate key import errors
- 2020-09-04 TUN-3345: tunnel run accepts name of tunnel as argument
- 2020-09-08 AUTH-2653 disble error pipe to see what is failing
- 2020-09-08 AUTH-2653 search for the certificate and not the identity
- 2020-09-09 TUN-3284: Use cloudflared/<version> as user agent of tunnelstore client
- 2020-09-09 TUN-3375: Upgrade x/text and gorilla websocket deps
- 2020-09-09 TUN-3375: Upgrade coredns and prometheus dependencies
- 2020-09-09 AUTH-2653 add notarization to mac build
- 2020-09-08 TUN-3292: Mention cleanup in tunnel run help.
- 2020-08-20 AUTH-2016 fixed variable fail
- 2020-08-12 TUN-3352 extra debug logging for websockets
2020.8.2
- 2020-08-20 AUTH-3021 fixed the git version call by using the older flag
- 2020-08-18 TUN-3268: Each connection has its own event digest to reconnect
2020.8.1
- 2020-08-14 AUTH-2975 don't check /etc on windows
- 2020-08-14 AUTH-2977 log file protection
- 2020-08-18 TUN-3286: Use either ID or name in Named Tunnel subcommands.
- 2020-08-18 AUTH-2712 fixed the mac build script
- 2020-08-19 AUTH-2653 disabling signing until we can get the keys
- 2020-08-05 TUN-3233: List tunnels support filtering by deleted, name, existed at and id
- 2020-08-05 TUN-3237: By default, don't show connections that are pending reconnect
- 2020-08-06 TUN-3242: Build with go 1.14
- 2020-08-07 TUN-3243: Refactor tunnel subcommands to allow commands to compose better
- 2020-08-07 AUTH-2864 - add macos build to github release
- 2020-07-31 AUTH-2857 update homebrew script to use new url
- 2020-07-30 TUN-3213: Create, route and run named tunnels in one command
- 2020-07-07 AUTH-2712 added MSI build for a windows agent
2020.8.0
- 2020-07-30 TUN-3220: tunnel route reports created route
- 2020-07-31 TUN-3221: ConnectionOptions tracks numPreviousAttempts.
- 2020-07-20 TUN-3190: Initialize logger using command line flags in tunnels subcommands
- 2020-07-21 TUN-3192: Use zone ID in tunnelstore request path; improve debug logging
- 2020-07-23 TUN-3194: Don't render log output when level is not enabled
- 2020-07-22 AUTH-2016 adds sha256 hashes to releases
- 2020-07-27 Removes centos 6 build
- 2020-07-27 TUN-3209: Add benchmark for header serialization
- 2020-07-24 TUN-3209: improve performance and reduce allocations during user header serialization from h1 to h2
- 2020-07-26 TUN-3208: Add benchmark for large response write
- 2020-07-27 TUN-3208: Reduce copies and allocations on h2mux write path. Pre-allocate 16KB write buffer on the first write if possible. Use explicit byte array for chunks on write thread to avoid copying through intermediate buffer due to io.CopyN.
- 2020-07-28 AUTH-2714: Adds arm64 cloudflared build
- 2020-07-29 AUTH-2927 run message update after all github builds are done
- 2020-07-17 AUTH-2902 redirect with just the root host on curl commands
2020.7.4
- 2020-07-20 Build cloudflared for arm64 on native agents
- 2020-07-10 TUN-3048: Handle error when user tries to delete active tunnel
- 2020-07-14 AUTH-2890: adds error handler to cli actions
- 2020-07-06 TUN-3156: Add route subcommand under tunnel
2020.7.3
- 2020-07-13 Change scp command to use file glob that matches both cloudflared rpms and debs
+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.3-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.3.linux-amd64.tar.gz -P /tmp/
- tar -C /usr/local -xzf /tmp/go1.15.3.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.3.linux-amd64.tar.gz -P /tmp/
- tar -C /usr/local -xzf /tmp/go1.15.3.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 (
+6 -6
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
@@ -27,17 +27,17 @@ func UsageError(format string, args ...interface{}) error {
// Ensures exit with error code if actionFunc returns an error
func ErrorHandler(actionFunc cli.ActionFunc) cli.ActionFunc {
return func(ctx *cli.Context) error {
defer logger.SharedWriteManager.Shutdown()
err := actionFunc(ctx)
if err != nil {
if _, ok := err.(usageError); ok {
msg := fmt.Sprintf("%s\nSee 'cloudflared %s --help'.", err.Error(), ctx.Command.FullName())
return cli.Exit(msg, -1)
err = cli.Exit(msg, -1)
} else if _, ok := err.(cli.ExitCoder); !ok {
err = cli.Exit(err.Error(), 1)
}
// os.Exits with error code if err is cli.ExitCoder or cli.MultiError
cli.HandleExitCoder(err)
err = cli.Exit(err.Error(), 1)
}
logger.SharedWriteManager.Shutdown()
return err
}
}
+223 -15
View File
@@ -1,16 +1,18 @@
package config
import (
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"time"
homedir "github.com/mitchellh/go-homedir"
"gopkg.in/urfave/cli.v2"
"gopkg.in/urfave/cli.v2/altsrc"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"gopkg.in/yaml.v2"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/validation"
)
@@ -26,10 +28,18 @@ var (
// Launchd doesn't set root env variables, so there is default
// Windows default config dir was ~/cloudflare-warp in documentation; let's keep it compatible
DefaultConfigDirs = []string{"~/.cloudflared", "~/.cloudflare-warp", "~/cloudflare-warp", "/etc/cloudflared", DefaultUnixConfigLocation}
defaultUserConfigDirs = []string{"~/.cloudflared", "~/.cloudflare-warp", "~/cloudflare-warp"}
defaultNixConfigDirs = []string{"/etc/cloudflared", DefaultUnixConfigLocation}
ErrNoConfigFile = fmt.Errorf("Cannot determine default configuration path. No file %v in %v", DefaultConfigFiles, DefaultConfigSearchDirectories())
)
const DefaultCredentialFile = "cert.pem"
const (
DefaultCredentialFile = "cert.pem"
// BastionFlag is to enable bastion, or jump host, operation
BastionFlag = "bastion"
)
// DefaultConfigDirectory returns the default directory of the config file
func DefaultConfigDirectory() string {
@@ -63,6 +73,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)
@@ -77,19 +97,11 @@ func FileExists(path string) (bool, error) {
return true, nil
}
// FindInputSourceContext pulls the input source from the config flag.
func FindInputSourceContext(context *cli.Context) (altsrc.InputSourceContext, error) {
if context.String("config") != "" {
return altsrc.NewYamlSourceFromFile(context.String("config"))
}
return nil, nil
}
// FindDefaultConfigPath returns the first path that contains a config file.
// If none of the combination of DefaultConfigDirs and DefaultConfigFiles
// If none of the combination of DefaultConfigSearchDirectories() and DefaultConfigFiles
// contains a config file, return empty string.
func FindDefaultConfigPath() string {
for _, configDir := range DefaultConfigDirs {
for _, configDir := range DefaultConfigSearchDirectories() {
for _, configFile := range DefaultConfigFiles {
dirPath, err := homedir.Expand(configDir)
if err != nil {
@@ -188,3 +200,199 @@ func ValidateUrl(c *cli.Context, allowFromArgs bool) (string, error) {
validUrl, err := validation.ValidateUrl(url)
return validUrl, err
}
type UnvalidatedIngressRule struct {
Hostname string
Path string
Service string
OriginRequest OriginRequestConfig `yaml:"originRequest"`
}
// OriginRequestConfig is a set of optional fields that users may set to
// customize how cloudflared sends requests to origin services. It is used to set
// up general config that apply to all rules, and also, specific per-rule
// config.
// Note: To specify a time.Duration in go-yaml, use e.g. "3s" or "24h".
type OriginRequestConfig struct {
// HTTP proxy timeout for establishing a new connection
ConnectTimeout *time.Duration `yaml:"connectTimeout"`
// HTTP proxy timeout for completing a TLS handshake
TLSTimeout *time.Duration `yaml:"tlsTimeout"`
// HTTP proxy TCP keepalive duration
TCPKeepAlive *time.Duration `yaml:"tcpKeepAlive"`
// HTTP proxy should disable "happy eyeballs" for IPv4/v6 fallback
NoHappyEyeballs *bool `yaml:"noHappyEyeballs"`
// HTTP proxy maximum keepalive connection pool size
KeepAliveConnections *int `yaml:"keepAliveConnections"`
// HTTP proxy timeout for closing an idle connection
KeepAliveTimeout *time.Duration `yaml:"keepAliveTimeout"`
// Sets the HTTP Host header for the local webserver.
HTTPHostHeader *string `yaml:"httpHostHeader"`
// Hostname on the origin server certificate.
OriginServerName *string `yaml:"originServerName"`
// Path to the CA for the certificate of your origin.
// This option should be used only if your certificate is not signed by Cloudflare.
CAPool *string `yaml:"caPool"`
// Disables TLS verification of the certificate presented by your origin.
// Will allow any certificate from the origin to be accepted.
// Note: The connection from your machine to Cloudflare's Edge is still encrypted.
NoTLSVerify *bool `yaml:"noTLSVerify"`
// Disables chunked transfer encoding.
// Useful if you are running a WSGI server.
DisableChunkedEncoding *bool `yaml:"disableChunkedEncoding"`
// Runs as jump host
BastionMode *bool `yaml:"bastionMode"`
// Listen address for the proxy.
ProxyAddress *string `yaml:"proxyAddress"`
// Listen port for the proxy.
ProxyPort *uint `yaml:"proxyPort"`
// Valid options are 'socks' or empty.
ProxyType *string `yaml:"proxyType"`
}
type Configuration struct {
TunnelID string `yaml:"tunnel"`
Ingress []UnvalidatedIngressRule
OriginRequest OriginRequestConfig `yaml:"originRequest"`
sourceFile string
}
type configFileSettings struct {
Configuration `yaml:",inline"`
// older settings will be aggregated into the generic map, should be read via cli.Context
Settings map[string]interface{} `yaml:",inline"`
}
func (c *Configuration) Source() string {
return c.sourceFile
}
func (c *configFileSettings) Int(name string) (int, error) {
if raw, ok := c.Settings[name]; ok {
if v, ok := raw.(int); ok {
return v, nil
}
return 0, fmt.Errorf("expected int found %T for %s", raw, name)
}
return 0, nil
}
func (c *configFileSettings) Duration(name string) (time.Duration, error) {
if raw, ok := c.Settings[name]; ok {
switch v := raw.(type) {
case time.Duration:
return v, nil
case string:
return time.ParseDuration(v)
}
return 0, fmt.Errorf("expected duration found %T for %s", raw, name)
}
return 0, nil
}
func (c *configFileSettings) Float64(name string) (float64, error) {
if raw, ok := c.Settings[name]; ok {
if v, ok := raw.(float64); ok {
return v, nil
}
return 0, fmt.Errorf("expected float found %T for %s", raw, name)
}
return 0, nil
}
func (c *configFileSettings) String(name string) (string, error) {
if raw, ok := c.Settings[name]; ok {
if v, ok := raw.(string); ok {
return v, nil
}
return "", fmt.Errorf("expected string found %T for %s", raw, name)
}
return "", nil
}
func (c *configFileSettings) StringSlice(name string) ([]string, error) {
if raw, ok := c.Settings[name]; ok {
if slice, ok := raw.([]interface{}); ok {
strSlice := make([]string, len(slice))
for i, v := range slice {
str, ok := v.(string)
if !ok {
return nil, fmt.Errorf("expected string, found %T for %v", i, v)
}
strSlice[i] = str
}
return strSlice, nil
}
return nil, fmt.Errorf("expected string slice found %T for %s", raw, name)
}
return nil, nil
}
func (c *configFileSettings) IntSlice(name string) ([]int, error) {
if raw, ok := c.Settings[name]; ok {
if slice, ok := raw.([]interface{}); ok {
intSlice := make([]int, len(slice))
for i, v := range slice {
str, ok := v.(int)
if !ok {
return nil, fmt.Errorf("expected int, found %T for %v ", v, v)
}
intSlice[i] = str
}
return intSlice, nil
}
if v, ok := raw.([]int); ok {
return v, nil
}
return nil, fmt.Errorf("expected int slice found %T for %s", raw, name)
}
return nil, nil
}
func (c *configFileSettings) Generic(name string) (cli.Generic, error) {
return nil, errors.New("option type Generic not supported")
}
func (c *configFileSettings) Bool(name string) (bool, error) {
if raw, ok := c.Settings[name]; ok {
if v, ok := raw.(bool); ok {
return v, nil
}
return false, fmt.Errorf("expected boolean found %T for %s", raw, name)
}
return false, nil
}
var configuration configFileSettings
func GetConfiguration() *Configuration {
return &configuration.Configuration
}
// ReadConfigFile returns InputSourceContext initialized from the configuration file.
// On repeat calls returns with the same file, returns without reading the file again; however,
// if value of "config" flag changes, will read the new config file
func ReadConfigFile(c *cli.Context, log logger.Service) (*configFileSettings, error) {
configFile := c.String("config")
if configuration.Source() == configFile || configFile == "" {
if configuration.Source() == "" {
return nil, ErrNoConfigFile
}
return &configuration, nil
}
log.Debugf("Loading configuration from %s", configFile)
file, err := os.Open(configFile)
if err != nil {
if os.IsNotExist(err) {
err = ErrNoConfigFile
}
return nil, err
}
defer file.Close()
if err := yaml.NewDecoder(file).Decode(&configuration); err != nil {
return nil, errors.Wrap(err, "error parsing config file at "+configFile)
}
configuration.sourceFile = configFile
return &configuration, nil
}
@@ -0,0 +1,76 @@
package config
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v2"
)
func TestConfigFileSettings(t *testing.T) {
var (
firstIngress = UnvalidatedIngressRule{
Hostname: "tunnel1.example.com",
Path: "/id",
Service: "https://localhost:8000",
}
secondIngress = UnvalidatedIngressRule{
Hostname: "*",
Path: "",
Service: "https://localhost:8001",
}
)
rawYAML := `
tunnel: config-file-test
ingress:
- hostname: tunnel1.example.com
path: /id
service: https://localhost:8000
- hostname: "*"
service: https://localhost:8001
retries: 5
grace-period: 30s
percentage: 3.14
hostname: example.com
tag:
- test
- central-1
counters:
- 123
- 456
`
var config configFileSettings
err := yaml.Unmarshal([]byte(rawYAML), &config)
assert.NoError(t, err)
assert.Equal(t, "config-file-test", config.TunnelID)
assert.Equal(t, firstIngress, config.Ingress[0])
assert.Equal(t, secondIngress, config.Ingress[1])
retries, err := config.Int("retries")
assert.NoError(t, err)
assert.Equal(t, 5, retries)
gracePeriod, err := config.Duration("grace-period")
assert.NoError(t, err)
assert.Equal(t, time.Second*30, gracePeriod)
percentage, err := config.Float64("percentage")
assert.NoError(t, err)
assert.Equal(t, 3.14, percentage)
hostname, err := config.String("hostname")
assert.NoError(t, err)
assert.Equal(t, "example.com", hostname)
tags, err := config.StringSlice("tag")
assert.NoError(t, err)
assert.Equal(t, "test", tags[0])
assert.Equal(t, "central-1", tags[1])
counters, err := config.IntSlice("counters")
assert.NoError(t, err)
assert.Equal(t, 123, counters[0])
assert.Equal(t, 456, counters[1])
}
+2 -1
View File
@@ -4,9 +4,10 @@ import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/watcher"
"github.com/stretchr/testify/assert"
)
type mockNotifier struct {
+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{}) {
+65 -20
View File
@@ -7,10 +7,12 @@ import (
"os"
"path/filepath"
"github.com/pkg/errors"
cli "github.com/urfave/cli/v2"
"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"
)
func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
@@ -21,12 +23,18 @@ 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),
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "legacy",
Usage: "Generate service file for non-named tunnels",
},
},
},
&cli.Command{
Name: "uninstall",
Usage: "Uninstall the Argo Tunnel service",
Action: uninstallLinuxService,
Action: cliutil.ErrorHandler(uninstallLinuxService),
},
},
})
@@ -39,6 +47,7 @@ const (
serviceConfigDir = "/etc/cloudflared"
serviceConfigFile = "config.yml"
serviceCredentialFile = "cert.pem"
serviceConfigPath = serviceConfigDir + "/" + serviceConfigFile
)
var systemdTemplates = []ServiceTemplate{
@@ -51,7 +60,7 @@ After=network.target
[Service]
TimeoutStartSec=0
Type=notify
ExecStart={{ .Path }} --config /etc/cloudflared/config.yml --origincert /etc/cloudflared/cert.pem --no-autoupdate
ExecStart={{ .Path }} --config /etc/cloudflared/config.yml --no-autoupdate{{ range .ExtraArgs }} {{ . }}{{ end }}
Restart=on-failure
RestartSec=5s
@@ -66,7 +75,7 @@ Description=Update Argo Tunnel
After=network.target
[Service]
ExecStart=/bin/bash -c '{{ .Path }} update; code=$?; if [ $code -eq 64 ]; then systemctl restart cloudflared; exit 0; fi; exit $code'
ExecStart=/bin/bash -c '{{ .Path }} update; code=$?; if [ $code -eq 11 ]; then systemctl restart cloudflared; exit 0; fi; exit $code'
`,
},
{
@@ -101,7 +110,7 @@ var sysvTemplate = ServiceTemplate{
# Description: Argo Tunnel agent
### END INIT INFO
name=$(basename $(readlink -f $0))
cmd="{{.Path}} --config /etc/cloudflared/config.yml --origincert /etc/cloudflared/cert.pem --pidfile /var/run/$name.pid --autoupdate-freq 24h0m0s"
cmd="{{.Path}} --config /etc/cloudflared/config.yml --pidfile /var/run/$name.pid --autoupdate-freq 24h0m0s{{ range .ExtraArgs }} {{ . }}{{ end }}"
pid_file="/var/run/$name.pid"
stdout_log="/var/log/$name.log"
stderr_log="/var/log/$name.err"
@@ -181,10 +190,6 @@ func isSystemd() bool {
}
func copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile string, logger logger.Service) error {
if err := ensureConfigDirExists(serviceConfigDir); err != nil {
return err
}
srcCredentialPath := filepath.Join(userConfigDir, userCredentialFile)
destCredentialPath := filepath.Join(serviceConfigDir, serviceCredentialFile)
if srcCredentialPath != destCredentialPath {
@@ -215,23 +220,63 @@ func installLinuxService(c *cli.Context) error {
if err != nil {
return fmt.Errorf("error determining executable path: %v", err)
}
templateArgs := ServiceTemplateArgs{Path: etPath}
templateArgs := ServiceTemplateArgs{
Path: etPath,
}
userConfigDir := filepath.Dir(c.String("config"))
userConfigFile := filepath.Base(c.String("config"))
userCredentialFile := config.DefaultCredentialFile
if err = copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile, logger); err != nil {
logger.Errorf("Failed to copy user configuration: %s. Before running the service, ensure that %s contains two files, %s and %s", err,
serviceConfigDir, serviceCredentialFile, serviceConfigFile)
if err := ensureConfigDirExists(serviceConfigDir); err != nil {
return err
}
if c.Bool("legacy") {
userConfigDir := filepath.Dir(c.String("config"))
userConfigFile := filepath.Base(c.String("config"))
userCredentialFile := config.DefaultCredentialFile
if err = copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile, logger); err != nil {
logger.Errorf("Failed to copy user configuration: %s. Before running the service, ensure that %s contains two files, %s and %s", err,
serviceConfigDir, serviceCredentialFile, serviceConfigFile)
return err
}
templateArgs.ExtraArgs = []string{
"--origincert", serviceConfigDir + "/" + serviceCredentialFile,
}
} else {
src, err := config.ReadConfigFile(c, logger)
if err != nil {
return err
}
// can't use context because this command doesn't define "credentials-file" flag
configPresent := func(s string) bool {
val, err := src.String(s)
return err == nil && val != ""
}
if src.TunnelID == "" || !configPresent("credentials-file") {
return fmt.Errorf(`Configuration file %s must contain entries for the tunnel to run and its associated credentials:
tunnel: TUNNEL-UUID
credentials-file: CREDENTIALS-FILE
`, src.Source())
}
if src.Source() != serviceConfigPath {
if exists, err := config.FileExists(serviceConfigPath); err != nil || exists {
return fmt.Errorf("Possible conflicting configuration in %[1]s and %[2]s. Either remove %[2]s or run `cloudflared --config %[2]s service install`", src.Source(), serviceConfigPath)
}
if err := copyFile(src.Source(), serviceConfigPath); err != nil {
return fmt.Errorf("failed to copy %s to %s: %w", src.Source(), serviceConfigPath, err)
}
}
templateArgs.ExtraArgs = []string{
"tunnel", "run",
}
}
switch {
case isSystemd():
logger.Infof("Using Systemd")
return installSystemd(&templateArgs, logger)
default:
logger.Infof("Using Sysv")
logger.Infof("Using SysV")
return installSysv(&templateArgs, logger)
}
}
@@ -290,7 +335,7 @@ func uninstallLinuxService(c *cli.Context) error {
logger.Infof("Using Systemd")
return uninstallSystemd(logger)
default:
logger.Infof("Using Sysv")
logger.Infof("Using SysV")
return uninstallSysv(logger)
}
}
+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),
},
},
})
+35 -16
View File
@@ -13,11 +13,12 @@ import (
log "github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/metrics"
"github.com/cloudflare/cloudflared/overwatch"
"github.com/cloudflare/cloudflared/tunneldns"
"github.com/cloudflare/cloudflared/watcher"
raven "github.com/getsentry/raven-go"
homedir "github.com/mitchellh/go-homedir"
cli "gopkg.in/urfave/cli.v2"
cli "github.com/urfave/cli/v2"
"github.com/pkg/errors"
)
@@ -76,21 +77,42 @@ func main() {
and configure access control.`
app.Flags = flags()
app.Action = action(Version, shutdownC, graceShutdownC)
app.Before = tunnel.Before
app.Before = tunnel.SetFlagsFromConfigFile
app.Commands = commands(cli.ShowVersion)
tunnel.Init(Version, shutdownC, graceShutdownC) // we need this to support the tunnel sub command...
access.Init(shutdownC, graceShutdownC)
updater.Init(Version)
runApp(app, shutdownC, graceShutdownC)
}
func commands(version func(c *cli.Context)) []*cli.Command {
cmds := []*cli.Command{
{
Name: "update",
Action: updater.Update,
Usage: "Update the agent if a new version exists",
ArgsUsage: " ",
Name: "update",
Action: cliutil.ErrorHandler(updater.Update),
Usage: "Update the agent if a new version exists",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "beta",
Usage: "specify if you wish to update to the latest beta version",
},
&cli.BoolFlag{
Name: "force",
Usage: "specify if you wish to force an upgrade to the latest version regardless of the current version",
Hidden: true,
},
&cli.BoolFlag{
Name: "staging",
Usage: "specify if you wish to use the staging url for updating",
Hidden: true,
},
&cli.StringFlag{
Name: "version",
Usage: "specify a version you wish to upgrade or downgrade to",
Hidden: false,
},
},
Description: `Looks for a new version on the official download server.
If a new version exists, updates the agent binary and quits.
Otherwise, does nothing.
@@ -108,6 +130,7 @@ To determine if an update happened in a script, check for error code 64.`,
},
}
cmds = append(cmds, tunnel.Commands()...)
cmds = append(cmds, tunneldns.Command(false))
cmds = append(cmds, access.Commands()...)
return cmds
}
@@ -122,23 +145,19 @@ func isEmptyInvocation(c *cli.Context) bool {
}
func action(version string, shutdownC, graceShutdownC chan struct{}) cli.ActionFunc {
return func(c *cli.Context) (err error) {
return cliutil.ErrorHandler(func(c *cli.Context) (err error) {
if isEmptyInvocation(c) {
return handleServiceMode(shutdownC)
}
tags := make(map[string]string)
tags["hostname"] = c.String("hostname")
raven.SetTagsContext(tags)
raven.CapturePanic(func() { err = tunnel.StartServer(c, version, shutdownC, graceShutdownC, nil) }, nil)
exitCode := 0
raven.CapturePanic(func() { err = tunnel.TunnelCommand(c) }, nil)
if err != nil {
handleError(err)
exitCode = 1
captureError(err)
}
// we already handle error printing, so we pass an empty string so we
// don't have to print again.
return cli.Exit("", exitCode)
}
return err
})
}
func userHomeDir() (string, error) {
@@ -154,7 +173,7 @@ func userHomeDir() (string, error) {
}
// In order to keep the amount of noise sent to Sentry low, typical network errors can be filtered out here by a substring match.
func handleError(err error) {
func captureError(err error) {
errorMessage := err.Error()
for _, ignoredErrorMessage := range ignoredErrors {
if strings.Contains(errorMessage, ignoredErrorMessage) {
+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
}
+32 -3
View File
@@ -10,8 +10,9 @@ import (
"os/exec"
"text/template"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/mitchellh/go-homedir"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
)
type ServiceTemplate struct {
@@ -21,7 +22,8 @@ type ServiceTemplate struct {
}
type ServiceTemplateArgs struct {
Path string
Path string
ExtraArgs []string
}
func (st *ServiceTemplate) ResolvePath() (string, error) {
@@ -80,7 +82,7 @@ func runCommand(command string, args ...string) error {
return fmt.Errorf("error starting %s: %v", command, err)
}
ioutil.ReadAll(stderr)
_, _ = ioutil.ReadAll(stderr)
err = cmd.Wait()
if err != nil {
return fmt.Errorf("%s returned with error: %v", command, err)
@@ -139,6 +141,33 @@ func copyCredential(srcCredentialPath, destCredentialPath string) error {
return nil
}
func copyFile(src, dest string) error {
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
destFile, err := os.Create(dest)
if err != nil {
return err
}
ok := false
defer func() {
destFile.Close()
if !ok {
_ = os.Remove(dest)
}
}()
if _, err := io.Copy(destFile, srcFile); err != nil {
return err
}
ok = true
return nil
}
func copyConfig(srcConfigPath, destConfigPath string) error {
// Copy or create config
destFile, exists, err := openFile(destConfigPath, true)
+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
}
File diff suppressed because it is too large Load Diff
+28 -78
View File
@@ -1,19 +1,15 @@
package tunnel
import (
"context"
"crypto/tls"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/origin"
"github.com/cloudflare/cloudflared/tlsconfig"
@@ -23,8 +19,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 +30,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 +91,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")
@@ -184,12 +180,6 @@ func prepareTunnelConfig(
tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID})
originURL, err := config.ValidateUrl(c, compatibilityMode)
if err != nil {
logger.Errorf("Error validating origin URL: %s", err)
return nil, errors.Wrap(err, "Error validating origin URL")
}
var originCert []byte
if !isFreeTunnel {
originCert, err = getOriginCert(c, logger)
@@ -198,65 +188,9 @@ func prepareTunnelConfig(
}
}
originCertPool, err := tlsconfig.LoadOriginCA(c, logger)
if err != nil {
logger.Errorf("Error loading cert pool: %s", err)
return nil, errors.Wrap(err, "Error loading cert pool")
}
tunnelMetrics := origin.NewTunnelMetrics()
httpTransport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
MaxIdleConns: c.Int("proxy-keepalive-connections"),
MaxIdleConnsPerHost: c.Int("proxy-keepalive-connections"),
IdleConnTimeout: c.Duration("proxy-keepalive-timeout"),
TLSHandshakeTimeout: c.Duration("proxy-tls-timeout"),
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{RootCAs: originCertPool, InsecureSkipVerify: c.IsSet("no-tls-verify")},
}
dialer := &net.Dialer{
Timeout: c.Duration("proxy-connect-timeout"),
KeepAlive: c.Duration("proxy-tcp-keepalive"),
}
if c.Bool("proxy-no-happy-eyeballs") {
dialer.FallbackDelay = -1 // As of Golang 1.12, a negative delay disables "happy eyeballs"
}
dialContext := dialer.DialContext
if c.IsSet("unix-socket") {
unixSocket, err := config.ValidateUnixSocket(c)
if err != nil {
logger.Errorf("Error validating --unix-socket: %s", err)
return nil, errors.Wrap(err, "Error validating --unix-socket")
}
logger.Infof("Proxying tunnel requests to unix:%s", unixSocket)
httpTransport.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
// if --unix-socket specified, enforce network type "unix"
return dialContext(ctx, "unix", unixSocket)
}
} else {
logger.Infof("Proxying tunnel requests to %s", originURL)
httpTransport.DialContext = dialContext
}
if !c.IsSet("hello-world") && c.IsSet("origin-server-name") {
httpTransport.TLSClientConfig.ServerName = c.String("origin-server-name")
}
// If tunnel running in bastion mode, a connection to origin will not exist until initiated by the client.
if !c.IsSet(bastionFlag) {
if err = validation.ValidateHTTPService(originURL, hostname, httpTransport); err != nil {
logger.Errorf("unable to connect to the origin: %s", err)
}
}
toEdgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c)
if err != nil {
logger.Errorf("unable to create TLS config to connect with edge: %s", err)
return nil, errors.Wrap(err, "unable to create TLS config to connect with edge")
}
var ingressRules ingress.Ingress
if namedTunnel != nil {
clientUUID, err := uuid.NewRandom()
if err != nil {
@@ -268,20 +202,37 @@ func prepareTunnelConfig(
Version: version,
Arch: fmt.Sprintf("%s_%s", buildInfo.GoOS, buildInfo.GoArch),
}
ingressRules, err = ingress.ParseIngress(config.GetConfiguration())
if err != nil && err != ingress.ErrNoIngressRules {
return nil, err
}
if !ingressRules.IsEmpty() && c.IsSet("url") {
return nil, ingress.ErrURLIncompatibleWithIngress
}
}
// Convert single-origin configuration into multi-origin configuration.
if ingressRules.IsEmpty() {
ingressRules, err = ingress.NewSingleOrigin(c, compatibilityMode, logger)
if err != nil {
return nil, err
}
}
toEdgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c)
if err != nil {
logger.Errorf("unable to create TLS config to connect with edge: %s", err)
return nil, errors.Wrap(err, "unable to create TLS config to connect with edge")
}
return &origin.TunnelConfig{
BuildInfo: buildInfo,
ClientID: clientID,
ClientTlsConfig: httpTransport.TLSClientConfig,
CompressionQuality: c.Uint64("compression-quality"),
EdgeAddrs: c.StringSlice("edge"),
GracePeriod: c.Duration("grace-period"),
HAConnections: c.Int("ha-connections"),
HTTPTransport: httpTransport,
HeartbeatInterval: c.Duration("heartbeat-interval"),
Hostname: hostname,
HTTPHostHeader: c.String("http-host-header"),
IncidentLookup: origin.NewIncidentLookup(),
IsAutoupdated: c.Bool("is-autoupdated"),
IsFreeTunnel: isFreeTunnel,
@@ -291,9 +242,7 @@ func prepareTunnelConfig(
MaxHeartbeats: c.Uint64("heartbeat-count"),
Metrics: tunnelMetrics,
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
NoChunkedEncoding: c.Bool("no-chunked-encoding"),
OriginCert: originCert,
OriginUrl: originURL,
ReportedVersion: version,
Retries: c.Uint("retries"),
RunFromTerminal: isRunningFromTerminal(),
@@ -301,8 +250,9 @@ func prepareTunnelConfig(
TlsConfig: toEdgeTLSConfig,
NamedTunnel: namedTunnel,
ReplaceExisting: c.Bool("force"),
IngressRules: ingressRules,
// 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
}
@@ -0,0 +1,114 @@
package tunnel
import (
"fmt"
"net/url"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/ingress"
)
func buildIngressSubcommand() *cli.Command {
return &cli.Command{
Name: "ingress",
Category: "Tunnel",
Usage: "Validate and test cloudflared tunnel's ingress configuration",
UsageText: "cloudflared tunnel [--config FILEPATH] ingress COMMAND [arguments...]",
Hidden: true,
Description: ` Cloudflared lets you route traffic from the internet to multiple different addresses on your
origin. Multiple-origin routing is configured by a set of rules. Each rule matches traffic
by its hostname or path, and routes it to an address. These rules are configured under the
'ingress' key of your config.yaml, for example:
ingress:
- hostname: www.example.com
service: https://localhost:8000
- hostname: *.example.xyz
path: /[a-zA-Z]+.html
service: https://localhost:8001
- hostname: *
service: https://localhost:8002
To ensure cloudflared can route all incoming requests, the last rule must be a catch-all
rule that matches all traffic. You can validate these rules with the 'ingress validate'
command, and test which rule matches a particular URL with 'ingress rule <URL>'.
Multiple-origin routing is incompatible with the --url flag.`,
Subcommands: []*cli.Command{buildValidateIngressCommand(), buildTestURLCommand()},
}
}
func buildValidateIngressCommand() *cli.Command {
return &cli.Command{
Name: "validate",
Action: cliutil.ErrorHandler(validateIngressCommand),
Usage: "Validate the ingress configuration ",
UsageText: "cloudflared tunnel [--config FILEPATH] ingress validate",
Description: "Validates the configuration file, ensuring your ingress rules are OK.",
}
}
func buildTestURLCommand() *cli.Command {
return &cli.Command{
Name: "rule",
Action: cliutil.ErrorHandler(testURLCommand),
Usage: "Check which ingress rule matches a given request URL",
UsageText: "cloudflared tunnel [--config FILEPATH] ingress rule URL",
ArgsUsage: "URL",
Description: "Check which ingress rule matches a given request URL. " +
"Ingress rules match a request's hostname and path. Hostname is " +
"optional and is either a full hostname like `www.example.com` or a " +
"hostname with a `*` for its subdomains, e.g. `*.example.com`. Path " +
"is optional and matches a regular expression, like `/[a-zA-Z0-9_]+.html`",
}
}
// validateIngressCommand check the syntax of the ingress rules in the cloudflared config file
func validateIngressCommand(c *cli.Context) error {
conf := config.GetConfiguration()
if conf.Source() == "" {
fmt.Println("No configuration file was found. Please create one, or use the --config flag to specify its filepath. You can use the help command to learn more about configuration files")
return nil
}
fmt.Println("Validating rules from", conf.Source())
if _, err := ingress.ParseIngress(conf); err != nil {
return errors.Wrap(err, "Validation failed")
}
if c.IsSet("url") {
return ingress.ErrURLIncompatibleWithIngress
}
fmt.Println("OK")
return nil
}
// testURLCommand checks which ingress rule matches the given URL.
func testURLCommand(c *cli.Context) error {
requestArg := c.Args().First()
if requestArg == "" {
return errors.New("cloudflared tunnel rule expects a single argument, the URL to test")
}
requestURL, err := url.Parse(requestArg)
if err != nil {
return fmt.Errorf("%s is not a valid URL", requestArg)
}
if requestURL.Hostname() == "" && requestURL.Scheme == "" {
return fmt.Errorf("%s doesn't have a hostname, consider adding a scheme", requestArg)
}
conf := config.GetConfiguration()
fmt.Println("Using rules from", conf.Source())
ing, err := ingress.ParseIngress(conf)
if err != nil {
return errors.Wrap(err, "Validation failed")
}
_, i := ing.FindMatchingRule(requestURL.Hostname(), requestURL.Path)
fmt.Printf("Matched rule #%d\n", i+1)
fmt.Println(ing.Rules[i].MultiLineString())
return nil
}
+23 -5
View File
@@ -7,12 +7,14 @@ import (
"path/filepath"
"syscall"
homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
cli "github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/cmd/cloudflared/transfer"
"github.com/cloudflare/cloudflared/logger"
homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
cli "gopkg.in/urfave/cli.v2"
)
const (
@@ -20,6 +22,22 @@ const (
callbackStoreURL = "https://login.argotunnel.com/"
)
func buildLoginSubcommand(hidden bool) *cli.Command {
return &cli.Command{
Name: "login",
Action: cliutil.ErrorHandler(login),
Usage: "Generate a configuration file with your login details",
ArgsUsage: " ",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "url",
Hidden: true,
},
},
Hidden: hidden,
}
}
func login(c *cli.Context) error {
logger, err := logger.New()
if err != nil {
@@ -40,7 +58,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 +69,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,355 @@
package tunnel
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"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"
)
// 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
}
return "", fmt.Errorf("Tunnel credentials file %s doesn't exist or is not a file", filePath)
}
// Fallback to look for tunnel credentials in the origin cert directory
if originCertPath, err := findOriginCert(sc.c, sc.logger); err == nil {
originCertDir := filepath.Dir(originCertPath)
if filePath, err := tunnelFilePath(tunnelID, originCertDir); err == nil {
if validFilePath(filePath) {
return filePath, nil
}
}
}
// Last resort look under default config directories
for _, configDir := range config.DefaultConfigSearchDirectories() {
if filePath, err := tunnelFilePath(tunnelID, configDir); err == nil {
if validFilePath(filePath) {
return filePath, nil
}
}
}
return "", fmt.Errorf("Tunnel credentials file not found")
}
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)
}
})
}
}
+315 -233
View File
@@ -7,6 +7,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"text/tabwriter"
@@ -15,14 +16,14 @@ import (
"github.com/google/uuid"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"golang.org/x/net/idna"
"gopkg.in/yaml.v2"
"github.com/cloudflare/cloudflared/certutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/origin"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/cloudflare/cloudflared/tunnelstore"
)
@@ -37,36 +38,67 @@ var (
Aliases: []string{"d"},
Usage: "Include deleted tunnels in the list",
}
outputFormatFlag = &cli.StringFlag{
listNameFlag = &cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
Usage: "List tunnels with the given `NAME`",
}
listExistedAtFlag = &cli.TimestampFlag{
Name: "when",
Aliases: []string{"w"},
Usage: "List tunnels that are active at the given `TIME` in RFC3339 format",
Layout: tunnelstore.TimeLayout,
DefaultText: fmt.Sprintf("current time, %s", time.Now().Format(tunnelstore.TimeLayout)),
}
listIDFlag = &cli.StringFlag{
Name: "id",
Aliases: []string{"i"},
Usage: "List tunnel by `ID`",
}
showRecentlyDisconnected = &cli.BoolFlag{
Name: "show-recently-disconnected",
Aliases: []string{"rd"},
Usage: "Include connections that have recently disconnected in the list",
}
outputFormatFlag = altsrc.NewStringFlag(&cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "Render output using given `FORMAT`. Valid options are 'json' or 'yaml'",
}
forceFlag = &cli.BoolFlag{
})
forceFlag = altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "By default, if a tunnel is currently being run from a cloudflared, you can't " +
"simultaneously rerun it again from a second cloudflared. The --force flag lets you " +
"overwrite the previous tunnel. If you want to use a single hostname with multiple " +
"tunnels, you can do so with Cloudflare's Load Balancer product.",
}
credentialsFileFlag = &cli.StringFlag{
})
credentialsFileFlag = altsrc.NewStringFlag(&cli.StringFlag{
Name: "credentials-file",
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),
Usage: "Create a new tunnel with given name",
ArgsUsage: "TUNNEL-NAME",
Hidden: hideSubcommands,
Flags: []cli.Flag{outputFormatFlag},
Name: "create",
Action: cliutil.ErrorHandler(createCommand),
Usage: "Create a new tunnel with given name",
UsageText: "cloudflared tunnel [tunnel command options] create [subcommand options] NAME",
Description: `Creates a tunnel, registers it with Cloudflare edge and generates credential file used to run this tunnel.
Use "cloudflared tunnel route" subcommand to map a DNS name to this tunnel and "cloudflared tunnel run" to start the connection.
For example, to create a tunnel named 'my-tunnel' run:
$ cloudflared tunnel create my-tunnel`,
Flags: []cli.Flag{outputFormatFlag},
CustomHelpTemplate: commandHelpTemplate(),
}
}
@@ -77,62 +109,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 +148,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 {
@@ -205,41 +158,43 @@ func validFilePath(path string) bool {
func buildListCommand() *cli.Command {
return &cli.Command{
Name: "list",
Action: cliutil.ErrorHandler(listTunnels),
Usage: "List existing tunnels",
ArgsUsage: " ",
Hidden: hideSubcommands,
Flags: []cli.Flag{outputFormatFlag, showDeletedFlag},
Name: "list",
Action: cliutil.ErrorHandler(listCommand),
Usage: "List existing tunnels",
UsageText: "cloudflared tunnel [tunnel command options] list [subcommand options]",
Description: "cloudflared tunnel list will display all active tunnels, their created time and associated connections. Use -d flag to include deleted tunnels. See the list of options to filter the list",
Flags: []cli.Flag{outputFormatFlag, showDeletedFlag, listNameFlag, listExistedAtFlag, listIDFlag, showRecentlyDisconnected},
CustomHelpTemplate: commandHelpTemplate(),
}
}
func listTunnels(c *cli.Context) error {
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
cert, _, err := getOriginCertFromContext(c, logger)
func listCommand(c *cli.Context) error {
sc, err := newSubcommandContext(c)
if err != nil {
return err
}
client := newTunnelstoreClient(c, cert, logger)
allTunnels, err := client.ListTunnels()
if err != nil {
return errors.Wrap(err, "Error listing tunnels")
filter := tunnelstore.NewFilter()
if !c.Bool("show-deleted") {
filter.NoDeleted()
}
if name := c.String("name"); name != "" {
filter.ByName(name)
}
if existedAt := c.Timestamp("time"); existedAt != nil {
filter.ByExistedAt(*existedAt)
}
if id := c.String("id"); id != "" {
tunnelID, err := uuid.Parse(id)
if err != nil {
return errors.Wrapf(err, "%s is not a valid tunnel ID", id)
}
filter.ByTunnelID(tunnelID)
}
var tunnels []tunnelstore.Tunnel
if c.Bool("show-deleted") {
tunnels = allTunnels
} else {
for _, tunnel := range allTunnels {
if tunnel.DeletedAt.IsZero() {
tunnels = append(tunnels, tunnel)
}
}
tunnels, err := sc.list(filter)
if err != nil {
return err
}
if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" {
@@ -247,15 +202,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 +219,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
@@ -304,47 +264,32 @@ func fmtConnections(connections []tunnelstore.Connection) string {
func buildDeleteCommand() *cli.Command {
return &cli.Command{
Name: "delete",
Action: cliutil.ErrorHandler(deleteTunnel),
Usage: "Delete existing tunnel with given ID",
ArgsUsage: "TUNNEL-ID",
Hidden: hideSubcommands,
Flags: []cli.Flag{credentialsFileFlag},
Name: "delete",
Action: cliutil.ErrorHandler(deleteCommand),
Usage: "Delete existing tunnel by UUID or name",
UsageText: "cloudflared tunnel [tunnel command options] delete [subcommand options] TUNNEL",
Description: "cloudflared tunnel delete will delete tunnels with the given tunnel UUIDs or names. A tunnel cannot be deleted if it has active connections. To delete the tunnel unconditionally, use -f flag.",
Flags: []cli.Flag{credentialsFileFlag, forceDeleteFlag},
CustomHelpTemplate: commandHelpTemplate(),
}
}
func deleteTunnel(c *cli.Context) error {
if c.NArg() != 1 {
return cliutil.UsageError(`"cloudflared tunnel delete" requires exactly 1 argument, the ID of the tunnel to delete.`)
}
id := c.Args().First()
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
cert, _, err := getOriginCertFromContext(c, logger)
func deleteCommand(c *cli.Context) error {
sc, err := newSubcommandContext(c)
if err != nil {
return err
}
client := newTunnelstoreClient(c, cert, logger)
if err := client.DeleteTunnel(id); err != nil {
return errors.Wrapf(err, "Error deleting tunnel %s", id)
if c.NArg() < 1 {
return cliutil.UsageError(`"cloudflared tunnel delete" requires at least 1 argument, the ID or name of the tunnel to delete.`)
}
tunnelCredentialsPath, err := tunnelCredentialsPath(c, id, logger)
tunnelIDs, err := sc.findIDs(c.Args().Slice())
if err != nil {
logger.Infof("Cannot locate tunnel credentials to delete, error: %v. Please delete the file manually", err)
return nil
return err
}
if err = os.Remove(tunnelCredentialsPath); err != nil {
logger.Infof("Cannot delete tunnel credentials, error: %v. Please delete the file manually", err)
}
return nil
return sc.delete(tunnelIDs)
}
func renderOutput(format string, v interface{}) error {
@@ -360,99 +305,236 @@ 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,
}
flags = append(flags, configureProxyFlags(false)...)
return &cli.Command{
Name: "run",
Action: cliutil.ErrorHandler(runTunnel),
Action: cliutil.ErrorHandler(runCommand),
Before: SetFlagsFromConfigFile,
Usage: "Proxy a local web server by running the given tunnel",
ArgsUsage: "TUNNEL-ID",
Hidden: hideSubcommands,
Flags: []cli.Flag{forceFlag, credentialsFileFlag},
UsageText: "cloudflared tunnel [tunnel command options] run [subcommand options] [TUNNEL]",
Description: `Runs the tunnel identified by name or UUUD, creating highly available connections
between your server and the Cloudflare edge. You can provide name or UUID of tunnel to run either as the
last command line argument or in the configuration file using "tunnel: TUNNEL".
This command requires the tunnel credentials file created when "cloudflared tunnel create" was run,
however it does not need access to cert.pem from "cloudflared login" if you identify the tunnel by UUID.
If you experience other problems running the tunnel, "cloudflared tunnel cleanup" may help by removing
any old connection records.
`,
Flags: flags,
CustomHelpTemplate: commandHelpTemplate(),
}
}
func runTunnel(c *cli.Context) error {
if c.NArg() != 1 {
return cliutil.UsageError(`"cloudflared tunnel run" requires exactly 1 argument, the ID of the tunnel to run.`)
func runCommand(c *cli.Context) error {
sc, err := newSubcommandContext(c)
if err != nil {
return err
}
id := c.Args().First()
tunnelID, err := uuid.Parse(id)
if c.NArg() > 1 {
return cliutil.UsageError(`"cloudflared tunnel run" accepts only one argument, the ID or name of the tunnel to run.`)
}
tunnelRef := c.Args().First()
if tunnelRef == "" {
// see if tunnel id was in the config file
tunnelRef = config.GetConfiguration().TunnelID
if tunnelRef == "" {
return cliutil.UsageError(`"cloudflared tunnel run" requires the ID or name of the tunnel to run as the last command line argument or in the configuration file.`)
}
}
return runNamedTunnel(sc, tunnelRef)
}
func runNamedTunnel(sc *subcommandContext, tunnelRef string) error {
tunnelID, err := sc.findID(tunnelRef)
if err != nil {
return errors.Wrap(err, "error parsing tunnel ID")
}
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
sc.logger.Infof("Starting tunnel %s", tunnelID.String())
credentials, err := readTunnelCredentials(c, id, logger)
if err != nil {
return err
}
logger.Debugf("Read credentials for %v", credentials.AccountTag)
return StartServer(c, version, shutdownC, graceShutdownC, &origin.NamedTunnelConfig{Auth: *credentials, ID: tunnelID})
return sc.run(tunnelID)
}
func buildCleanupCommand() *cli.Command {
return &cli.Command{
Name: "cleanup",
Action: cliutil.ErrorHandler(cleanupConnections),
Usage: "Cleanup connections for the tunnel with given IDs",
ArgsUsage: "TUNNEL-IDS",
Hidden: hideSubcommands,
Name: "cleanup",
Action: cliutil.ErrorHandler(cleanupCommand),
Usage: "Cleanup tunnel connections",
UsageText: "cloudflared tunnel [tunnel command options] cleanup [subcommand options] TUNNEL",
Description: "Delete connections for tunnels with the given UUIDs or names.",
CustomHelpTemplate: commandHelpTemplate(),
}
}
func cleanupConnections(c *cli.Context) error {
func cleanupCommand(c *cli.Context) error {
if c.NArg() < 1 {
return cliutil.UsageError(`"cloudflared tunnel cleanup" requires at least 1 argument, the IDs of the tunnels to cleanup connections.`)
}
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
cert, _, err := getOriginCertFromContext(c, logger)
sc, err := newSubcommandContext(c)
if err != nil {
return err
}
client := newTunnelstoreClient(c, cert, logger)
for i := 0; i < c.NArg(); i++ {
id := c.Args().Get(i)
logger.Infof("Cleanup connection for tunnel %s", id)
if err := client.CleanupConnections(id); err != nil {
logger.Errorf("Error cleaning up connections for tunnel %s, error :%v", id, err)
}
tunnelIDs, err := sc.findIDs(c.Args().Slice())
if err != nil {
return err
}
return sc.cleanupConnections(tunnelIDs)
}
func buildRouteCommand() *cli.Command {
return &cli.Command{
Name: "route",
Action: cliutil.ErrorHandler(routeCommand),
Usage: "Define what hostname or load balancer can route to this tunnel",
UsageText: "cloudflared tunnel [tunnel command options] route [subcommand options] dns|lb TUNNEL HOSTNAME [LB-POOL]",
Description: `The route defines what hostname or load balancer will proxy requests to this tunnel.
To route a hostname by creating a CNAME to tunnel's address:
cloudflared tunnel route dns <tunnel ID> <hostname>
To use this tunnel as a load balancer origin, creating pool and load balancer if necessary:
cloudflared tunnel route lb <tunnel ID> <load balancer name> <load balancer pool>`,
CustomHelpTemplate: commandHelpTemplate(),
}
}
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
}
func commandHelpTemplate() string {
var parentFlagsHelp string
for _, f := range configureCloudflaredFlags(false) {
parentFlagsHelp += fmt.Sprintf(" %s\n\t", f)
}
for _, f := range configureLoggingFlags(false) {
parentFlagsHelp += fmt.Sprintf(" %s\n\t", f)
}
const template = `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{.UsageText}}
DESCRIPTION:
{{.Description}}
TUNNEL COMMAND OPTIONS:
%s
SUBCOMMAND OPTIONS:
{{range .VisibleFlags}}{{.}}
{{end}}
`
return fmt.Sprintf(template, parentFlagsHelp)
}
+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)
}
}
}
+243
View File
@@ -0,0 +1,243 @@
package ui
import (
"context"
"fmt"
"strings"
"time"
"github.com/cloudflare/cloudflared/ingress"
"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
localServices []string
connections []connState
}
type palette struct {
url string
connected string
defaultText string
disconnected string
reconnecting string
}
func NewUIModel(version, hostname, metricsURL string, ing *ingress.Ingress, haConnections int) *uiModel {
localServices := make([]string, len(ing.Rules))
for i, rule := range ing.Rules {
localServices[i] = rule.Service.String()
}
return &uiModel{
version: version,
edgeURL: hostname,
metricsURL: metricsURL,
localServices: localServices,
connections: make([]connState, haConnections),
}
}
func (data *uiModel) 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)
status := fmt.Sprintf("[%s]\u2022[%s] Proxying to [%s::b]%s", palette.connected, palette.defaultText, palette.url, strings.Join(data.localServices, ", "))
grid.AddItem(NewDynamicColorTextView().SetText(status), 1, 1, 1, 1, 0, 0, false)
grid.AddItem(connTable, 2, 1, 1, 1, 0, 0, false)
grid.AddItem(NewDynamicColorTextView().SetText(fmt.Sprintf("Metrics at [%s::b]http://%s/metrics", palette.url, data.metricsURL)), 3, 1, 1, 1, 0, 0, false)
// Add TextView to stream logs
// Logs are displayed in a new grid so a border can be set around them
logGrid := tview.NewGrid().SetBorders(true).AddItem(logTextView.SetChangedFunc(handleNewText(app, logTextView)), 0, 0, 5, 2, 0, 0, false)
// LogFrame holds the Logs header as well as the grid with the textView for streamed logs
logFrame := tview.NewFrame(logGrid).AddText("[::b]Logs:[::-]", true, tview.AlignLeft, tcell.ColorWhite).SetBorders(0, 0, 0, 0, 0, 0)
// Footer for log frame
logFrame.AddText("[::d]Use Ctrl+C to exit[::-]", false, tview.AlignRight, tcell.ColorWhite)
grid.AddItem(logFrame, 4, 0, 5, 2, 0, 0, false)
go func() {
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)
}
+26
View File
@@ -0,0 +1,26 @@
package updater
// Version is the functions needed to perform an update
type Version interface {
Apply() error
String() string
}
// Service is the functions to get check for new updates
type Service interface {
Check() (Version, error)
}
const (
// OSKeyName is the url parameter key to send to the checkin API for the operating system of the local cloudflared (e.g. windows, darwin, linux)
OSKeyName = "os"
// ArchitectureKeyName is the url parameter key to send to the checkin API for the architecture of the local cloudflared (e.g. amd64, x86)
ArchitectureKeyName = "arch"
// BetaKeyName is the url parameter key to send to the checkin API to signal if the update should be a beta version or not
BetaKeyName = "beta"
// VersionKeyName is the url parameter key to send to the checkin API to specific what version to upgrade or downgrade to
VersionKeyName = "version"
)
+62 -30
View File
@@ -8,12 +8,11 @@ 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"
"github.com/equinox-io/equinox"
"github.com/facebookgo/grace/gracenet"
"github.com/pkg/errors"
)
@@ -25,20 +24,16 @@ const (
noUpdateOnWindowsMessage = "cloudflared will not automatically update on Windows systems."
noUpdateManagedPackageMessage = "cloudflared will not automatically update if installed by a package manager."
isManagedInstallFile = ".installedFromPackageManager"
UpdateURL = "https://update.argotunnel.com"
StagingUpdateURL = "https://staging-update.argotunnel.com"
)
var (
publicKey = []byte(`
-----BEGIN ECDSA PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4OWZocTVZ8Do/L6ScLdkV+9A0IYMHoOf
dsCmJ/QZ6aw0w9qkkwEpne1Lmo6+0pGexZzFZOH6w5amShn+RXt7qkSid9iWlzGq
EKx0BZogHSor9Wy5VztdFaAaVbsJiCbO
-----END ECDSA PUBLIC KEY-----
`)
version string
)
// BinaryUpdated implements ExitCoder interface, the app will exit with status code 11
// https://pkg.go.dev/gopkg.in/urfave/cli.v2?tab=doc#ExitCoder
// https://pkg.go.dev/github.com/urfave/cli/v2?tab=doc#ExitCoder
type statusSuccess struct {
newVersion string
}
@@ -64,6 +59,13 @@ func (e *statusErr) ExitCode() int {
return 10
}
type updateOptions struct {
isBeta bool
isStaging bool
isForced bool
version string
}
type UpdateOutcome struct {
Updated bool
Version string
@@ -74,29 +76,44 @@ func (uo *UpdateOutcome) noUpdate() bool {
return uo.Error == nil && uo.Updated == false
}
func checkForUpdateAndApply() UpdateOutcome {
var opts equinox.Options
if err := opts.SetPublicKeyPEM(publicKey); err != nil {
return UpdateOutcome{Error: err}
}
func Init(v string) {
version = v
}
resp, err := equinox.Check(appID, opts)
switch {
case err == equinox.NotAvailableErr:
return UpdateOutcome{}
case err != nil:
return UpdateOutcome{Error: err}
}
err = resp.Apply()
func checkForUpdateAndApply(options updateOptions) UpdateOutcome {
cfdPath, err := os.Executable()
if err != nil {
return UpdateOutcome{Error: err}
}
return UpdateOutcome{Updated: true, Version: resp.ReleaseVersion}
url := UpdateURL
if options.isStaging {
url = StagingUpdateURL
}
s := NewWorkersService(version, url, cfdPath, Options{IsBeta: options.isBeta,
IsForced: options.isForced, RequestedVersion: options.version})
v, err := s.Check()
if err != nil {
return UpdateOutcome{Error: err}
}
//already on the latest version
if v == nil {
return UpdateOutcome{}
}
err = v.Apply()
if err != nil {
return UpdateOutcome{Error: err}
}
return UpdateOutcome{Updated: true, Version: v.String()}
}
func Update(_ *cli.Context) error {
// Update is the handler for the update command from the command line
func Update(c *cli.Context) error {
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
@@ -107,7 +124,22 @@ func Update(_ *cli.Context) error {
return nil
}
updateOutcome := loggedUpdate(logger)
isBeta := c.Bool("beta")
if isBeta {
logger.Info("cloudflared is set to update to the latest beta version")
}
isStaging := c.Bool("staging")
if isStaging {
logger.Info("cloudflared is set to update from staging")
}
isForced := c.Bool("force")
if isForced {
logger.Info("cloudflared is set to upgrade to the latest publish version regardless of the current version")
}
updateOutcome := loggedUpdate(logger, updateOptions{isBeta: isBeta, isStaging: isStaging, isForced: isForced, version: c.String("version")})
if updateOutcome.Error != nil {
return &statusErr{updateOutcome.Error}
}
@@ -121,8 +153,8 @@ func Update(_ *cli.Context) error {
}
// Checks for an update and applies it if one is available
func loggedUpdate(logger logger.Service) UpdateOutcome {
updateOutcome := checkForUpdateAndApply()
func loggedUpdate(logger logger.Service, options updateOptions) UpdateOutcome {
updateOutcome := checkForUpdateAndApply(options)
if updateOutcome.Updated {
logger.Infof("cloudflared has been updated to version %s", updateOutcome.Version)
}
@@ -168,7 +200,7 @@ func (a *AutoUpdater) Run(ctx context.Context) error {
ticker := time.NewTicker(a.configurable.freq)
for {
if a.configurable.enabled {
updateOutcome := loggedUpdate(a.logger)
updateOutcome := loggedUpdate(a.logger, updateOptions{})
if updateOutcome.Updated {
os.Args = append(os.Args, "--is-autoupdated=true")
if IsSysV() {
+153
View File
@@ -0,0 +1,153 @@
package updater
import (
"encoding/json"
"errors"
"net/http"
"runtime"
"strconv"
"strings"
)
// Options are the update options supported by the
type Options struct {
// IsBeta is for beta updates to be installed if available
IsBeta bool
// IsForced is to forcibly download the latest version regardless of the current version
IsForced bool
// RequestedVersion is the specific version to upgrade or downgrade to
RequestedVersion string
}
// VersionResponse is the JSON response from the Workers API endpoint
type VersionResponse struct {
URL string `json:"url"`
Version string `json:"version"`
Checksum string `json:"checksum"`
IsCompressed bool `json:"compressed"`
Error string `json:"error"`
}
// WorkersService implements Service.
// It contains everything needed to check in with the WorkersAPI and download and apply the updates
type WorkersService struct {
currentVersion string
url string
targetPath string
opts Options
}
// NewWorkersService creates a new updater Service object.
func NewWorkersService(currentVersion, url, targetPath string, opts Options) Service {
return &WorkersService{
currentVersion: currentVersion,
url: url,
targetPath: targetPath,
opts: opts,
}
}
// Check does a check in with the Workers API to get a new version update
func (s *WorkersService) Check() (Version, error) {
client := &http.Client{
Timeout: clientTimeout,
}
req, err := http.NewRequest(http.MethodGet, s.url, nil)
q := req.URL.Query()
q.Add(OSKeyName, runtime.GOOS)
q.Add(ArchitectureKeyName, runtime.GOARCH)
if s.opts.IsBeta {
q.Add(BetaKeyName, "true")
}
if s.opts.RequestedVersion != "" {
q.Add(VersionKeyName, s.opts.RequestedVersion)
}
req.URL.RawQuery = q.Encode()
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var v VersionResponse
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
return nil, err
}
if v.Error != "" {
return nil, errors.New(v.Error)
}
if !s.opts.IsForced && !IsNewerVersion(s.currentVersion, v.Version) {
return nil, nil
}
return NewWorkersVersion(v.URL, v.Version, v.Checksum, s.targetPath, v.IsCompressed), nil
}
// IsNewerVersion checks semantic versioning for the latest version
// cloudflared tagging is more of a date than a semantic version,
// but the same comparision logic still holds for major.minor.patch
// e.g. 2020.8.2 is newer than 2020.8.1.
func IsNewerVersion(current string, check string) bool {
if strings.Contains(strings.ToLower(current), "dev") {
return false // dev builds shouldn't update
}
cMajor, cMinor, cPatch, err := SemanticParts(current)
if err != nil {
return false
}
nMajor, nMinor, nPatch, err := SemanticParts(check)
if err != nil {
return false
}
if nMajor > cMajor {
return true
}
if nMajor == cMajor && nMinor > cMinor {
return true
}
if nMajor == cMajor && nMinor == cMinor && nPatch > cPatch {
return true
}
return false
}
// SemanticParts gets the major, minor, and patch version of a semantic version string
// e.g. 3.1.2 would return 3, 1, 2, nil
func SemanticParts(version string) (major int, minor int, patch int, err error) {
major = 0
minor = 0
patch = 0
parts := strings.Split(version, ".")
if len(parts) != 3 {
err = errors.New("invalid version")
return
}
major, err = strconv.Atoi(parts[0])
if err != nil {
return
}
minor, err = strconv.Atoi(parts[1])
if err != nil {
return
}
patch, err = strconv.Atoi(parts[2])
if err != nil {
return
}
return
}
@@ -0,0 +1,273 @@
package updater
import (
"archive/tar"
"bytes"
"compress/gzip"
"crypto/sha256"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
"os"
"runtime"
"testing"
"github.com/stretchr/testify/require"
)
func respondWithJSON(w http.ResponseWriter, v interface{}, status int) {
data, _ := json.Marshal(v)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
w.Write(data)
}
func respondWithData(w http.ResponseWriter, b []byte, status int) {
w.Header().Set("Content-Type", "application/octet-stream")
w.WriteHeader(status)
w.Write(b)
}
func updateHandler(w http.ResponseWriter, r *http.Request) {
version := "2020.08.05"
host := fmt.Sprintf("http://%s", r.Host)
url := host + "/download"
query := r.URL.Query()
if query.Get(BetaKeyName) == "true" {
version = "2020.08.06"
url = host + "/beta"
}
requestedVersion := query.Get(VersionKeyName)
if requestedVersion != "" {
version = requestedVersion
url = fmt.Sprintf("%s?version=%s", url, requestedVersion)
}
if query.Get(ArchitectureKeyName) != runtime.GOARCH || query.Get(OSKeyName) != runtime.GOOS {
respondWithJSON(w, VersionResponse{Error: "unsupported os and architecture"}, http.StatusBadRequest)
return
}
h := sha256.New()
fmt.Fprint(h, version)
checksum := fmt.Sprintf("%x", h.Sum(nil))
v := VersionResponse{URL: url, Version: version, Checksum: checksum}
respondWithJSON(w, v, http.StatusOK)
}
func gzipUpdateHandler(w http.ResponseWriter, r *http.Request) {
log.Println("got a request!")
version := "2020.09.02"
h := sha256.New()
fmt.Fprint(h, version)
checksum := fmt.Sprintf("%x", h.Sum(nil))
url := fmt.Sprintf("http://%s/gzip-download.tgz", r.Host)
v := VersionResponse{URL: url, Version: version, Checksum: checksum}
respondWithJSON(w, v, http.StatusOK)
}
func compressedDownloadHandler(w http.ResponseWriter, r *http.Request) {
version := "2020.09.02"
buf := new(bytes.Buffer)
gw := gzip.NewWriter(buf)
tw := tar.NewWriter(gw)
header := &tar.Header{
Size: int64(len(version)),
Name: "download",
}
tw.WriteHeader(header)
tw.Write([]byte(version))
tw.Close()
gw.Close()
respondWithData(w, buf.Bytes(), http.StatusOK)
}
func downloadHandler(w http.ResponseWriter, r *http.Request) {
version := "2020.08.05"
requestedVersion := r.URL.Query().Get(VersionKeyName)
if requestedVersion != "" {
version = requestedVersion
}
respondWithData(w, []byte(version), http.StatusOK)
}
func betaHandler(w http.ResponseWriter, r *http.Request) {
respondWithData(w, []byte("2020.08.06"), http.StatusOK)
}
func failureHandler(w http.ResponseWriter, r *http.Request) {
respondWithJSON(w, VersionResponse{Error: "unsupported os and architecture"}, http.StatusBadRequest)
}
func createServer() *httptest.Server {
mux := http.NewServeMux()
mux.HandleFunc("/updater", updateHandler)
mux.HandleFunc("/download", downloadHandler)
mux.HandleFunc("/beta", betaHandler)
mux.HandleFunc("/fail", failureHandler)
mux.HandleFunc("/compressed", gzipUpdateHandler)
mux.HandleFunc("/gzip-download.tgz", compressedDownloadHandler)
return httptest.NewServer(mux)
}
func createTestFile(t *testing.T, path string) {
f, err := os.Create("tmpfile")
require.NoError(t, err)
fmt.Fprint(f, "2020.08.04")
f.Close()
}
func TestUpdateService(t *testing.T) {
ts := createServer()
defer ts.Close()
testFilePath := "tmpfile"
createTestFile(t, testFilePath)
defer os.Remove(testFilePath)
log.Println("server url: ", ts.URL)
s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{})
v, err := s.Check()
require.NoError(t, err)
require.Equal(t, v.String(), "2020.08.05")
require.NoError(t, v.Apply())
dat, err := ioutil.ReadFile(testFilePath)
require.NoError(t, err)
require.Equal(t, string(dat), "2020.08.05")
}
func TestBetaUpdateService(t *testing.T) {
ts := createServer()
defer ts.Close()
testFilePath := "tmpfile"
createTestFile(t, testFilePath)
defer os.Remove(testFilePath)
s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{IsBeta: true})
v, err := s.Check()
require.NoError(t, err)
require.Equal(t, v.String(), "2020.08.06")
require.NoError(t, v.Apply())
dat, err := ioutil.ReadFile(testFilePath)
require.NoError(t, err)
require.Equal(t, string(dat), "2020.08.06")
}
func TestFailUpdateService(t *testing.T) {
ts := createServer()
defer ts.Close()
testFilePath := "tmpfile"
createTestFile(t, testFilePath)
defer os.Remove(testFilePath)
s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/fail", ts.URL), testFilePath, Options{})
v, err := s.Check()
require.Error(t, err)
require.Nil(t, v)
}
func TestNoUpdateService(t *testing.T) {
ts := createServer()
defer ts.Close()
testFilePath := "tmpfile"
createTestFile(t, testFilePath)
defer os.Remove(testFilePath)
s := NewWorkersService("2020.8.5", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{})
v, err := s.Check()
require.NoError(t, err)
require.Nil(t, v)
}
func TestForcedUpdateService(t *testing.T) {
ts := createServer()
defer ts.Close()
testFilePath := "tmpfile"
createTestFile(t, testFilePath)
defer os.Remove(testFilePath)
s := NewWorkersService("2020.8.5", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{IsForced: true})
v, err := s.Check()
require.NoError(t, err)
require.Equal(t, v.String(), "2020.08.05")
require.NoError(t, v.Apply())
dat, err := ioutil.ReadFile(testFilePath)
require.NoError(t, err)
require.Equal(t, string(dat), "2020.08.05")
}
func TestUpdateSpecificVersionService(t *testing.T) {
ts := createServer()
defer ts.Close()
testFilePath := "tmpfile"
createTestFile(t, testFilePath)
defer os.Remove(testFilePath)
reqVersion := "2020.9.1"
s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{RequestedVersion: reqVersion})
v, err := s.Check()
require.NoError(t, err)
require.Equal(t, reqVersion, v.String())
require.NoError(t, v.Apply())
dat, err := ioutil.ReadFile(testFilePath)
require.NoError(t, err)
require.Equal(t, reqVersion, string(dat))
}
func TestCompressedUpdateService(t *testing.T) {
ts := createServer()
defer ts.Close()
testFilePath := "tmpfile"
createTestFile(t, testFilePath)
defer os.Remove(testFilePath)
s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/compressed", ts.URL), testFilePath, Options{})
v, err := s.Check()
require.NoError(t, err)
require.Equal(t, "2020.09.02", v.String())
require.NoError(t, v.Apply())
dat, err := ioutil.ReadFile(testFilePath)
require.NoError(t, err)
require.Equal(t, "2020.09.02", string(dat))
}
func TestVersionParsing(t *testing.T) {
require.False(t, IsNewerVersion("2020.8.2", "2020.8.2"))
require.True(t, IsNewerVersion("2020.8.2", "2020.8.3"))
require.True(t, IsNewerVersion("2020.8.2", "2021.1.2"))
require.True(t, IsNewerVersion("2020.8.2", "2020.9.1"))
require.True(t, IsNewerVersion("2020.8.2", "2020.12.45"))
require.False(t, IsNewerVersion("2020.8.2", "2020.6.3"))
require.False(t, IsNewerVersion("DEV", "2020.8.5"))
require.False(t, IsNewerVersion("2020.8.2", "asdlkfjasdf"))
require.True(t, IsNewerVersion("3.0.1", "4.2.1"))
}
+230
View File
@@ -0,0 +1,230 @@
package updater
import (
"archive/tar"
"compress/gzip"
"crypto/sha256"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"text/template"
"time"
)
const (
clientTimeout = time.Second * 60
// stop the service
// rename cloudflared.exe to cloudflared.exe.old
// rename cloudflared.exe.new to cloudflared.exe
// delete cloudflared.exe.old
// start the service
// delete the batch file
windowsUpdateCommandTemplate = `@echo off
sc stop cloudflared >nul 2>&1
rename "{{.TargetPath}}" {{.OldName}}
rename "{{.NewPath}}" {{.BinaryName}}
del "{{.OldPath}}"
sc start cloudflared >nul 2>&1
del {{.BatchName}}`
batchFileName = "cfd_update.bat"
)
// Prepare some data to insert into the template.
type batchData struct {
TargetPath string
OldName string
NewPath string
OldPath string
BinaryName string
BatchName string
}
// WorkersVersion implements the Version interface.
// It contains everything needed to preform a version upgrade
type WorkersVersion struct {
downloadURL string
checksum string
version string
targetPath string
isCompressed bool
}
// NewWorkersVersion creates a new Version object. This is normally created by a WorkersService JSON checkin response
// url is where to download the file
// version is the version of this update
// checksum is the expected checksum of the downloaded file
// target path is where the file should be replace. Normally this the running cloudflared's path
func NewWorkersVersion(url, version, checksum, targetPath string, isCompressed bool) Version {
return &WorkersVersion{downloadURL: url, version: version, checksum: checksum, targetPath: targetPath, isCompressed: isCompressed}
}
// Apply does the actual verification and update logic.
// This includes signature and checksum validation,
// replacing the binary, etc
func (v *WorkersVersion) Apply() error {
newFilePath := fmt.Sprintf("%s.new", v.targetPath)
os.Remove(newFilePath) //remove any failed updates before download
// download the file
if err := download(v.downloadURL, newFilePath, v.isCompressed); err != nil {
return err
}
// check that the file is what is expected
if err := isValidChecksum(v.checksum, newFilePath); err != nil {
return err
}
oldFilePath := fmt.Sprintf("%s.old", v.targetPath)
// Windows requires more effort to self update, especially when it is running as a service:
// you have to stop the service (if running as one) in order to move/rename the binary
// but now the binary isn't running though, so an external process
// has to move the old binary out and the new one in then start the service
// the easiest way to do this is with a batch file (or with a DLL, but that gets ugly for a cross compiled binary like cloudflared)
// a batch file isn't ideal, but it is the simplest path forward for the constraints Windows creates
if runtime.GOOS == "windows" {
if err := writeBatchFile(v.targetPath, newFilePath, oldFilePath); err != nil {
return err
}
rootDir := filepath.Dir(v.targetPath)
batchPath := filepath.Join(rootDir, batchFileName)
return runWindowsBatch(batchPath)
}
// now move the current file out, move the new file in and delete the old file
if err := os.Rename(v.targetPath, oldFilePath); err != nil {
return err
}
if err := os.Rename(newFilePath, v.targetPath); err != nil {
//attempt rollback
os.Rename(oldFilePath, v.targetPath)
return err
}
os.Remove(oldFilePath)
return nil
}
// String returns the version number of this update/release (e.g. 2020.08.05)
func (v *WorkersVersion) String() string {
return v.version
}
// download the file from the link in the json
func download(url, filepath string, isCompressed bool) error {
client := &http.Client{
Timeout: clientTimeout,
}
resp, err := client.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
var r io.Reader
r = resp.Body
// compressed macos binary, need to decompress
if isCompressed || isCompressedFile(url) {
// first the gzip reader
gr, err := gzip.NewReader(resp.Body)
if err != nil {
return err
}
defer gr.Close()
// now the tar
tr := tar.NewReader(gr)
// advance the reader pass the header, which will be the single binary file
tr.Next()
r = tr
}
out, err := os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, r)
return err
}
// isCompressedFile is a really simple file extension check to see if this is a macos tar and gzipped
func isCompressedFile(urlstring string) bool {
if strings.HasSuffix(urlstring, ".tgz") {
return true
}
u, err := url.Parse(urlstring)
if err != nil {
return false
}
return strings.HasSuffix(u.Path, ".tgz")
}
// checks if the checksum in the json response matches the checksum of the file download
func isValidChecksum(checksum, filePath string) error {
f, err := os.Open(filePath)
if err != nil {
return err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return err
}
hash := fmt.Sprintf("%x", h.Sum(nil))
if checksum != hash {
return errors.New("checksum validation failed")
}
return nil
}
// writeBatchFile writes a batch file out to disk
// see the dicussion on why it has to be done this way
func writeBatchFile(targetPath string, newPath string, oldPath string) error {
os.Remove(batchFileName) //remove any failed updates before download
f, err := os.Create(batchFileName)
if err != nil {
return err
}
defer f.Close()
cfdName := filepath.Base(targetPath)
oldName := filepath.Base(oldPath)
data := batchData{
TargetPath: targetPath,
OldName: oldName,
NewPath: newPath,
OldPath: oldPath,
BinaryName: cfdName,
BatchName: batchFileName,
}
t, err := template.New("batch").Parse(windowsUpdateCommandTemplate)
if err != nil {
return err
}
return t.Execute(f, data)
}
// run each OS command for windows
func runWindowsBatch(batchFile string) error {
cmd := exec.Command("cmd", "/c", batchFile)
return cmd.Start()
}
+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))
}
+17 -25
View File
@@ -2,48 +2,40 @@ package connection
import (
"context"
"fmt"
"time"
"io"
rpc "zombiezen.com/go/capnproto2/rpc"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tunnelrpc"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
// NewRPCClient creates and returns a new RPC client, which will communicate
// NewTunnelRPCClient creates and returns a new RPC client, which will communicate
// using a stream on the given muxer
func NewRPCClient(
func NewTunnelRPCClient(
ctx context.Context,
muxer *h2mux.Muxer,
stream io.ReadWriteCloser,
logger logger.Service,
openStreamTimeout time.Duration,
) (client tunnelpogs.TunnelServer_PogsClient, err error) {
openStreamCtx, openStreamCancel := context.WithTimeout(ctx, openStreamTimeout)
defer openStreamCancel()
stream, err := muxer.OpenRPCStream(openStreamCtx)
if err != nil {
return
}
if !isRPCStreamResponse(stream.Headers) {
stream.Close()
err = fmt.Errorf("rpc: bad response headers: %v", stream.Headers)
return
}
conn := rpc.NewConn(
tunnelrpc.NewTransportLogger(logger, rpc.StreamTransport(stream)),
tunnelrpc.ConnLog(logger),
)
client = tunnelpogs.TunnelServer_PogsClient{Client: conn.Bootstrap(ctx), Conn: conn}
registrationClient := tunnelpogs.RegistrationServer_PogsClient{Client: conn.Bootstrap(ctx), Conn: conn}
client = tunnelpogs.TunnelServer_PogsClient{RegistrationServer_PogsClient: registrationClient, Client: conn.Bootstrap(ctx), Conn: conn}
return client, nil
}
func isRPCStreamResponse(headers []h2mux.Header) bool {
return len(headers) == 1 &&
headers[0].Name == ":status" &&
headers[0].Value == "200"
func NewRegistrationRPCClient(
ctx context.Context,
stream io.ReadWriteCloser,
logger logger.Service,
) (client tunnelpogs.RegistrationServer_PogsClient, err error) {
conn := rpc.NewConn(
tunnelrpc.NewTransportLogger(logger, rpc.StreamTransport(stream)),
tunnelrpc.ConnLog(logger),
)
client = tunnelpogs.RegistrationServer_PogsClient{Client: conn.Bootstrap(ctx), Conn: conn}
return client, nil
}
+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) {
+6 -1
View File
@@ -1,4 +1,9 @@
FROM golang:1.12 as builder
FROM golang:1.15.3 as builder
ENV GO111MODULE=on \
CGO_ENABLED=0
WORKDIR /go/src/github.com/cloudflare/cloudflared/
RUN apt-get update
COPY . .
# compile cloudflared
RUN make cloudflared
RUN cp /go/src/github.com/cloudflare/cloudflared/cloudflared /usr/local/bin/
+6 -6
View File
@@ -121,15 +121,15 @@ func resolveSRVToTCP(srv *net.SRV) ([]*net.TCPAddr, error) {
}
// ResolveAddrs resolves TCP address given a list of addresses. Address can be a hostname, however, it will return at most one
// of the hostname's IP addresses
func ResolveAddrs(addrs []string) ([]*net.TCPAddr, error) {
var tcpAddrs []*net.TCPAddr
// of the hostname's IP addresses.
func ResolveAddrs(addrs []string, logger logger.Service) (resolved []*net.TCPAddr) {
for _, addr := range addrs {
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
return nil, err
logger.Errorf("Failed to resolve %s, err: %v", addr, err)
} else {
resolved = append(resolved, tcpAddr)
}
tcpAddrs = append(tcpAddrs, tcpAddr)
}
return tcpAddrs, nil
return
}
+5 -5
View File
@@ -35,12 +35,12 @@ func ResolveEdge(logger logger.Service) (*Regions, error) {
// StaticEdge creates a list of edge addresses from the list of hostnames.
// Mainly used for testing connectivity.
func StaticEdge(hostnames []string) (*Regions, error) {
addrs, err := ResolveAddrs(hostnames)
if err != nil {
return nil, err
func StaticEdge(hostnames []string, logger logger.Service) (*Regions, error) {
resolved := ResolveAddrs(hostnames, logger)
if len(resolved) == 0 {
return nil, fmt.Errorf("failed to resolve any edge address")
}
return NewNoResolve(addrs), nil
return NewNoResolve(resolved), nil
}
// NewNoResolve doesn't resolve the edge. Instead it just uses the given addresses.
+1 -1
View File
@@ -41,7 +41,7 @@ func ResolveEdge(l logger.Service) (*Edge, error) {
// StaticEdge creates a list of edge addresses from the list of hostnames. Mainly used for testing connectivity.
func StaticEdge(l logger.Service, hostnames []string) (*Edge, error) {
regions, err := allregions.StaticEdge(hostnames)
regions, err := allregions.StaticEdge(hostnames, l)
if err != nil {
return new(Edge), err
}
+118
View File
@@ -0,0 +1,118 @@
#!/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 filepath in glob.glob("artifacts/*"):
pkg_hash = get_sha256(filepath)
# add the sha256 of the new artifact to the release message body
name = os.path.basename(filepath)
msg = update_or_add_message(msg, name, 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()
+85
View File
@@ -6,6 +6,10 @@ Creates Github Releases and uploads assets
import argparse
import logging
import os
import shutil
import hashlib
import requests
import tarfile
from github import Github, GithubException, UnknownObjectException
@@ -14,6 +18,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 +112,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 +142,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 +175,36 @@ def main():
return
release.upload_asset(args.path, name=args.name)
# check and extract if the file is a tar and gzipped file (as is the case with the macos builds)
binary_path = args.path
if binary_path.endswith("tgz"):
try:
shutil.rmtree('cfd')
except OSError as e:
pass
zipfile = tarfile.open(binary_path, "r:gz")
zipfile.extractall('cfd') # specify which folder to extract to
zipfile.close()
binary_path = os.path.join(os.getcwd(), 'cfd', 'cloudflared')
# send the sha256 (the checksum) to workers kv
pkg_hash = get_sha256(binary_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)
try:
shutil.copy(args.path, copy_path)
except shutil.SameFileError:
pass # the macOS release copy fails with being the same file (already in the artifacts directory). Catching to ignore.
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
+1
View File
@@ -19,6 +19,7 @@ var (
ErrUnexpectedFrameType = MuxerProtocolError{"2001 unexpected frame type", http2.ErrCodeProtocol}
ErrUnknownStream = MuxerProtocolError{"2002 unknown stream", http2.ErrCodeProtocol}
ErrInvalidStream = MuxerProtocolError{"2003 invalid stream", http2.ErrCodeProtocol}
ErrNotRPCStream = MuxerProtocolError{"2004 not RPC stream", http2.ErrCodeProtocol}
ErrStreamHeadersSent = MuxerApplicationError{"3000 headers already sent"}
ErrStreamRequestConnectionClosed = MuxerApplicationError{"3001 connection closed while opening stream"}
+19 -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,
@@ -439,11 +441,17 @@ func (m *Muxer) OpenStream(ctx context.Context, headers []Header, body io.Reader
func (m *Muxer) OpenRPCStream(ctx context.Context) (*MuxedStream, error) {
stream := m.NewStream(RPCHeaders())
if err := m.MakeMuxedStreamRequest(ctx, NewMuxedStreamRequest(stream, nil)); err != nil {
stream.Close()
return nil, err
}
if err := m.AwaitResponseHeaders(ctx, stream); err != nil {
stream.Close()
return nil, err
}
if !IsRPCStreamResponse(stream) {
stream.Close()
return nil, ErrNotRPCStream
}
return stream, nil
}
@@ -497,3 +505,10 @@ func (m *Muxer) abort() {
func (m *Muxer) TimerRetries() uint64 {
return m.muxWriter.idleTimer.RetryCount()
}
func IsRPCStreamResponse(stream *MuxedStream) bool {
headers := stream.Headers
return len(headers) == 1 &&
headers[0].Name == ":status" &&
headers[0].Value == "200"
}
+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 {
+7
View File
@@ -150,8 +150,15 @@ func uptimeHandler(startTime time.Time) http.HandlerFunc {
// This handler will echo message
func websocketHandler(logger logger.Service, upgrader websocket.Upgrader) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// This addresses the issue of r.Host includes port but origin header doesn't
host, _, err := net.SplitHostPort(r.Host)
if err == nil {
r.Host = host
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
logger.Errorf("failed to upgrade to websocket connection, error: %s", err)
return
}
defer conn.Close()
+231
View File
@@ -0,0 +1,231 @@
package ingress
import (
"fmt"
"net"
"net/url"
"regexp"
"strconv"
"strings"
"sync"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
)
var (
ErrNoIngressRules = errors.New("No ingress rules were specified in the config file")
errLastRuleNotCatchAll = errors.New("The last ingress rule must match all hostnames (i.e. it must be missing, or must be \"*\")")
errBadWildcard = errors.New("Hostname patterns can have at most one wildcard character (\"*\") and it can only be used for subdomains, e.g. \"*.example.com\"")
errHostnameContainsPort = errors.New("Hostname cannot contain port")
ErrURLIncompatibleWithIngress = errors.New("You can't set the --url flag (or $TUNNEL_URL) when using multiple-origin ingress rules")
)
// FindMatchingRule returns the index of the Ingress Rule which matches the given
// hostname and path. This function assumes the last rule matches everything,
// which is the case if the rules were instantiated via the ingress#Validate method
func (ing Ingress) FindMatchingRule(hostname, path string) (*Rule, int) {
// The hostname might contain port. We only want to compare the host part with the rule
host, _, err := net.SplitHostPort(hostname)
if err == nil {
hostname = host
}
for i, rule := range ing.Rules {
if rule.Matches(hostname, path) {
return &rule, i
}
}
i := len(ing.Rules) - 1
return &ing.Rules[i], i
}
func matchHost(ruleHost, reqHost string) bool {
if ruleHost == reqHost {
return true
}
// Validate hostnames that use wildcards at the start
if strings.HasPrefix(ruleHost, "*.") {
toMatch := strings.TrimPrefix(ruleHost, "*.")
return strings.HasSuffix(reqHost, toMatch)
}
return false
}
// Ingress maps eyeball requests to origins.
type Ingress struct {
Rules []Rule
defaults OriginRequestConfig
}
// NewSingleOrigin constructs an Ingress set with only one rule, constructed from
// legacy CLI parameters like --url or --no-chunked-encoding.
func NewSingleOrigin(c *cli.Context, compatibilityMode bool, logger logger.Service) (Ingress, error) {
service, err := parseSingleOriginService(c, compatibilityMode)
if err != nil {
return Ingress{}, err
}
// Construct an Ingress with the single rule.
ing := Ingress{
Rules: []Rule{
{
Service: service,
},
},
defaults: originRequestFromSingeRule(c),
}
return ing, err
}
// Get a single origin service from the CLI/config.
func parseSingleOriginService(c *cli.Context, compatibilityMode bool) (OriginService, error) {
if c.IsSet("hello-world") {
return new(helloWorld), nil
}
if c.IsSet("url") {
originURLStr, err := config.ValidateUrl(c, compatibilityMode)
if err != nil {
return nil, errors.Wrap(err, "Error validating origin URL")
}
originURL, err := url.Parse(originURLStr)
if err != nil {
return nil, errors.Wrap(err, "couldn't parse origin URL")
}
return &localService{URL: originURL, RootURL: originURL}, nil
}
if c.IsSet("unix-socket") {
path, err := config.ValidateUnixSocket(c)
if err != nil {
return nil, errors.Wrap(err, "Error validating --unix-socket")
}
return &unixSocketPath{path: path}, nil
}
return nil, errors.New("You must either set ingress rules in your config file, or use --url or use --unix-socket")
}
// IsEmpty checks if there are any ingress rules.
func (ing Ingress) IsEmpty() bool {
return len(ing.Rules) == 0
}
// StartOrigins will start any origin services managed by cloudflared, e.g. proxy servers or Hello World.
func (ing Ingress) StartOrigins(wg *sync.WaitGroup, log logger.Service, shutdownC <-chan struct{}, errC chan error) {
for _, rule := range ing.Rules {
if err := rule.Service.start(wg, log, shutdownC, errC, rule.Config); err != nil {
log.Errorf("Error starting local service %s: %s", rule.Service, err)
}
}
}
// CatchAll returns the catch-all rule (i.e. the last rule)
func (ing Ingress) CatchAll() *Rule {
return &ing.Rules[len(ing.Rules)-1]
}
func validate(ingress []config.UnvalidatedIngressRule, defaults OriginRequestConfig) (Ingress, error) {
rules := make([]Rule, len(ingress))
for i, r := range ingress {
var service OriginService
if prefix := "unix:"; strings.HasPrefix(r.Service, prefix) {
// No validation necessary for unix socket filepath services
path := strings.TrimPrefix(r.Service, prefix)
service = &unixSocketPath{path: path}
} else if prefix := "http_status:"; strings.HasPrefix(r.Service, prefix) {
status, err := strconv.Atoi(strings.TrimPrefix(r.Service, prefix))
if err != nil {
return Ingress{}, errors.Wrap(err, "invalid HTTP status")
}
srv := newStatusCode(status)
service = &srv
} else if r.Service == "hello_world" || r.Service == "hello-world" || r.Service == "helloworld" {
service = new(helloWorld)
} else {
// Validate URL services
u, err := url.Parse(r.Service)
if err != nil {
return Ingress{}, err
}
if u.Scheme == "" || u.Hostname() == "" {
return Ingress{}, fmt.Errorf("The service %s must have a scheme and a hostname", r.Service)
}
if u.Path != "" {
return Ingress{}, fmt.Errorf("%s is an invalid address, ingress rules don't support proxying to a different path on the origin service. The path will be the same as the eyeball request's path", r.Service)
}
serviceURL := localService{URL: u}
service = &serviceURL
}
if err := validateHostname(r, i, len(ingress)); err != nil {
return Ingress{}, err
}
var pathRegex *regexp.Regexp
if r.Path != "" {
var err error
pathRegex, err = regexp.Compile(r.Path)
if err != nil {
return Ingress{}, errors.Wrapf(err, "Rule #%d has an invalid regex", i+1)
}
}
rules[i] = Rule{
Hostname: r.Hostname,
Service: service,
Path: pathRegex,
Config: setConfig(defaults, r.OriginRequest),
}
}
return Ingress{Rules: rules, defaults: defaults}, nil
}
func validateHostname(r config.UnvalidatedIngressRule, ruleIndex, totalRules int) error {
// Ensure that the hostname doesn't contain port
_, _, err := net.SplitHostPort(r.Hostname)
if err == nil {
return errHostnameContainsPort
}
// Ensure that there are no wildcards anywhere except the first character
// of the hostname.
if strings.LastIndex(r.Hostname, "*") > 0 {
return errBadWildcard
}
// The last rule should catch all hostnames.
isCatchAllRule := (r.Hostname == "" || r.Hostname == "*") && r.Path == ""
isLastRule := ruleIndex == totalRules-1
if isLastRule && !isCatchAllRule {
return errLastRuleNotCatchAll
}
// ONLY the last rule should catch all hostnames.
if !isLastRule && isCatchAllRule {
return errRuleShouldNotBeCatchAll{index: ruleIndex, hostname: r.Hostname}
}
return nil
}
type errRuleShouldNotBeCatchAll struct {
index int
hostname string
}
func (e errRuleShouldNotBeCatchAll) Error() string {
return fmt.Sprintf("Rule #%d is matching the hostname '%s', but "+
"this will match every hostname, meaning the rules which follow it "+
"will never be triggered.", e.index+1, e.hostname)
}
// ParseIngress parses ingress rules, but does not send HTTP requests to the origins.
func ParseIngress(conf *config.Configuration) (Ingress, error) {
if len(conf.Ingress) == 0 {
return Ingress{}, ErrNoIngressRules
}
return validate(conf.Ingress, originRequestFromYAML(conf.OriginRequest))
}
+332
View File
@@ -0,0 +1,332 @@
package ingress
import (
"fmt"
"net/url"
"regexp"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
)
func TestParseUnixSocket(t *testing.T) {
rawYAML := `
ingress:
- service: unix:/tmp/echo.sock
`
ing, err := ParseIngress(MustReadIngress(rawYAML))
require.NoError(t, err)
_, ok := ing.Rules[0].Service.(*unixSocketPath)
require.True(t, ok)
}
func Test_parseIngress(t *testing.T) {
localhost8000 := MustParseURL(t, "https://localhost:8000")
localhost8001 := MustParseURL(t, "https://localhost:8001")
fourOhFour := newStatusCode(404)
defaultConfig := setConfig(originRequestFromYAML(config.OriginRequestConfig{}), config.OriginRequestConfig{})
require.Equal(t, defaultKeepAliveConnections, defaultConfig.KeepAliveConnections)
type args struct {
rawYAML string
}
tests := []struct {
name string
args args
want []Rule
wantErr bool
}{
{
name: "Empty file",
args: args{rawYAML: ""},
wantErr: true,
},
{
name: "Multiple rules",
args: args{rawYAML: `
ingress:
- hostname: tunnel1.example.com
service: https://localhost:8000
- hostname: "*"
service: https://localhost:8001
`},
want: []Rule{
{
Hostname: "tunnel1.example.com",
Service: &localService{URL: localhost8000},
Config: defaultConfig,
},
{
Hostname: "*",
Service: &localService{URL: localhost8001},
Config: defaultConfig,
},
},
},
{
name: "Extra keys",
args: args{rawYAML: `
ingress:
- hostname: "*"
service: https://localhost:8000
extraKey: extraValue
`},
want: []Rule{
{
Hostname: "*",
Service: &localService{URL: localhost8000},
Config: defaultConfig,
},
},
},
{
name: "Hostname can be omitted",
args: args{rawYAML: `
ingress:
- service: https://localhost:8000
`},
want: []Rule{
{
Service: &localService{URL: localhost8000},
Config: defaultConfig,
},
},
},
{
name: "Invalid service",
args: args{rawYAML: `
ingress:
- hostname: "*"
service: https://local host:8000
`},
wantErr: true,
},
{
name: "Last rule isn't catchall",
args: args{rawYAML: `
ingress:
- hostname: example.com
service: https://localhost:8000
`},
wantErr: true,
},
{
name: "First rule is catchall",
args: args{rawYAML: `
ingress:
- service: https://localhost:8000
- hostname: example.com
service: https://localhost:8000
`},
wantErr: true,
},
{
name: "Catch-all rule can't have a path",
args: args{rawYAML: `
ingress:
- service: https://localhost:8001
path: /subpath1/(.*)/subpath2
`},
wantErr: true,
},
{
name: "Invalid regex",
args: args{rawYAML: `
ingress:
- hostname: example.com
service: https://localhost:8000
path: "*/subpath2"
- service: https://localhost:8001
`},
wantErr: true,
},
{
name: "Service must have a scheme",
args: args{rawYAML: `
ingress:
- service: localhost:8000
`},
wantErr: true,
},
{
name: "Wildcard not at start",
args: args{rawYAML: `
ingress:
- hostname: "test.*.example.com"
service: https://localhost:8000
`},
wantErr: true,
},
{
name: "Service can't have a path",
args: args{rawYAML: `
ingress:
- service: https://localhost:8000/static/
`},
wantErr: true,
},
{
name: "Invalid HTTP status",
args: args{rawYAML: `
ingress:
- service: http_status:asdf
`},
wantErr: true,
},
{
name: "Valid HTTP status",
args: args{rawYAML: `
ingress:
- service: http_status:404
`},
want: []Rule{
{
Hostname: "",
Service: &fourOhFour,
Config: defaultConfig,
},
},
},
{
name: "Valid hello world service",
args: args{rawYAML: `
ingress:
- service: hello_world
`},
want: []Rule{
{
Hostname: "",
Service: new(helloWorld),
Config: defaultConfig,
},
},
},
{
name: "Hostname contains port",
args: args{rawYAML: `
ingress:
- hostname: "test.example.com:443"
service: https://localhost:8000
- hostname: "*"
service: https://localhost:8001
`},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseIngress(MustReadIngress(tt.args.rawYAML))
if (err != nil) != tt.wantErr {
t.Errorf("ParseIngress() error = %v, wantErr %v", err, tt.wantErr)
return
}
require.Equal(t, tt.want, got.Rules)
})
}
}
func TestFindMatchingRule(t *testing.T) {
ingress := Ingress{
Rules: []Rule{
{
Hostname: "tunnel-a.example.com",
Path: nil,
},
{
Hostname: "tunnel-b.example.com",
Path: mustParsePath(t, "/health"),
},
{
Hostname: "*",
},
},
}
tests := []struct {
host string
path string
wantRuleIndex int
}{
{
host: "tunnel-a.example.com",
path: "/",
wantRuleIndex: 0,
},
{
host: "tunnel-a.example.com",
path: "/pages/about",
wantRuleIndex: 0,
},
{
host: "tunnel-a.example.com:443",
path: "/pages/about",
wantRuleIndex: 0,
},
{
host: "tunnel-b.example.com",
path: "/health",
wantRuleIndex: 1,
},
{
host: "tunnel-b.example.com",
path: "/index.html",
wantRuleIndex: 2,
},
{
host: "tunnel-c.example.com",
path: "/",
wantRuleIndex: 2,
},
}
for i, test := range tests {
_, ruleIndex := ingress.FindMatchingRule(test.host, test.path)
assert.Equal(t, test.wantRuleIndex, ruleIndex, fmt.Sprintf("Expect host=%s, path=%s to match rule %d, got %d", test.host, test.path, test.wantRuleIndex, i))
}
}
func mustParsePath(t *testing.T, path string) *regexp.Regexp {
regexp, err := regexp.Compile(path)
assert.NoError(t, err)
return regexp
}
func MustParseURL(t *testing.T, rawURL string) *url.URL {
u, err := url.Parse(rawURL)
require.NoError(t, err)
return u
}
func BenchmarkFindMatch(b *testing.B) {
rulesYAML := `
ingress:
- hostname: tunnel1.example.com
service: https://localhost:8000
- hostname: tunnel2.example.com
service: https://localhost:8001
- hostname: "*"
service: https://localhost:8002
`
ing, err := ParseIngress(MustReadIngress(rulesYAML))
if err != nil {
b.Error(err)
}
for n := 0; n < b.N; n++ {
ing.FindMatchingRule("tunnel1.example.com", "")
ing.FindMatchingRule("tunnel2.example.com", "")
ing.FindMatchingRule("tunnel3.example.com", "")
}
}
func MustReadIngress(s string) *config.Configuration {
var conf config.Configuration
err := yaml.Unmarshal([]byte(s), &conf)
if err != nil {
panic(err)
}
return &conf
}
+331
View File
@@ -0,0 +1,331 @@
package ingress
import (
"time"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/tlsconfig"
"github.com/urfave/cli/v2"
)
const (
defaultConnectTimeout = 30 * time.Second
defaultTLSTimeout = 10 * time.Second
defaultTCPKeepAlive = 30 * time.Second
defaultKeepAliveConnections = 100
defaultKeepAliveTimeout = 90 * time.Second
defaultProxyAddress = "127.0.0.1"
SSHServerFlag = "ssh-server"
Socks5Flag = "socks5"
ProxyConnectTimeoutFlag = "proxy-connect-timeout"
ProxyTLSTimeoutFlag = "proxy-tls-timeout"
ProxyTCPKeepAlive = "proxy-tcp-keepalive"
ProxyNoHappyEyeballsFlag = "proxy-no-happy-eyeballs"
ProxyKeepAliveConnectionsFlag = "proxy-keepalive-connections"
ProxyKeepAliveTimeoutFlag = "proxy-keepalive-timeout"
HTTPHostHeaderFlag = "http-host-header"
OriginServerNameFlag = "origin-server-name"
NoTLSVerifyFlag = "no-tls-verify"
NoChunkedEncodingFlag = "no-chunked-encoding"
ProxyAddressFlag = "proxy-address"
ProxyPortFlag = "proxy-port"
)
const (
socksProxy = "socks"
)
func originRequestFromSingeRule(c *cli.Context) OriginRequestConfig {
var connectTimeout time.Duration = defaultConnectTimeout
var tlsTimeout time.Duration = defaultTLSTimeout
var tcpKeepAlive time.Duration = defaultTCPKeepAlive
var noHappyEyeballs bool
var keepAliveConnections int = defaultKeepAliveConnections
var keepAliveTimeout time.Duration = defaultKeepAliveTimeout
var httpHostHeader string
var originServerName string
var caPool string
var noTLSVerify bool
var disableChunkedEncoding bool
var bastionMode bool
var proxyAddress string
var proxyPort uint
var proxyType string
if flag := ProxyConnectTimeoutFlag; c.IsSet(flag) {
connectTimeout = c.Duration(flag)
}
if flag := ProxyTLSTimeoutFlag; c.IsSet(flag) {
tlsTimeout = c.Duration(flag)
}
if flag := ProxyTCPKeepAlive; c.IsSet(flag) {
tcpKeepAlive = c.Duration(flag)
}
if flag := ProxyNoHappyEyeballsFlag; c.IsSet(flag) {
noHappyEyeballs = c.Bool(flag)
}
if flag := ProxyKeepAliveConnectionsFlag; c.IsSet(flag) {
keepAliveConnections = c.Int(flag)
}
if flag := ProxyKeepAliveTimeoutFlag; c.IsSet(flag) {
keepAliveTimeout = c.Duration(flag)
}
if flag := HTTPHostHeaderFlag; c.IsSet(flag) {
httpHostHeader = c.String(flag)
}
if flag := OriginServerNameFlag; c.IsSet(flag) {
originServerName = c.String(flag)
}
if flag := tlsconfig.OriginCAPoolFlag; c.IsSet(flag) {
caPool = c.String(flag)
}
if flag := NoTLSVerifyFlag; c.IsSet(flag) {
noTLSVerify = c.Bool(flag)
}
if flag := NoChunkedEncodingFlag; c.IsSet(flag) {
disableChunkedEncoding = c.Bool(flag)
}
if flag := config.BastionFlag; c.IsSet(flag) {
bastionMode = c.Bool(flag)
}
if flag := ProxyAddressFlag; c.IsSet(flag) {
proxyAddress = c.String(flag)
}
if flag := ProxyPortFlag; c.IsSet(flag) {
proxyPort = c.Uint(flag)
}
if c.IsSet(Socks5Flag) {
proxyType = socksProxy
}
return OriginRequestConfig{
ConnectTimeout: connectTimeout,
TLSTimeout: tlsTimeout,
TCPKeepAlive: tcpKeepAlive,
NoHappyEyeballs: noHappyEyeballs,
KeepAliveConnections: keepAliveConnections,
KeepAliveTimeout: keepAliveTimeout,
HTTPHostHeader: httpHostHeader,
OriginServerName: originServerName,
CAPool: caPool,
NoTLSVerify: noTLSVerify,
DisableChunkedEncoding: disableChunkedEncoding,
BastionMode: bastionMode,
ProxyAddress: proxyAddress,
ProxyPort: proxyPort,
ProxyType: proxyType,
}
}
func originRequestFromYAML(y config.OriginRequestConfig) OriginRequestConfig {
out := OriginRequestConfig{
ConnectTimeout: defaultConnectTimeout,
TLSTimeout: defaultTLSTimeout,
TCPKeepAlive: defaultTCPKeepAlive,
KeepAliveConnections: defaultKeepAliveConnections,
KeepAliveTimeout: defaultKeepAliveTimeout,
ProxyAddress: defaultProxyAddress,
}
if y.ConnectTimeout != nil {
out.ConnectTimeout = *y.ConnectTimeout
}
if y.TLSTimeout != nil {
out.TLSTimeout = *y.TLSTimeout
}
if y.TCPKeepAlive != nil {
out.TCPKeepAlive = *y.TCPKeepAlive
}
if y.NoHappyEyeballs != nil {
out.NoHappyEyeballs = *y.NoHappyEyeballs
}
if y.KeepAliveConnections != nil {
out.KeepAliveConnections = *y.KeepAliveConnections
}
if y.KeepAliveTimeout != nil {
out.KeepAliveTimeout = *y.KeepAliveTimeout
}
if y.HTTPHostHeader != nil {
out.HTTPHostHeader = *y.HTTPHostHeader
}
if y.OriginServerName != nil {
out.OriginServerName = *y.OriginServerName
}
if y.CAPool != nil {
out.CAPool = *y.CAPool
}
if y.NoTLSVerify != nil {
out.NoTLSVerify = *y.NoTLSVerify
}
if y.DisableChunkedEncoding != nil {
out.DisableChunkedEncoding = *y.DisableChunkedEncoding
}
if y.BastionMode != nil {
out.BastionMode = *y.BastionMode
}
if y.ProxyAddress != nil {
out.ProxyAddress = *y.ProxyAddress
}
if y.ProxyPort != nil {
out.ProxyPort = *y.ProxyPort
}
if y.ProxyType != nil {
out.ProxyType = *y.ProxyType
}
return out
}
// OriginRequestConfig configures how Cloudflared sends requests to origin
// services.
// Note: To specify a time.Duration in go-yaml, use e.g. "3s" or "24h".
type OriginRequestConfig struct {
// HTTP proxy timeout for establishing a new connection
ConnectTimeout time.Duration `yaml:"connectTimeout"`
// HTTP proxy timeout for completing a TLS handshake
TLSTimeout time.Duration `yaml:"tlsTimeout"`
// HTTP proxy TCP keepalive duration
TCPKeepAlive time.Duration `yaml:"tcpKeepAlive"`
// HTTP proxy should disable "happy eyeballs" for IPv4/v6 fallback
NoHappyEyeballs bool `yaml:"noHappyEyeballs"`
// HTTP proxy maximum keepalive connection pool size
KeepAliveConnections int `yaml:"keepAliveConnections"`
// HTTP proxy timeout for closing an idle connection
KeepAliveTimeout time.Duration `yaml:"keepAliveTimeout"`
// Sets the HTTP Host header for the local webserver.
HTTPHostHeader string `yaml:"httpHostHeader"`
// Hostname on the origin server certificate.
OriginServerName string `yaml:"originServerName"`
// Path to the CA for the certificate of your origin.
// This option should be used only if your certificate is not signed by Cloudflare.
CAPool string `yaml:"caPool"`
// Disables TLS verification of the certificate presented by your origin.
// Will allow any certificate from the origin to be accepted.
// Note: The connection from your machine to Cloudflare's Edge is still encrypted.
NoTLSVerify bool `yaml:"noTLSVerify"`
// Disables chunked transfer encoding.
// Useful if you are running a WSGI server.
DisableChunkedEncoding bool `yaml:"disableChunkedEncoding"`
// Runs as jump host
BastionMode bool `yaml:"bastionMode"`
// Listen address for the proxy.
ProxyAddress string `yaml:"proxyAddress"`
// Listen port for the proxy.
ProxyPort uint `yaml:"proxyPort"`
// What sort of proxy should be started
ProxyType string `yaml:"proxyType"`
}
func (defaults *OriginRequestConfig) setConnectTimeout(overrides config.OriginRequestConfig) {
if val := overrides.ConnectTimeout; val != nil {
defaults.ConnectTimeout = *val
}
}
func (defaults *OriginRequestConfig) setTLSTimeout(overrides config.OriginRequestConfig) {
if val := overrides.TLSTimeout; val != nil {
defaults.TLSTimeout = *val
}
}
func (defaults *OriginRequestConfig) setNoHappyEyeballs(overrides config.OriginRequestConfig) {
if val := overrides.NoHappyEyeballs; val != nil {
defaults.NoHappyEyeballs = *val
}
}
func (defaults *OriginRequestConfig) setKeepAliveConnections(overrides config.OriginRequestConfig) {
if val := overrides.KeepAliveConnections; val != nil {
defaults.KeepAliveConnections = *val
}
}
func (defaults *OriginRequestConfig) setKeepAliveTimeout(overrides config.OriginRequestConfig) {
if val := overrides.KeepAliveTimeout; val != nil {
defaults.KeepAliveTimeout = *val
}
}
func (defaults *OriginRequestConfig) setTCPKeepAlive(overrides config.OriginRequestConfig) {
if val := overrides.TCPKeepAlive; val != nil {
defaults.TCPKeepAlive = *val
}
}
func (defaults *OriginRequestConfig) setHTTPHostHeader(overrides config.OriginRequestConfig) {
if val := overrides.HTTPHostHeader; val != nil {
defaults.HTTPHostHeader = *val
}
}
func (defaults *OriginRequestConfig) setOriginServerName(overrides config.OriginRequestConfig) {
if val := overrides.OriginServerName; val != nil {
defaults.OriginServerName = *val
}
}
func (defaults *OriginRequestConfig) setCAPool(overrides config.OriginRequestConfig) {
if val := overrides.CAPool; val != nil {
defaults.CAPool = *val
}
}
func (defaults *OriginRequestConfig) setNoTLSVerify(overrides config.OriginRequestConfig) {
if val := overrides.NoTLSVerify; val != nil {
defaults.NoTLSVerify = *val
}
}
func (defaults *OriginRequestConfig) setDisableChunkedEncoding(overrides config.OriginRequestConfig) {
if val := overrides.DisableChunkedEncoding; val != nil {
defaults.DisableChunkedEncoding = *val
}
}
func (defaults *OriginRequestConfig) setBastionMode(overrides config.OriginRequestConfig) {
if val := overrides.BastionMode; val != nil {
defaults.BastionMode = *val
}
}
func (defaults *OriginRequestConfig) setProxyPort(overrides config.OriginRequestConfig) {
if val := overrides.ProxyPort; val != nil {
defaults.ProxyPort = *val
}
}
func (defaults *OriginRequestConfig) setProxyAddress(overrides config.OriginRequestConfig) {
if val := overrides.ProxyAddress; val != nil {
defaults.ProxyAddress = *val
}
}
func (defaults *OriginRequestConfig) setProxyType(overrides config.OriginRequestConfig) {
if val := overrides.ProxyType; val != nil {
defaults.ProxyType = *val
}
}
// SetConfig gets config for the requests that cloudflared sends to origins.
// Each field has a setter method which sets a value for the field by trying to find:
// 1. The user config for this rule
// 2. The user config for the overall ingress config
// 3. Defaults chosen by the cloudflared team
// 4. Golang zero values for that type
// If an earlier option isn't set, it will try the next option down.
func setConfig(defaults OriginRequestConfig, overrides config.OriginRequestConfig) OriginRequestConfig {
cfg := defaults
cfg.setConnectTimeout(overrides)
cfg.setTLSTimeout(overrides)
cfg.setNoHappyEyeballs(overrides)
cfg.setKeepAliveConnections(overrides)
cfg.setKeepAliveTimeout(overrides)
cfg.setTCPKeepAlive(overrides)
cfg.setHTTPHostHeader(overrides)
cfg.setOriginServerName(overrides)
cfg.setCAPool(overrides)
cfg.setNoTLSVerify(overrides)
cfg.setDisableChunkedEncoding(overrides)
cfg.setBastionMode(overrides)
cfg.setProxyPort(overrides)
cfg.setProxyAddress(overrides)
cfg.setProxyType(overrides)
return cfg
}
+184
View File
@@ -0,0 +1,184 @@
package ingress
import (
"testing"
"time"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
)
// Ensure that the nullable config from `config` package and the
// non-nullable config from `ingress` package have the same number of
// fields.
// This test ensures that programmers didn't add a new field to
// one struct and forget to add it to the other ;)
func TestCorrespondingFields(t *testing.T) {
require.Equal(
t,
CountFields(t, config.OriginRequestConfig{}),
CountFields(t, OriginRequestConfig{}),
)
}
func CountFields(t *testing.T, val interface{}) int {
b, err := yaml.Marshal(val)
require.NoError(t, err)
m := make(map[string]interface{}, 0)
err = yaml.Unmarshal(b, &m)
require.NoError(t, err)
return len(m)
}
func TestOriginRequestConfigOverrides(t *testing.T) {
rulesYAML := `
originRequest:
connectTimeout: 1m
tlsTimeout: 1s
noHappyEyeballs: true
tcpKeepAlive: 1s
keepAliveConnections: 1
keepAliveTimeout: 1s
httpHostHeader: abc
originServerName: a1
caPool: /tmp/path0
noTLSVerify: true
disableChunkedEncoding: true
bastionMode: True
proxyAddress: 127.1.2.3
proxyPort: 100
proxyType: socks5
ingress:
- hostname: tun.example.com
service: https://localhost:8000
- hostname: "*"
service: https://localhost:8001
originRequest:
connectTimeout: 2m
tlsTimeout: 2s
noHappyEyeballs: false
tcpKeepAlive: 2s
keepAliveConnections: 2
keepAliveTimeout: 2s
httpHostHeader: def
originServerName: b2
caPool: /tmp/path1
noTLSVerify: false
disableChunkedEncoding: false
bastionMode: false
proxyAddress: interface
proxyPort: 200
proxyType: ""
`
ing, err := ParseIngress(MustReadIngress(rulesYAML))
if err != nil {
t.Error(err)
}
// Rule 0 didn't override anything, so it inherits the user-specified
// root-level configuration.
actual0 := ing.Rules[0].Config
expected0 := OriginRequestConfig{
ConnectTimeout: 1 * time.Minute,
TLSTimeout: 1 * time.Second,
NoHappyEyeballs: true,
TCPKeepAlive: 1 * time.Second,
KeepAliveConnections: 1,
KeepAliveTimeout: 1 * time.Second,
HTTPHostHeader: "abc",
OriginServerName: "a1",
CAPool: "/tmp/path0",
NoTLSVerify: true,
DisableChunkedEncoding: true,
BastionMode: true,
ProxyAddress: "127.1.2.3",
ProxyPort: uint(100),
ProxyType: "socks5",
}
require.Equal(t, expected0, actual0)
// Rule 1 overrode all the root-level config.
actual1 := ing.Rules[1].Config
expected1 := OriginRequestConfig{
ConnectTimeout: 2 * time.Minute,
TLSTimeout: 2 * time.Second,
NoHappyEyeballs: false,
TCPKeepAlive: 2 * time.Second,
KeepAliveConnections: 2,
KeepAliveTimeout: 2 * time.Second,
HTTPHostHeader: "def",
OriginServerName: "b2",
CAPool: "/tmp/path1",
NoTLSVerify: false,
DisableChunkedEncoding: false,
BastionMode: false,
ProxyAddress: "interface",
ProxyPort: uint(200),
ProxyType: "",
}
require.Equal(t, expected1, actual1)
}
func TestOriginRequestConfigDefaults(t *testing.T) {
rulesYAML := `
ingress:
- hostname: tun.example.com
service: https://localhost:8000
- hostname: "*"
service: https://localhost:8001
originRequest:
connectTimeout: 2m
tlsTimeout: 2s
noHappyEyeballs: false
tcpKeepAlive: 2s
keepAliveConnections: 2
keepAliveTimeout: 2s
httpHostHeader: def
originServerName: b2
caPool: /tmp/path1
noTLSVerify: false
disableChunkedEncoding: false
bastionMode: false
proxyAddress: interface
proxyPort: 200
proxyType: ""
`
ing, err := ParseIngress(MustReadIngress(rulesYAML))
if err != nil {
t.Error(err)
}
// Rule 0 didn't override anything, so it inherits the cloudflared defaults
actual0 := ing.Rules[0].Config
expected0 := OriginRequestConfig{
ConnectTimeout: defaultConnectTimeout,
TLSTimeout: defaultTLSTimeout,
TCPKeepAlive: defaultTCPKeepAlive,
KeepAliveConnections: defaultKeepAliveConnections,
KeepAliveTimeout: defaultKeepAliveTimeout,
ProxyAddress: defaultProxyAddress,
}
require.Equal(t, expected0, actual0)
// Rule 1 overrode all defaults.
actual1 := ing.Rules[1].Config
expected1 := OriginRequestConfig{
ConnectTimeout: 2 * time.Minute,
TLSTimeout: 2 * time.Second,
NoHappyEyeballs: false,
TCPKeepAlive: 2 * time.Second,
KeepAliveConnections: 2,
KeepAliveTimeout: 2 * time.Second,
HTTPHostHeader: "def",
OriginServerName: "b2",
CAPool: "/tmp/path1",
NoTLSVerify: false,
DisableChunkedEncoding: false,
BastionMode: false,
ProxyAddress: "interface",
ProxyPort: uint(200),
ProxyType: "",
}
require.Equal(t, expected1, actual1)
}
+320
View File
@@ -0,0 +1,320 @@
package ingress
import (
"context"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strconv"
"sync"
"time"
"github.com/cloudflare/cloudflared/hello"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/socks"
"github.com/cloudflare/cloudflared/tlsconfig"
"github.com/cloudflare/cloudflared/websocket"
gws "github.com/gorilla/websocket"
"github.com/pkg/errors"
)
// OriginService is something a tunnel can proxy traffic to.
type OriginService interface {
// RoundTrip is how cloudflared proxies eyeball requests to the actual origin services
http.RoundTripper
String() string
// Start the origin service if it's managed by cloudflared, e.g. proxy servers or Hello World.
// If it's not managed by cloudflared, this is a no-op because the user is responsible for
// starting the origin service.
start(wg *sync.WaitGroup, log logger.Service, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error
}
// unixSocketPath is an OriginService representing a unix socket (which accepts HTTP)
type unixSocketPath struct {
path string
transport *http.Transport
}
func (o *unixSocketPath) String() string {
return "unix socket: " + o.path
}
func (o *unixSocketPath) start(wg *sync.WaitGroup, log logger.Service, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error {
transport, err := newHTTPTransport(o, cfg)
if err != nil {
return err
}
o.transport = transport
return nil
}
func (o *unixSocketPath) RoundTrip(req *http.Request) (*http.Response, error) {
return o.transport.RoundTrip(req)
}
func (o *unixSocketPath) Dial(reqURL *url.URL, headers http.Header) (*gws.Conn, *http.Response, error) {
d := &gws.Dialer{
NetDial: o.transport.Dial,
NetDialContext: o.transport.DialContext,
TLSClientConfig: o.transport.TLSClientConfig,
}
reqURL.Scheme = websocket.ChangeRequestScheme(reqURL)
return d.Dial(reqURL.String(), headers)
}
// localService is an OriginService listening on a TCP/IP address the user's origin can route to.
type localService struct {
// The URL for the user's origin service
RootURL *url.URL
// The URL that cloudflared should send requests to.
// If this origin requires starting a proxy, this is the proxy's address,
// and that proxy points to RootURL. Otherwise, this is equal to RootURL.
URL *url.URL
transport *http.Transport
}
func (o *localService) Dial(reqURL *url.URL, headers http.Header) (*gws.Conn, *http.Response, error) {
d := &gws.Dialer{TLSClientConfig: o.transport.TLSClientConfig}
// Rewrite the request URL so that it goes to the origin service.
reqURL.Host = o.URL.Host
reqURL.Scheme = websocket.ChangeRequestScheme(o.URL)
return d.Dial(reqURL.String(), headers)
}
func (o *localService) address() string {
return o.URL.String()
}
func (o *localService) start(wg *sync.WaitGroup, log logger.Service, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error {
transport, err := newHTTPTransport(o, cfg)
if err != nil {
return err
}
o.transport = transport
// Start a proxy if one is needed
staticHost := o.staticHost()
if originRequiresProxy(staticHost, cfg) {
if err := o.startProxy(staticHost, wg, log, shutdownC, errC, cfg); err != nil {
return err
}
}
return nil
}
func (o *localService) startProxy(staticHost string, wg *sync.WaitGroup, log logger.Service, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error {
// Start a listener for the proxy
proxyAddress := net.JoinHostPort(cfg.ProxyAddress, strconv.Itoa(int(cfg.ProxyPort)))
listener, err := net.Listen("tcp", proxyAddress)
if err != nil {
log.Errorf("Cannot start Websocket Proxy Server: %s", err)
return errors.Wrap(err, "Cannot start Websocket Proxy Server")
}
// Start the proxy itself
wg.Add(1)
go func() {
defer wg.Done()
streamHandler := websocket.DefaultStreamHandler
// This origin's config specifies what type of proxy to start.
switch cfg.ProxyType {
case socksProxy:
log.Info("SOCKS5 server started")
streamHandler = func(wsConn *websocket.Conn, remoteConn net.Conn, _ http.Header) {
dialer := socks.NewConnDialer(remoteConn)
requestHandler := socks.NewRequestHandler(dialer)
socksServer := socks.NewConnectionHandler(requestHandler)
socksServer.Serve(wsConn)
}
case "":
log.Debug("Not starting any websocket proxy")
default:
log.Errorf("%s isn't a valid proxy (valid options are {%s})", cfg.ProxyType, socksProxy)
}
errC <- websocket.StartProxyServer(log, listener, staticHost, shutdownC, streamHandler)
}()
// Modify this origin, so that it no longer points at the origin service directly.
// Instead, it points at the proxy to the origin service.
newURL, err := url.Parse("http://" + listener.Addr().String())
if err != nil {
return err
}
o.URL = newURL
return nil
}
func (o *localService) String() string {
return o.address()
}
func (o *localService) RoundTrip(req *http.Request) (*http.Response, error) {
// Rewrite the request URL so that it goes to the origin service.
req.URL.Host = o.URL.Host
req.URL.Scheme = o.URL.Scheme
return o.transport.RoundTrip(req)
}
func (o *localService) staticHost() string {
addPortIfMissing := func(uri *url.URL, port int) string {
if uri.Port() != "" {
return uri.Host
}
return fmt.Sprintf("%s:%d", uri.Hostname(), port)
}
switch o.URL.Scheme {
case "ssh":
return addPortIfMissing(o.URL, 22)
case "rdp":
return addPortIfMissing(o.URL, 3389)
case "smb":
return addPortIfMissing(o.URL, 445)
case "tcp":
return addPortIfMissing(o.URL, 7864) // just a random port since there isn't a default in this case
}
return ""
}
// HelloWorld is an OriginService for the built-in Hello World server.
// Users only use this for testing and experimenting with cloudflared.
type helloWorld struct {
server net.Listener
transport *http.Transport
}
func (o *helloWorld) String() string {
return "Hello World test origin"
}
// Start starts a HelloWorld server and stores its address in the Service receiver.
func (o *helloWorld) start(wg *sync.WaitGroup, log logger.Service, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error {
transport, err := newHTTPTransport(o, cfg)
if err != nil {
return err
}
o.transport = transport
helloListener, err := hello.CreateTLSListener("127.0.0.1:")
if err != nil {
return errors.Wrap(err, "Cannot start Hello World Server")
}
wg.Add(1)
go func() {
defer wg.Done()
_ = hello.StartHelloWorldServer(log, helloListener, shutdownC)
}()
o.server = helloListener
return nil
}
func (o *helloWorld) RoundTrip(req *http.Request) (*http.Response, error) {
// Rewrite the request URL so that it goes to the Hello World server.
req.URL.Host = o.server.Addr().String()
req.URL.Scheme = "https"
return o.transport.RoundTrip(req)
}
func (o *helloWorld) Dial(reqURL *url.URL, headers http.Header) (*gws.Conn, *http.Response, error) {
d := &gws.Dialer{
TLSClientConfig: o.transport.TLSClientConfig,
}
reqURL.Host = o.server.Addr().String()
reqURL.Scheme = "wss"
return d.Dial(reqURL.String(), headers)
}
func originRequiresProxy(staticHost string, cfg OriginRequestConfig) bool {
return staticHost != "" || cfg.BastionMode
}
// statusCode is an OriginService that just responds with a given HTTP status.
// Typical use-case is "user wants the catch-all rule to just respond 404".
type statusCode struct {
resp *http.Response
}
func newStatusCode(status int) statusCode {
resp := &http.Response{
StatusCode: status,
Status: http.StatusText(status),
Body: new(NopReadCloser),
}
return statusCode{resp: resp}
}
func (o *statusCode) String() string {
return fmt.Sprintf("HTTP %d", o.resp.StatusCode)
}
func (o *statusCode) start(wg *sync.WaitGroup, log logger.Service, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error {
return nil
}
func (o *statusCode) RoundTrip(_ *http.Request) (*http.Response, error) {
return o.resp, nil
}
type NopReadCloser struct{}
// Read always returns EOF to signal end of input
func (nrc *NopReadCloser) Read(buf []byte) (int, error) {
return 0, io.EOF
}
func (nrc *NopReadCloser) Close() error {
return nil
}
func newHTTPTransport(service OriginService, cfg OriginRequestConfig) (*http.Transport, error) {
originCertPool, err := tlsconfig.LoadOriginCA(cfg.CAPool, nil)
if err != nil {
return nil, errors.Wrap(err, "Error loading cert pool")
}
httpTransport := http.Transport{
Proxy: http.ProxyFromEnvironment,
MaxIdleConns: cfg.KeepAliveConnections,
MaxIdleConnsPerHost: cfg.KeepAliveConnections,
IdleConnTimeout: cfg.KeepAliveTimeout,
TLSHandshakeTimeout: cfg.TLSTimeout,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{RootCAs: originCertPool, InsecureSkipVerify: cfg.NoTLSVerify},
}
if _, isHelloWorld := service.(*helloWorld); !isHelloWorld && cfg.OriginServerName != "" {
httpTransport.TLSClientConfig.ServerName = cfg.OriginServerName
}
dialer := &net.Dialer{
Timeout: cfg.ConnectTimeout,
KeepAlive: cfg.TCPKeepAlive,
}
if cfg.NoHappyEyeballs {
dialer.FallbackDelay = -1 // As of Golang 1.12, a negative delay disables "happy eyeballs"
}
// DialContext depends on which kind of origin is being used.
dialContext := dialer.DialContext
switch service := service.(type) {
// If this origin is a unix socket, enforce network type "unix".
case *unixSocketPath:
httpTransport.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
return dialContext(ctx, "unix", service.path)
}
// Otherwise, use the regular network config.
default:
httpTransport.DialContext = dialContext
}
return &httpTransport, nil
}
+50
View File
@@ -0,0 +1,50 @@
package ingress
import (
"regexp"
"strings"
)
// Rule routes traffic from a hostname/path on the public internet to the
// service running on the given URL.
type Rule struct {
// Requests for this hostname will be proxied to this rule's service.
Hostname string
// Path is an optional regex that can specify path-driven ingress rules.
Path *regexp.Regexp
// A (probably local) address. Requests for a hostname which matches this
// rule's hostname pattern will be proxied to the service running on this
// address.
Service OriginService
// Configure the request cloudflared sends to this specific origin.
Config OriginRequestConfig
}
// MultiLineString is for outputting rules in a human-friendly way when Cloudflared
// is used as a CLI tool (not as a daemon).
func (r Rule) MultiLineString() string {
var out strings.Builder
if r.Hostname != "" {
out.WriteString("\thostname: ")
out.WriteString(r.Hostname)
out.WriteRune('\n')
}
if r.Path != nil {
out.WriteString("\tpath: ")
out.WriteString(r.Path.String())
out.WriteRune('\n')
}
out.WriteString("\tservice: ")
out.WriteString(r.Service.String())
return out.String()
}
// Matches checks if the rule matches a given hostname/path combination.
func (r *Rule) Matches(hostname, path string) bool {
hostMatch := r.Hostname == "" || r.Hostname == "*" || matchHost(r.Hostname, hostname)
pathMatch := r.Path == nil || r.Path.MatchString(path)
return hostMatch && pathMatch
}
+146
View File
@@ -0,0 +1,146 @@
package ingress
import (
"io"
"net/http/httptest"
"net/url"
"regexp"
"testing"
"github.com/stretchr/testify/require"
)
func Test_rule_matches(t *testing.T) {
type fields struct {
Hostname string
Path *regexp.Regexp
Service OriginService
}
type args struct {
requestURL *url.URL
}
tests := []struct {
name string
fields fields
args args
want bool
}{
{
name: "Just hostname, pass",
fields: fields{
Hostname: "example.com",
},
args: args{
requestURL: MustParseURL(t, "https://example.com"),
},
want: true,
},
{
name: "Entire hostname is wildcard, should match everything",
fields: fields{
Hostname: "*",
},
args: args{
requestURL: MustParseURL(t, "https://example.com"),
},
want: true,
},
{
name: "Just hostname, fail",
fields: fields{
Hostname: "example.com",
},
args: args{
requestURL: MustParseURL(t, "https://foo.bar"),
},
want: false,
},
{
name: "Just wildcard hostname, pass",
fields: fields{
Hostname: "*.example.com",
},
args: args{
requestURL: MustParseURL(t, "https://adam.example.com"),
},
want: true,
},
{
name: "Just wildcard hostname, fail",
fields: fields{
Hostname: "*.example.com",
},
args: args{
requestURL: MustParseURL(t, "https://tunnel.com"),
},
want: false,
},
{
name: "Just wildcard outside of subdomain in hostname, fail",
fields: fields{
Hostname: "*example.com",
},
args: args{
requestURL: MustParseURL(t, "https://www.example.com"),
},
want: false,
},
{
name: "Wildcard over multiple subdomains",
fields: fields{
Hostname: "*.example.com",
},
args: args{
requestURL: MustParseURL(t, "https://adam.chalmers.example.com"),
},
want: true,
},
{
name: "Hostname and path",
fields: fields{
Hostname: "*.example.com",
Path: regexp.MustCompile("/static/.*\\.html"),
},
args: args{
requestURL: MustParseURL(t, "https://www.example.com/static/index.html"),
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := Rule{
Hostname: tt.fields.Hostname,
Path: tt.fields.Path,
Service: tt.fields.Service,
}
u := tt.args.requestURL
if got := r.Matches(u.Hostname(), u.Path); got != tt.want {
t.Errorf("rule.matches() = %v, want %v", got, tt.want)
}
})
}
}
func TestStaticHTTPStatus(t *testing.T) {
o := newStatusCode(404)
buf := make([]byte, 100)
sendReq := func() {
resp, err := o.RoundTrip(nil)
require.NoError(t, err)
_, err = resp.Body.Read(buf)
require.Equal(t, io.EOF, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 404, resp.StatusCode)
resp, err = o.RoundTrip(nil)
require.NoError(t, err)
w := httptest.NewRecorder()
n, err := io.Copy(w, resp.Body)
require.NoError(t, err)
require.Equal(t, int64(0), n)
}
sendReq()
sendReq()
}
+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,
}
}
+189
View File
@@ -0,0 +1,189 @@
package origin
import (
"context"
"errors"
"fmt"
"sync"
"time"
"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")
rpcClient, err := newTunnelRPCClient(ctx, muxer, config, reconnect)
if err != nil {
return err
}
defer rpcClient.Close()
// Request server info without blocking tunnel registration; must use capnp library directly.
serverInfoPromise := tunnelrpc.TunnelServer{Client: rpcClient.Client}.GetServerInfo(ctx, func(tunnelrpc.TunnelServer_getServerInfo_Params) error {
return nil
})
LogServerInfo(serverInfoPromise.Result(), connectionID, config.Metrics, logger, config.TunnelEventChan)
registration := rpcClient.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)
}
+17 -111
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 {
@@ -417,16 +323,16 @@ func (s *Supervisor) authenticate(ctx context.Context, numPreviousAttempts int)
<-muxer.Shutdown()
}()
tunnelServer, err := connection.NewRPCClient(ctx, muxer, s.logger, openStreamTimeout)
rpcClient, err := newTunnelRPCClient(ctx, muxer, s.config, authenticate)
if err != nil {
return nil, err
}
defer tunnelServer.Close()
defer rpcClient.Close()
const arbitraryConnectionID = uint8(0)
registrationOptions := s.config.RegistrationOptions(arbitraryConnectionID, edgeConn.LocalAddr().String(), s.cloudflaredUUID)
registrationOptions.NumPreviousAttempts = uint8(numPreviousAttempts)
authResponse, err := tunnelServer.Authenticate(
authResponse, err := rpcClient.Authenticate(
ctx,
s.config.OriginCert,
s.config.Hostname,
+147 -166
View File
@@ -21,14 +21,15 @@ 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/ingress"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/signal"
"github.com/cloudflare/cloudflared/tunnelrpc"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/cloudflare/cloudflared/validation"
"github.com/cloudflare/cloudflared/websocket"
)
@@ -43,26 +44,25 @@ const (
FeatureQuickReconnects = "quick_reconnects"
)
type registerRPCName string
type rpcName string
const (
register registerRPCName = "register"
reconnect registerRPCName = "reconnect"
register rpcName = "register"
reconnect rpcName = "reconnect"
unregister rpcName = "unregister"
authenticate rpcName = " authenticate"
)
type TunnelConfig struct {
BuildInfo *buildinfo.BuildInfo
ClientID string
ClientTlsConfig *tls.Config
CloseConnOnce *sync.Once // Used to close connectedSignal no more than once
CompressionQuality uint64
EdgeAddrs []string
GracePeriod time.Duration
HAConnections int
HTTPTransport http.RoundTripper
HeartbeatInterval time.Duration
Hostname string
HTTPHostHeader string
IncidentLookup IncidentLookup
IsAutoupdated bool
IsFreeTunnel bool
@@ -72,7 +72,6 @@ type TunnelConfig struct {
MaxHeartbeats uint64
Metrics *TunnelMetrics
MetricsUpdateFreq time.Duration
NoChunkedEncoding bool
OriginCert []byte
ReportedVersion string
Retries uint
@@ -80,24 +79,14 @@ type TunnelConfig struct {
Tags []tunnelpogs.Tag
TlsConfig *tls.Config
WSGI bool
// OriginUrl may not be used if a user specifies a unix socket.
OriginUrl string
// feature-flag to use new edge reconnect tokens
UseReconnectToken bool
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
IngressRules ingress.Ingress
}
type dupConnRegisterTunnelError struct{}
@@ -129,7 +118,7 @@ type clientRegisterTunnelError struct {
cause error
}
func newClientRegisterTunnelError(cause error, counter *prometheus.CounterVec, name registerRPCName) clientRegisterTunnelError {
func newRPCError(cause error, counter *prometheus.CounterVec, name rpcName) clientRegisterTunnelError {
counter.WithLabelValues(cause.Error(), string(name)).Inc()
return clientRegisterTunnelError{cause: cause}
}
@@ -172,16 +161,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 +183,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 +202,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 +237,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 +252,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 +275,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 +310,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)
})
@@ -341,7 +334,7 @@ func ServeTunnel(
if config.NamedTunnel != nil {
_ = UnregisterConnection(ctx, handler.muxer, config)
} else {
_ = UnregisterTunnel(handler.muxer, config.GracePeriod, config.TransportLogger)
_ = UnregisterTunnel(handler.muxer, config)
}
}
handler.muxer.Shutdown()
@@ -416,23 +409,23 @@ func RegisterConnection(
config *TunnelConfig,
connectionIndex uint8,
originLocalAddr string,
numPreviousAttempts uint8,
) error {
const registerConnection = "registerConnection"
config.TransportLogger.Debug("initiating RPC stream for RegisterConnection")
rpc, err := connection.NewRPCClient(ctx, muxer, config.TransportLogger, openStreamTimeout)
rpcClient, err := newTunnelRPCClient(ctx, muxer, config, registerConnection)
if err != nil {
// RPC stream open error
return newClientRegisterTunnelError(err, config.Metrics.rpcFail, registerConnection)
return err
}
defer rpc.Close()
defer rpcClient.Close()
conn, err := rpc.RegisterConnection(
conn, err := rpcClient.RegisterConnection(
ctx,
config.NamedTunnel.Auth,
config.NamedTunnel.ID,
connectionIndex,
config.ConnectionOptions(originLocalAddr),
config.ConnectionOptions(originLocalAddr, numPreviousAttempts),
)
if err != nil {
if err.Error() == DuplicateConnectionError {
@@ -446,6 +439,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
}
@@ -468,19 +466,19 @@ func UnregisterConnection(
config *TunnelConfig,
) error {
config.TransportLogger.Debug("initiating RPC stream for UnregisterConnection")
rpc, err := connection.NewRPCClient(ctx, muxer, config.TransportLogger, openStreamTimeout)
rpcClient, err := newTunnelRPCClient(ctx, muxer, config, register)
if err != nil {
// RPC stream open error
return newClientRegisterTunnelError(err, config.Metrics.rpcFail, register)
return err
}
defer rpc.Close()
defer rpcClient.Close()
return rpc.UnregisterConnection(ctx)
return rpcClient.UnregisterConnection(ctx)
}
func RegisterTunnel(
ctx context.Context,
credentialManager ReconnectTunnelCredentialManager,
credentialManager *reconnectCredentialManager,
muxer *h2mux.Muxer,
config *TunnelConfig,
logger logger.Service,
@@ -489,18 +487,21 @@ func RegisterTunnel(
uuid uuid.UUID,
) error {
config.TransportLogger.Debug("initiating RPC stream to register")
tunnelServer, err := connection.NewRPCClient(ctx, muxer, config.TransportLogger, openStreamTimeout)
if err != nil {
// RPC stream open error
return newClientRegisterTunnelError(err, config.Metrics.rpcFail, register)
if config.TunnelEventChan != nil {
config.TunnelEventChan <- ui.TunnelEvent{EventType: ui.RegisteringTunnel}
}
defer tunnelServer.Close()
rpcClient, err := newTunnelRPCClient(ctx, muxer, config, register)
if err != nil {
return err
}
defer rpcClient.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 {
serverInfoPromise := tunnelrpc.TunnelServer{Client: rpcClient.Client}.GetServerInfo(ctx, func(tunnelrpc.TunnelServer_getServerInfo_Params) error {
return nil
})
LogServerInfo(serverInfoPromise.Result(), connectionID, config.Metrics, logger)
registration := tunnelServer.RegisterTunnel(
LogServerInfo(serverInfoPromise.Result(), connectionID, config.Metrics, logger, config.TunnelEventChan)
registration := rpcClient.RegisterTunnel(
ctx,
config.OriginCert,
config.Hostname,
@@ -510,47 +511,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(
@@ -558,8 +525,8 @@ func processRegistrationSuccess(
logger logger.Service,
connectionID uint8,
registration *tunnelpogs.TunnelRegistration,
name registerRPCName,
credentialManager ReconnectTunnelCredentialManager,
name rpcName,
credentialManager *reconnectCredentialManager,
) error {
for _, logLine := range registration.LogLines {
logger.Info(logLine)
@@ -570,15 +537,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")
}
}
@@ -590,7 +559,7 @@ func processRegistrationSuccess(
return nil
}
func processRegisterTunnelError(err tunnelpogs.TunnelRegistrationError, metrics *TunnelMetrics, name registerRPCName) error {
func processRegisterTunnelError(err tunnelpogs.TunnelRegistrationError, metrics *TunnelMetrics, name rpcName) error {
if err.Error() == DuplicateConnectionError {
metrics.regFail.WithLabelValues("dup_edge_conn", string(name)).Inc()
return errDuplicationConnection
@@ -602,18 +571,18 @@ func processRegisterTunnelError(err tunnelpogs.TunnelRegistrationError, metrics
}
}
func UnregisterTunnel(muxer *h2mux.Muxer, gracePeriod time.Duration, logger logger.Service) error {
logger.Debug("initiating RPC stream to unregister")
func UnregisterTunnel(muxer *h2mux.Muxer, config *TunnelConfig) error {
config.TransportLogger.Debug("initiating RPC stream to unregister")
ctx := context.Background()
tunnelServer, err := connection.NewRPCClient(ctx, muxer, logger, openStreamTimeout)
rpcClient, err := newTunnelRPCClient(ctx, muxer, config, unregister)
if err != nil {
// RPC stream open error
return err
}
defer tunnelServer.Close()
defer rpcClient.Close()
// gracePeriod is encoded in int64 using capnproto
return tunnelServer.UnregisterTunnel(ctx, gracePeriod.Nanoseconds())
return rpcClient.UnregisterTunnel(ctx, config.GracePeriod.Nanoseconds())
}
func LogServerInfo(
@@ -621,6 +590,7 @@ func LogServerInfo(
connectionID uint8,
metrics *TunnelMetrics,
logger logger.Service,
tunnelEventChan chan<- ui.TunnelEvent,
) {
serverInfoMessage, err := promise.Struct()
if err != nil {
@@ -632,22 +602,22 @@ 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)
}
type TunnelHandler struct {
originUrl string
httpHostHeader string
muxer *h2mux.Muxer
httpClient http.RoundTripper
tlsConfig *tls.Config
tags []tunnelpogs.Tag
metrics *TunnelMetrics
ingressRules ingress.Ingress
muxer *h2mux.Muxer
tags []tunnelpogs.Tag
metrics *TunnelMetrics
// connectionID is only used by metrics, and prometheus requires labels to be string
connectionID string
logger logger.Service
noChunkedEncoding bool
connectionID string
logger logger.Service
bufferPool *buffer.Pool
}
@@ -659,24 +629,14 @@ func NewTunnelHandler(ctx context.Context,
connectionID uint8,
bufferPool *buffer.Pool,
) (*TunnelHandler, string, error) {
originURL, err := validation.ValidateUrl(config.OriginUrl)
if err != nil {
return nil, "", fmt.Errorf("unable to parse origin URL %#v", originURL)
}
h := &TunnelHandler{
originUrl: originURL,
httpHostHeader: config.HTTPHostHeader,
httpClient: config.HTTPTransport,
tlsConfig: config.ClientTlsConfig,
tags: config.Tags,
metrics: config.Metrics,
connectionID: uint8ToString(connectionID),
logger: config.Logger,
noChunkedEncoding: config.NoChunkedEncoding,
bufferPool: bufferPool,
}
if h.httpClient == nil {
h.httpClient = http.DefaultTransport
ingressRules: config.IngressRules,
tags: config.Tags,
metrics: config.Metrics,
connectionID: uint8ToString(connectionID),
logger: config.Logger,
bufferPool: bufferPool,
}
edgeConn, err := connection.DialEdge(ctx, dialTimeout, config.TlsConfig, addr)
@@ -702,7 +662,7 @@ func (h *TunnelHandler) ServeStream(stream *h2mux.MuxedStream) error {
h.metrics.incrementRequests(h.connectionID)
defer h.metrics.decrementConcurrentRequests(h.connectionID)
req, reqErr := h.createRequest(stream)
req, rule, reqErr := h.createRequest(stream)
if reqErr != nil {
h.writeErrorResponse(stream, reqErr)
return reqErr
@@ -715,9 +675,9 @@ func (h *TunnelHandler) ServeStream(stream *h2mux.MuxedStream) error {
var resp *http.Response
var respErr error
if websocket.IsWebSocketUpgrade(req) {
resp, respErr = h.serveWebsocket(stream, req)
resp, respErr = h.serveWebsocket(stream, req, rule)
} else {
resp, respErr = h.serveHTTP(stream, req)
resp, respErr = h.serveHTTP(stream, req, rule)
}
if respErr != nil {
h.writeErrorResponse(stream, respErr)
@@ -727,26 +687,32 @@ func (h *TunnelHandler) ServeStream(stream *h2mux.MuxedStream) error {
return nil
}
func (h *TunnelHandler) createRequest(stream *h2mux.MuxedStream) (*http.Request, error) {
req, err := http.NewRequest("GET", h.originUrl, h2mux.MuxedStreamReader{MuxedStream: stream})
func (h *TunnelHandler) createRequest(stream *h2mux.MuxedStream) (*http.Request, *ingress.Rule, error) {
req, err := http.NewRequest("GET", "http://localhost:8080", h2mux.MuxedStreamReader{MuxedStream: stream})
if err != nil {
return nil, errors.Wrap(err, "Unexpected error from http.NewRequest")
return nil, nil, errors.Wrap(err, "Unexpected error from http.NewRequest")
}
err = h2mux.H2RequestHeadersToH1Request(stream.Headers, req)
if err != nil {
return nil, errors.Wrap(err, "invalid request received")
return nil, nil, errors.Wrap(err, "invalid request received")
}
h.AppendTagHeaders(req)
return req, nil
// For incoming requests, the Host header is promoted to the Request.Host field and removed from the Header map.
rule, _ := h.ingressRules.FindMatchingRule(req.Host, req.URL.Path)
return req, rule, nil
}
func (h *TunnelHandler) serveWebsocket(stream *h2mux.MuxedStream, req *http.Request) (*http.Response, error) {
if h.httpHostHeader != "" {
req.Header.Set("Host", h.httpHostHeader)
req.Host = h.httpHostHeader
func (h *TunnelHandler) serveWebsocket(stream *h2mux.MuxedStream, req *http.Request, rule *ingress.Rule) (*http.Response, error) {
if hostHeader := rule.Config.HTTPHostHeader; hostHeader != "" {
req.Header.Set("Host", hostHeader)
req.Host = hostHeader
}
conn, response, err := websocket.ClientConnect(req, h.tlsConfig)
dialler, ok := rule.Service.(websocket.Dialler)
if !ok {
return nil, fmt.Errorf("Websockets aren't supported by the origin service '%s'", rule.Service)
}
conn, response, err := websocket.ClientConnect(req, dialler)
if err != nil {
return nil, err
}
@@ -762,9 +728,9 @@ func (h *TunnelHandler) serveWebsocket(stream *h2mux.MuxedStream, req *http.Requ
return response, nil
}
func (h *TunnelHandler) serveHTTP(stream *h2mux.MuxedStream, req *http.Request) (*http.Response, error) {
func (h *TunnelHandler) serveHTTP(stream *h2mux.MuxedStream, req *http.Request, rule *ingress.Rule) (*http.Response, error) {
// Support for WSGI Servers by switching transfer encoding from chunked to gzip/deflate
if h.noChunkedEncoding {
if rule.Config.DisableChunkedEncoding {
req.TransferEncoding = []string{"gzip", "deflate"}
cLength, err := strconv.Atoi(req.Header.Get("Content-Length"))
if err == nil {
@@ -775,12 +741,12 @@ func (h *TunnelHandler) serveHTTP(stream *h2mux.MuxedStream, req *http.Request)
// Request origin to keep connection alive to improve performance
req.Header.Set("Connection", "keep-alive")
if h.httpHostHeader != "" {
req.Header.Set("Host", h.httpHostHeader)
req.Host = h.httpHostHeader
if hostHeader := rule.Config.HTTPHostHeader; hostHeader != "" {
req.Header.Set("Host", hostHeader)
req.Host = hostHeader
}
response, err := h.httpClient.RoundTrip(req)
response, err := rule.Service.RoundTrip(req)
if err != nil {
return nil, errors.Wrap(err, "Error proxying request to origin")
}
@@ -931,3 +897,18 @@ func findCfRayHeader(h1 *http.Request) string {
func isLBProbeRequest(req *http.Request) bool {
return strings.HasPrefix(req.UserAgent(), lbProbeUserAgentPrefix)
}
func newTunnelRPCClient(ctx context.Context, muxer *h2mux.Muxer, config *TunnelConfig, rpcName rpcName) (tunnelpogs.TunnelServer_PogsClient, error) {
openStreamCtx, openStreamCancel := context.WithTimeout(ctx, openStreamTimeout)
defer openStreamCancel()
stream, err := muxer.OpenRPCStream(openStreamCtx)
if err != nil {
return tunnelpogs.TunnelServer_PogsClient{}, err
}
rpcClient, err := connection.NewTunnelRPCClient(ctx, stream, config.TransportLogger)
if err != nil {
// RPC stream open error
return tunnelpogs.TunnelServer_PogsClient{}, newRPCError(err, config.Metrics.rpcFail, rpcName)
}
return rpcClient, nil
}
+2 -3
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 (
@@ -65,10 +65,9 @@ func (cr *CertReloader) LoadCert() error {
return nil
}
func LoadOriginCA(c *cli.Context, logger logger.Service) (*x509.CertPool, error) {
func LoadOriginCA(originCAPoolFilename string, logger logger.Service) (*x509.CertPool, error) {
var originCustomCAPool []byte
originCAPoolFilename := c.String(OriginCAPoolFlag)
if originCAPoolFilename != "" {
var err error
originCustomCAPool, err = ioutil.ReadFile(originCAPoolFilename)
+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
}
+43 -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
@@ -27,6 +27,48 @@ type Listener struct {
logger logger.Service
}
func Command(hidden bool) *cli.Command {
return &cli.Command{
Name: "proxy-dns",
Action: cliutil.ErrorHandler(Run),
Usage: "Run a DNS over HTTPS proxy server.",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "metrics",
Value: "localhost:",
Usage: "Listen address for metrics reporting.",
EnvVars: []string{"TUNNEL_METRICS"},
},
&cli.StringFlag{
Name: "address",
Usage: "Listen address for the DNS over HTTPS proxy server.",
Value: "localhost",
EnvVars: []string{"TUNNEL_DNS_ADDRESS"},
},
&cli.IntFlag{
Name: "port",
Usage: "Listen on given port for the DNS over HTTPS proxy server.",
Value: 53,
EnvVars: []string{"TUNNEL_DNS_PORT"},
},
&cli.StringSliceFlag{
Name: "upstream",
Usage: "Upstream endpoint URL, you can specify multiple endpoints for redundancy.",
Value: cli.NewStringSlice("https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"),
EnvVars: []string{"TUNNEL_DNS_UPSTREAM"},
},
&cli.StringSliceFlag{
Name: "bootstrap",
Usage: "bootstrap endpoint URL, you can specify multiple endpoints for redundancy.",
Value: cli.NewStringSlice("https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"),
EnvVars: []string{"TUNNEL_DNS_BOOTSTRAP"},
},
},
ArgsUsage: " ", // can't be the empty string or we get the default output
Hidden: hidden,
}
}
// Run implements a foreground runner
func Run(c *cli.Context) error {
logDirectory, logLevel := config.FindLogSettings()
+157 -141
View File
@@ -7,7 +7,9 @@ import (
"time"
"github.com/google/uuid"
capnp "zombiezen.com/go/capnproto2"
"zombiezen.com/go/capnproto2/pogs"
"zombiezen.com/go/capnproto2/rpc"
"zombiezen.com/go/capnproto2/server"
"github.com/cloudflare/cloudflared/tunnelrpc"
@@ -18,6 +20,156 @@ type RegistrationServer interface {
UnregisterConnection(ctx context.Context)
}
type RegistrationServer_PogsImpl struct {
impl RegistrationServer
}
func RegistrationServer_ServerToClient(s RegistrationServer) tunnelrpc.RegistrationServer {
return tunnelrpc.RegistrationServer_ServerToClient(RegistrationServer_PogsImpl{s})
}
func (i RegistrationServer_PogsImpl) RegisterConnection(p tunnelrpc.RegistrationServer_registerConnection) error {
server.Ack(p.Options)
auth, err := p.Params.Auth()
if err != nil {
return err
}
var pogsAuth TunnelAuth
err = pogsAuth.UnmarshalCapnproto(auth)
if err != nil {
return err
}
uuidBytes, err := p.Params.TunnelId()
if err != nil {
return err
}
tunnelID, err := uuid.FromBytes(uuidBytes)
if err != nil {
return err
}
connIndex := p.Params.ConnIndex()
options, err := p.Params.Options()
if err != nil {
return err
}
var pogsOptions ConnectionOptions
err = pogsOptions.UnmarshalCapnproto(options)
if err != nil {
return err
}
connDetails, callError := i.impl.RegisterConnection(p.Ctx, pogsAuth, tunnelID, connIndex, &pogsOptions)
resp, err := p.Results.NewResult()
if err != nil {
return err
}
if callError != nil {
if connError, err := resp.Result().NewError(); err != nil {
return err
} else {
return MarshalError(connError, callError)
}
}
if details, err := resp.Result().NewConnectionDetails(); err != nil {
return err
} else {
return connDetails.MarshalCapnproto(details)
}
}
func (i RegistrationServer_PogsImpl) UnregisterConnection(p tunnelrpc.RegistrationServer_unregisterConnection) error {
server.Ack(p.Options)
i.impl.UnregisterConnection(p.Ctx)
return nil
}
type RegistrationServer_PogsClient struct {
Client capnp.Client
Conn *rpc.Conn
}
func (c RegistrationServer_PogsClient) RegisterConnection(ctx context.Context, auth TunnelAuth, tunnelID uuid.UUID, connIndex byte, options *ConnectionOptions) (*ConnectionDetails, error) {
client := tunnelrpc.TunnelServer{Client: c.Client}
promise := client.RegisterConnection(ctx, func(p tunnelrpc.RegistrationServer_registerConnection_Params) error {
tunnelAuth, err := p.NewAuth()
if err != nil {
return err
}
if err = auth.MarshalCapnproto(tunnelAuth); err != nil {
return err
}
err = p.SetAuth(tunnelAuth)
if err != nil {
return err
}
err = p.SetTunnelId(tunnelID[:])
if err != nil {
return err
}
p.SetConnIndex(connIndex)
connectionOptions, err := p.NewOptions()
if err != nil {
return err
}
err = options.MarshalCapnproto(connectionOptions)
if err != nil {
return err
}
return nil
})
response, err := promise.Result().Struct()
if err != nil {
return nil, wrapRPCError(err)
}
result := response.Result()
switch result.Which() {
case tunnelrpc.ConnectionResponse_result_Which_error:
resultError, err := result.Error()
if err != nil {
return nil, wrapRPCError(err)
}
cause, err := resultError.Cause()
if err != nil {
return nil, wrapRPCError(err)
}
err = errors.New(cause)
if resultError.ShouldRetry() {
err = RetryErrorAfter(err, time.Duration(resultError.RetryAfter()))
}
return nil, err
case tunnelrpc.ConnectionResponse_result_Which_connectionDetails:
connDetails, err := result.ConnectionDetails()
if err != nil {
return nil, wrapRPCError(err)
}
details := new(ConnectionDetails)
if err = details.UnmarshalCapnproto(connDetails); err != nil {
return nil, wrapRPCError(err)
}
return details, nil
}
return nil, newRPCError("unknown result which %d", result.Which())
}
func (c RegistrationServer_PogsClient) UnregisterConnection(ctx context.Context) error {
client := tunnelrpc.TunnelServer{Client: c.Client}
promise := client.UnregisterConnection(ctx, func(p tunnelrpc.RegistrationServer_unregisterConnection_Params) error {
return nil
})
_, err := promise.Struct()
if err != nil {
return wrapRPCError(err)
}
return nil
}
type ClientInfo struct {
ClientID []byte `capnp:"clientId"` // must be a slice for capnp compatibility
Features []string
@@ -26,10 +178,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 {
@@ -97,140 +250,3 @@ func MarshalError(s tunnelrpc.ConnectionError, err error) error {
return nil
}
func (i TunnelServer_PogsImpl) RegisterConnection(p tunnelrpc.RegistrationServer_registerConnection) error {
server.Ack(p.Options)
auth, err := p.Params.Auth()
if err != nil {
return err
}
var pogsAuth TunnelAuth
err = pogsAuth.UnmarshalCapnproto(auth)
if err != nil {
return err
}
uuidBytes, err := p.Params.TunnelId()
if err != nil {
return err
}
tunnelID, err := uuid.FromBytes(uuidBytes)
if err != nil {
return err
}
connIndex := p.Params.ConnIndex()
options, err := p.Params.Options()
if err != nil {
return err
}
var pogsOptions ConnectionOptions
err = pogsOptions.UnmarshalCapnproto(options)
if err != nil {
return err
}
connDetails, callError := i.impl.RegisterConnection(p.Ctx, pogsAuth, tunnelID, connIndex, &pogsOptions)
resp, err := p.Results.NewResult()
if err != nil {
return err
}
if callError != nil {
if connError, err := resp.Result().NewError(); err != nil {
return err
} else {
return MarshalError(connError, callError)
}
}
if details, err := resp.Result().NewConnectionDetails(); err != nil {
return err
} else {
return connDetails.MarshalCapnproto(details)
}
}
func (i TunnelServer_PogsImpl) UnregisterConnection(p tunnelrpc.RegistrationServer_unregisterConnection) error {
server.Ack(p.Options)
i.impl.UnregisterConnection(p.Ctx)
return nil
}
func (c TunnelServer_PogsClient) RegisterConnection(ctx context.Context, auth TunnelAuth, tunnelID uuid.UUID, connIndex byte, options *ConnectionOptions) (*ConnectionDetails, error) {
client := tunnelrpc.TunnelServer{Client: c.Client}
promise := client.RegisterConnection(ctx, func(p tunnelrpc.RegistrationServer_registerConnection_Params) error {
tunnelAuth, err := p.NewAuth()
if err != nil {
return err
}
if err = auth.MarshalCapnproto(tunnelAuth); err != nil {
return err
}
err = p.SetAuth(tunnelAuth)
if err != nil {
return err
}
err = p.SetTunnelId(tunnelID[:])
if err != nil {
return err
}
p.SetConnIndex(connIndex)
connectionOptions, err := p.NewOptions()
if err != nil {
return err
}
err = options.MarshalCapnproto(connectionOptions)
if err != nil {
return err
}
return nil
})
response, err := promise.Result().Struct()
if err != nil {
return nil, wrapRPCError(err)
}
result := response.Result()
switch result.Which() {
case tunnelrpc.ConnectionResponse_result_Which_error:
resultError, err := result.Error()
if err != nil {
return nil, wrapRPCError(err)
}
cause, err := resultError.Cause()
if err != nil {
return nil, wrapRPCError(err)
}
err = errors.New(cause)
if resultError.ShouldRetry() {
err = RetryErrorAfter(err, time.Duration(resultError.RetryAfter()))
}
return nil, err
case tunnelrpc.ConnectionResponse_result_Which_connectionDetails:
connDetails, err := result.ConnectionDetails()
if err != nil {
return nil, wrapRPCError(err)
}
details := new(ConnectionDetails)
if err = details.UnmarshalCapnproto(connDetails); err != nil {
return nil, wrapRPCError(err)
}
return details, nil
}
return nil, newRPCError("unknown result which %d", result.Which())
}
func (c TunnelServer_PogsClient) UnregisterConnection(ctx context.Context) error {
client := tunnelrpc.TunnelServer{Client: c.Client}
promise := client.UnregisterConnection(ctx, func(p tunnelrpc.RegistrationServer_unregisterConnection_Params) error {
return nil
})
_, err := promise.Struct()
if err != nil {
return wrapRPCError(err)
}
return nil
}
+4
View File
@@ -62,6 +62,10 @@ func TestConnectionRegistrationRPC(t *testing.T) {
clientConn := rpc.NewConn(t2)
defer clientConn.Close()
client := TunnelServer_PogsClient{
RegistrationServer_PogsClient: RegistrationServer_PogsClient{
Client: clientConn.Bootstrap(ctx),
Conn: clientConn,
},
Client: clientConn.Bootstrap(ctx),
Conn: clientConn,
}
+3 -1
View File
@@ -210,10 +210,11 @@ type TunnelServer interface {
}
func TunnelServer_ServerToClient(s TunnelServer) tunnelrpc.TunnelServer {
return tunnelrpc.TunnelServer_ServerToClient(TunnelServer_PogsImpl{s})
return tunnelrpc.TunnelServer_ServerToClient(TunnelServer_PogsImpl{RegistrationServer_PogsImpl{s}, s})
}
type TunnelServer_PogsImpl struct {
RegistrationServer_PogsImpl
impl TunnelServer
}
@@ -268,6 +269,7 @@ func (i TunnelServer_PogsImpl) ObsoleteDeclarativeTunnelConnect(p tunnelrpc.Tunn
}
type TunnelServer_PogsClient struct {
RegistrationServer_PogsClient
Client capnp.Client
Conn *rpc.Conn
}
+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,
+301 -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,159 @@ 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 response
// 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 := result.checkErrors(); 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 {
type response struct {
Success bool `json:"success,omitempty"`
Errors []apiErr `json:"errors,omitempty"`
Messages []string `json:"messages,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
}
func (r *response) checkErrors() error {
if len(r.Errors) == 0 {
return nil
}
if len(r.Errors) == 1 {
return r.Errors[0]
}
var messages string
for _, e := range r.Errors {
messages += fmt.Sprintf("%s; ", e)
}
return fmt.Errorf("API errors: %s", messages)
}
type apiErr struct {
Code json.Number `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}
func (e apiErr) Error() string {
return fmt.Sprintf("code: %v, reason: %s", e.Code, e.Message)
}
func (r *RESTClient) statusCodeToError(op string, resp *http.Response) error {
if resp.Header.Get("Content-Type") == "application/json" {
var errorsResp response
if json.NewDecoder(resp.Body).Decode(&errorsResp) == nil {
if err := errorsResp.checkErrors(); err != nil {
return errors.Errorf("Failed to %s: %s", op, err)
}
}
}
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": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": {"cname": "new"}}`,
`{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}, {"code": 1004, "message":"Cannot use tunnel as origin for non-proxied load balancer"}], "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": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": {"pool": "unchanged", "load_balancer": "updated"}}`,
`{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}, {"code": 1004, "message":"Cannot use tunnel as origin for non-proxied load balancer"}], "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": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "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": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "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
}

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