mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-08-07 15:24:46 +00:00
Compare commits
464 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 607dcff697 | |||
| 1d1cf6a827 | |||
| 61f3fab757 | |||
| e2ff7f65fc | |||
| da0792a321 | |||
| fa061ab54e | |||
| 197d65659a | |||
| 85d0afd3b0 | |||
| 20623255dd | |||
| afa5e68fe5 | |||
| 747427f816 | |||
| f99b6c6421 | |||
| 250bc54110 | |||
| bb530b87dd | |||
| 02587c1edc | |||
| 26fc20d406 | |||
| 8a829b773a | |||
| 71b98e6111 | |||
| fee13dc62f | |||
| 094e0c7592 | |||
| b57a953caa | |||
| d8ebde37ca | |||
| 0708a49848 | |||
| b26f3082e6 | |||
| cb6d424765 | |||
| 5753aa9f18 | |||
| bfae12008d | |||
| c52e0dc8ef | |||
| 55346444c9 | |||
| ba785ec58d | |||
| 3be2545ad4 | |||
| 4a8597c245 | |||
| 741cd66c9e | |||
| 7acea1ac99 | |||
| 22d771b51d | |||
| 00d6ab2eb7 | |||
| cc0a5ac3df | |||
| 5fb938d6d6 | |||
| cd5bdb837e | |||
| 107abf9d29 | |||
| 218ee30206 | |||
| 8764fbfdfa | |||
| 2463d6b92c | |||
| 810d268c99 | |||
| f5b479dbbc | |||
| b52728e829 | |||
| 8eeb452cce | |||
| 9323844ea7 | |||
| b698fe5ef3 | |||
| edb980d439 | |||
| 310bd0dbf1 | |||
| 1a96889141 | |||
| 60de05bfc1 | |||
| 3deef6197f | |||
| 70114c2145 | |||
| 5499c77e62 | |||
| 292a7f07a2 | |||
| 679f36303a | |||
| 1b61d699c4 | |||
| a7562dff68 | |||
| 8daf1e882f | |||
| 20466dacb7 | |||
| 5b3b592108 | |||
| 9952ce0069 | |||
| 1cbc8fb8ac | |||
| edc69694cb | |||
| bd15c6b8c3 | |||
| 42fe2e7266 | |||
| 44e3be2c88 | |||
| 61d5461138 | |||
| b696ca8b1c | |||
| 6a4d020c27 | |||
| 4791ba3b87 | |||
| cf1c9a3083 | |||
| d61e3fb130 | |||
| ca7d6797e1 | |||
| af0d04d0f3 | |||
| 6274567e16 | |||
| 8836ee1dda | |||
| 7afde79600 | |||
| 3d782f7162 | |||
| 058598ea58 | |||
| 8e617df914 | |||
| 7260f3e487 | |||
| a42b66e8bd | |||
| 28d556b8d4 | |||
| 33701f50ec | |||
| abfeebf67d | |||
| 3b293048f4 | |||
| ac3638f6b1 | |||
| f7ff41f1dc | |||
| f5c8ff77e9 | |||
| 87e06100df | |||
| 1ed9e0fceb | |||
| b9d6aaebbe | |||
| a99780ed9d | |||
| 92765b4261 | |||
| 8b88b4a403 | |||
| 2ce6720a6e | |||
| 772ccc9607 | |||
| 7724ff8176 | |||
| 2a3d486126 | |||
| 932e383051 | |||
| dbe3516204 | |||
| 6fc9f1b405 | |||
| 370c17e48c | |||
| 92da73aa9d | |||
| 0c65daaa7d | |||
| b46acd7f63 | |||
| e3a9aa4296 | |||
| 3886021ba5 | |||
| 4d3ebaf984 | |||
| 9131e842a5 | |||
| 4f9cfa6542 | |||
| a1a8645294 | |||
| b95b289a8c | |||
| 425554077f | |||
| 7b2f286210 | |||
| 0f893fab47 | |||
| 12c615e2e6 | |||
| 6e5ccd7c85 | |||
| 3ec500bdbb | |||
| 8f75feac94 | |||
| 448a7798f7 | |||
| dc3a228d51 | |||
| 554c97a8cb | |||
| fd1941dfbe | |||
| 5fd348dcfc | |||
| 1a6403b2fd | |||
| 55acf7283c | |||
| acb7d604fd | |||
| 6e761cb7ae | |||
| ae8d784e36 | |||
| c716dd273c | |||
| eb3c4a7a9f | |||
| 97a901a229 | |||
| e054c5bb7b | |||
| 0d87279b2f | |||
| f8638839c0 | |||
| 2f70b05c64 | |||
| e02d09a731 | |||
| 0ff491905f | |||
| 8c59254488 | |||
| 9e76e42e3c | |||
| e376a13025 | |||
| 3a086e9cc2 | |||
| fb82b2ced5 | |||
| be0514c5c9 | |||
| 046be63253 | |||
| a908453aa4 | |||
| 7a77ead423 | |||
| b89cc22896 | |||
| 6a7418e1af | |||
| df3ad2b223 | |||
| 8c870c19a6 | |||
| 83a1dc93d8 | |||
| 2b7fbbb7b7 | |||
| 2c878c47ed | |||
| 8cc69f2a95 | |||
| 7bb9e0af4c | |||
| fbe2989f61 | |||
| c3fa4552aa | |||
| 2cf327ba01 | |||
| dd0881f32b | |||
| 41c358147c | |||
| 976eb24883 | |||
| c782716e49 | |||
| f18209af7d | |||
| 6d63f84a75 | |||
| 1c6ea36e73 | |||
| 06f29306cd | |||
| 322f909edb | |||
| a37da2b165 | |||
| 710f66b0bb | |||
| 0b2b6c8e12 | |||
| acea15161c | |||
| c76283a2b4 | |||
| ae374c0463 | |||
| 42246f986c | |||
| 80f387214c | |||
| a368fbbe9b | |||
| 32df01a9da | |||
| 6dcf3a4cbc | |||
| 96f11de7ab | |||
| d03469b6d3 | |||
| 0cf6ce9aeb | |||
| e8f55cc911 | |||
| 35cee13175 | |||
| 5376df5439 | |||
| db9b6541d0 | |||
| 5bd4028ea7 | |||
| 1b2a96f96b | |||
| d50fee4fa0 | |||
| 6b3e2b020b | |||
| 6624a24040 | |||
| 7b81cf8aa6 | |||
| 26f5f80811 | |||
| 29f4650e25 | |||
| a14aa0322c | |||
| 8f9bbcb9a0 | |||
| afc2cd38e1 | |||
| a5f67091bf | |||
| 464bb53049 | |||
| 6488843ac4 | |||
| 52ab2c8227 | |||
| 269351bbea | |||
| a83b6a2155 | |||
| a60c0273f5 | |||
| d6c2c4ee4a | |||
| 5b1bea7892 | |||
| 54b386188a | |||
| 386b02355a | |||
| 203b939614 | |||
| e729dfc51e | |||
| d5139d3882 | |||
| e31ff3a70f | |||
| dfe61fda88 | |||
| 053b2c17f1 | |||
| 7367827a11 | |||
| 7d7bdffde5 | |||
| 7e31b77646 | |||
| 87102a2646 | |||
| 789ca6f6f4 | |||
| cc2a1d1204 | |||
| 6aa48d2eb2 | |||
| 8b43454024 | |||
| 5e7ca14412 | |||
| b499c0fdba | |||
| bbf31377c2 | |||
| 8f4fd70783 | |||
| 92736b2677 | |||
| 379cb16efe | |||
| 43babbc2f9 | |||
| dfd1ca5fb5 | |||
| f51712bef9 | |||
| b0d31a0ef3 | |||
| dd614881b6 | |||
| 23e12cf5a3 | |||
| 9605f00c77 | |||
| 871c3a194f | |||
| 8ee1faf317 | |||
| c5bacf4d95 | |||
| 1ba5abfdb3 | |||
| 7173da9359 | |||
| 0676923d24 | |||
| ca7fbf43da | |||
| 6ea9b5c3ff | |||
| 1d96bccc04 | |||
| c5af83af66 | |||
| 759cd019be | |||
| 13bf65ce4e | |||
| 3a2e12818f | |||
| e14ec1a1fb | |||
| 3a9a0a0d75 | |||
| 068b148e05 | |||
| e5335b6c1b | |||
| fa841fc89a | |||
| e9a8c9850f | |||
| a52f47c9a9 | |||
| ad9559c66a | |||
| 7133eceb9b | |||
| e0c2758b64 | |||
| 5464408ea7 | |||
| 802e538c42 | |||
| 79065514ff | |||
| 51acf2ef08 | |||
| 1f6a330098 | |||
| 28cc1c65af | |||
| 6322c5029d | |||
| 4bb5b97518 | |||
| 99daa76715 | |||
| babe684141 | |||
| 8b6e3bc1d1 | |||
| 95704b11fb | |||
| 4d2583edf5 | |||
| a4b3ee5959 | |||
| 91d9dca34e | |||
| dbde3870da | |||
| b3bcce97da | |||
| a99fac1e31 | |||
| 71d66ae7ee | |||
| 133e6fdc88 | |||
| 1d5cc45ac7 | |||
| a412f629c2 | |||
| 979e5be8ab | |||
| 2789d0cf36 | |||
| 5bcb2da0fe | |||
| 4f23da2a6d | |||
| fe032843f3 | |||
| ff795a7beb | |||
| 40d9370bb6 | |||
| 02f0ed951f | |||
| c2a71c5a51 | |||
| 945bf76897 | |||
| d3b254f9ae | |||
| ee588eeeaa | |||
| dd521aba29 | |||
| a06390a078 | |||
| 2cac1f0f78 | |||
| a10785a701 | |||
| dc730615f2 | |||
| b836cb350a | |||
| 5e85a8bd16 | |||
| f41121f4e5 | |||
| 93a9504824 | |||
| 7abbe91d41 | |||
| cf314ddb58 | |||
| ff97fb6dc8 | |||
| dc48cdce1a | |||
| f130e6d4d7 | |||
| ef5b44b2d0 | |||
| 858ef29868 | |||
| baec3e289e | |||
| df25ed9bde | |||
| 4e1df1a211 | |||
| c3c88cc31e | |||
| fb8ff33203 | |||
| 188f4667cb | |||
| 30c9e2af9b | |||
| 66e087a825 | |||
| 47254113ee | |||
| 026f2d1cc9 | |||
| e9c9bf3cbd | |||
| 5da2109811 | |||
| 28f6c2ed7c | |||
| f6c7f8a96c | |||
| 1d73c2752e | |||
| 5afbb3fd6d | |||
| fd4ab314dc | |||
| 993a9bc4b9 | |||
| 8d1ea7202a | |||
| 9adbab96af | |||
| bdd70e798a | |||
| 5feba7e3a9 | |||
| d3d9460f64 | |||
| 74f3a55c57 | |||
| 3c93d9b300 | |||
| 3c0ea0db65 | |||
| 7c03e2cbd2 | |||
| 8f25704a90 | |||
| 583bad4972 | |||
| b5f340d64c | |||
| 9cd0b7048a | |||
| 4858ce79d0 | |||
| 4090049fff | |||
| e255a7da26 | |||
| c2a3ac3991 | |||
| 2fa09e1cc6 | |||
| 0a742feb98 | |||
| 80a15547e3 | |||
| d26a8c5d44 | |||
| ca619a97bc | |||
| 25a04e0c69 | |||
| d3f9aa2ae7 | |||
| d32fb8e82c | |||
| acd17f6ab6 | |||
| 1ca841d220 | |||
| 39d60d1239 | |||
| 713a2d689e | |||
| babcd9fe2b | |||
| a1403fe968 | |||
| 1485ca0fc7 | |||
| 4bff1ef9df | |||
| 25cfffd0d1 | |||
| 4662e40068 | |||
| 38d78f9844 | |||
| 6f8708d33c | |||
| fa17b0200f | |||
| 14f50d0922 | |||
| 945320880a | |||
| 809d2f3f28 | |||
| 137928ecaf | |||
| 8183af31e2 | |||
| 28f890a701 | |||
| bab7583a97 | |||
| 2e2fa29637 | |||
| 2bef5dbe72 | |||
| 13d25a52a9 | |||
| 9a43a92b1c | |||
| 102b364cc9 | |||
| c18702f297 | |||
| 980bee22a5 | |||
| 80d3d989eb | |||
| 6804a5ff9d | |||
| b5dab1f5da | |||
| 8c6cfa34aa | |||
| 8560436487 | |||
| 619bc95501 | |||
| 781e1fef7b | |||
| 541cf68608 | |||
| eafc89bf73 | |||
| fd451ca158 | |||
| d22e214000 | |||
| 92d6d73f9c | |||
| b02718f86b | |||
| 5a0b7ecb62 | |||
| 07a409ffef | |||
| 58e5e9c9a4 | |||
| 6ca642e572 | |||
| 073c5bfdaa | |||
| fea3569956 | |||
| abdbc76a46 | |||
| f22202b31b | |||
| 4586ed3e51 | |||
| 7475e3e487 | |||
| e025a4cd7b | |||
| 850f804c47 | |||
| 1558280185 | |||
| 15d68a0eed | |||
| e3aed15c08 | |||
| 99713e3439 | |||
| 27c6977746 | |||
| 200f9a3786 | |||
| 92defa26d4 | |||
| c9f1c1ec70 | |||
| 10e4a2940c | |||
| 77f12c7c34 | |||
| ca33ed9f6a | |||
| 6a331b13ba | |||
| 47c878b9c4 | |||
| 61cd4a918d | |||
| 9db4b7fdfb | |||
| 2f59b8ac22 | |||
| f94699e07b | |||
| f7cf597f54 | |||
| aeb8dd7e51 | |||
| 5bf6dd8f85 | |||
| 62b1ab8c98 | |||
| 8de19dc647 | |||
| 8a099d8f6e | |||
| ef400afe00 | |||
| bf596c035e | |||
| 36276e957a | |||
| c69437ba83 | |||
| b2d0c612a5 | |||
| 8e2908f889 | |||
| 3e8d886c25 | |||
| 446c5cf60c | |||
| 13f88b3739 | |||
| 69ee6c1d88 | |||
| 192ae35728 | |||
| 10d547f528 | |||
| b59fd4b7d8 | |||
| c85c8526e8 | |||
| f49d9dcb67 | |||
| c2ac282aca | |||
| 58daf6bfed | |||
| 611b284e20 | |||
| 236a0a164d | |||
| 148eea5899 | |||
| 83c6c8713b | |||
| 2b820d790c | |||
| 36286301f7 | |||
| 9a48fe959d | |||
| f6014cb2b4 | |||
| 9fe21fa906 | |||
| 80a75e91d2 | |||
| 6acc95f756 | |||
| 72412db4c2 | |||
| fa92441415 | |||
| 41916365b6 | |||
| 41429cc6a8 | |||
| da0defcec9 | |||
| ca9902a8d1 |
@@ -0,0 +1,8 @@
|
||||
images:
|
||||
- name: cloudflared
|
||||
dockerfile: Dockerfile
|
||||
context: .
|
||||
registries:
|
||||
- name: docker.io/cloudflare
|
||||
user: env:DOCKER_USER
|
||||
password: env:DOCKER_PASSWORD
|
||||
+11
@@ -5,5 +5,16 @@ guide/public
|
||||
/.GOPATH
|
||||
/bin
|
||||
.idea
|
||||
.build
|
||||
.vscode
|
||||
\#*\#
|
||||
cscope.*
|
||||
cloudflared
|
||||
cloudflared.pkg
|
||||
cloudflared.exe
|
||||
cloudflared.msi
|
||||
cloudflared-x86-64*
|
||||
!cmd/cloudflared/
|
||||
.DS_Store
|
||||
*-session.log
|
||||
ssh_server_tests/.env
|
||||
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# uninstall first in case this is an upgrade
|
||||
/usr/local/bin/cloudflared service uninstall
|
||||
|
||||
# install the new service using launchctl
|
||||
/usr/local/bin/cloudflared service install
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
/usr/local/bin/cloudflared service uninstall
|
||||
rm /usr/local/bin/cloudflared
|
||||
pkgutil --forget com.cloudflare.cloudflared
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "$(uname)" != "Darwin" ]] ; then
|
||||
echo "This should be run on macOS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
go version
|
||||
export GO111MODULE=on
|
||||
|
||||
# build 'cloudflared-darwin-amd64.tgz'
|
||||
mkdir -p artifacts
|
||||
FILENAME="$(pwd)/artifacts/cloudflared-darwin-amd64.tgz"
|
||||
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
|
||||
|
||||
# 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
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
FILENAME="${PWD}/artifacts/cloudflared-darwin-amd64.tgz"
|
||||
|
||||
if ! VERSION="$(git describe --tags --exact-match 2>/dev/null)" ; then
|
||||
echo "Skipping public release for an untagged commit."
|
||||
echo "##teamcity[buildStatus status='SUCCESS' text='Skipped due to lack of tag']"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ ! -f "$FILENAME" ]] ; then
|
||||
echo "Missing $FILENAME"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${GITHUB_PRIVATE_KEY:-}" == "" ]] ; then
|
||||
echo "Missing GITHUB_PRIVATE_KEY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# upload to s3 bucket for use by Homebrew formula
|
||||
s3cmd \
|
||||
--acl-public --signature-v2 --access_key="$AWS_ACCESS_KEY_ID" --secret_key="$AWS_SECRET_ACCESS_KEY" --host-bucket="%(bucket)s.s3.cfdata.org" \
|
||||
put "$FILENAME" "s3://cftunnel-docs/dl/cloudflared-$VERSION-darwin-amd64.tgz"
|
||||
s3cmd \
|
||||
--acl-public --signature-v2 --access_key="$AWS_ACCESS_KEY_ID" --secret_key="$AWS_SECRET_ACCESS_KEY" --host-bucket="%(bucket)s.s3.cfdata.org" \
|
||||
cp "s3://cftunnel-docs/dl/cloudflared-$VERSION-darwin-amd64.tgz" "s3://cftunnel-docs/dl/cloudflared-stable-darwin-amd64.tgz"
|
||||
SHA256=$(sha256sum "$FILENAME" | cut -b1-64)
|
||||
|
||||
# set up git (note that UserKnownHostsFile is an absolute path so we can cd wherever)
|
||||
mkdir -p tmp
|
||||
ssh-keyscan -t rsa github.com > tmp/github.txt
|
||||
echo "$GITHUB_PRIVATE_KEY" > tmp/private.key
|
||||
chmod 0400 tmp/private.key
|
||||
export GIT_SSH_COMMAND="ssh -o UserKnownHostsFile=$PWD/tmp/github.txt -i $PWD/tmp/private.key -o IdentitiesOnly=yes"
|
||||
|
||||
# clone Homebrew repo into tmp/homebrew-cloudflare
|
||||
git clone git@github.com:cloudflare/homebrew-cloudflare.git tmp/homebrew-cloudflare
|
||||
cd tmp/homebrew-cloudflare
|
||||
git checkout -f master
|
||||
git reset --hard origin/master
|
||||
|
||||
# modify cloudflared.rb
|
||||
URL="https://packages.argotunnel.com/dl/cloudflared-$VERSION-darwin-amd64.tgz"
|
||||
tee cloudflared.rb <<EOF
|
||||
class Cloudflared < Formula
|
||||
desc 'Argo Tunnel'
|
||||
homepage 'https://developers.cloudflare.com/argo-tunnel/'
|
||||
url '$URL'
|
||||
sha256 '$SHA256'
|
||||
version '$VERSION'
|
||||
def install
|
||||
bin.install 'cloudflared'
|
||||
end
|
||||
end
|
||||
EOF
|
||||
|
||||
# push cloudflared.rb
|
||||
git add cloudflared.rb
|
||||
git diff
|
||||
git config user.name "cloudflare-warp-bot"
|
||||
git config user.email "warp-bot@cloudflare.com"
|
||||
git commit -m "Release Argo Tunnel $VERSION"
|
||||
|
||||
git push -v origin master
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
# use a builder image for building cloudflare
|
||||
ARG TARGET_GOOS
|
||||
ARG TARGET_GOARCH
|
||||
FROM golang:1.13.3 as builder
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0 \
|
||||
TARGET_GOOS=${TARGET_GOOS} \
|
||||
TARGET_GOARCH=${TARGET_GOARCH}
|
||||
|
||||
WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
||||
|
||||
# copy our sources into the builder image
|
||||
COPY . .
|
||||
|
||||
# compile cloudflared
|
||||
RUN make cloudflared
|
||||
|
||||
# use a distroless base image with glibc
|
||||
FROM gcr.io/distroless/base-debian10:nonroot
|
||||
|
||||
# copy our compiled binary
|
||||
COPY --from=builder --chown=nonroot /go/src/github.com/cloudflare/cloudflared/cloudflared /usr/local/bin/
|
||||
|
||||
# run as non-privileged user
|
||||
USER nonroot
|
||||
|
||||
# command / entrypoint of container
|
||||
ENTRYPOINT ["cloudflared", "--no-autoupdate"]
|
||||
CMD ["version"]
|
||||
Generated
-432
@@ -1,432 +0,0 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/BurntSushi/toml"
|
||||
packages = ["."]
|
||||
revision = "b26d9c308763d68093482582cea63d69be07a0f0"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/beorn7/perks"
|
||||
packages = ["quantile"]
|
||||
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/certifi/gocertifi"
|
||||
packages = ["."]
|
||||
revision = "deb3ae2ef2610fde3330947281941c562861188b"
|
||||
version = "2018.01.18"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/cloudflare/brotli-go"
|
||||
packages = ["."]
|
||||
revision = "18c9f6c67e3dfc12e0ddaca748d2887f97a7ac28"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/coredns/coredns"
|
||||
packages = [
|
||||
"core/dnsserver",
|
||||
"coremain",
|
||||
"pb",
|
||||
"plugin",
|
||||
"plugin/cache",
|
||||
"plugin/cache/freq",
|
||||
"plugin/etcd/msg",
|
||||
"plugin/metrics",
|
||||
"plugin/metrics/vars",
|
||||
"plugin/pkg/cache",
|
||||
"plugin/pkg/dnstest",
|
||||
"plugin/pkg/dnsutil",
|
||||
"plugin/pkg/doh",
|
||||
"plugin/pkg/edns",
|
||||
"plugin/pkg/fuzz",
|
||||
"plugin/pkg/log",
|
||||
"plugin/pkg/nonwriter",
|
||||
"plugin/pkg/rcode",
|
||||
"plugin/pkg/response",
|
||||
"plugin/pkg/trace",
|
||||
"plugin/pkg/uniq",
|
||||
"plugin/pkg/watch",
|
||||
"plugin/test",
|
||||
"request"
|
||||
]
|
||||
revision = "992e7928c7c258628d2b13b769acc86781b9faea"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/coreos/go-systemd"
|
||||
packages = ["daemon"]
|
||||
revision = "39ca1b05acc7ad1220e09f133283b8859a8b71ab"
|
||||
version = "v17"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/elgs/gosqljson"
|
||||
packages = ["."]
|
||||
revision = "027aa4915315a0b2825c0f025cea347829b974fa"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/equinox-io/equinox"
|
||||
packages = [
|
||||
".",
|
||||
"internal/go-update",
|
||||
"internal/go-update/internal/binarydist",
|
||||
"internal/go-update/internal/osext",
|
||||
"internal/osext",
|
||||
"proto"
|
||||
]
|
||||
revision = "f24972fa72facf59d05c91c848b65eac38815915"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/facebookgo/grace"
|
||||
packages = ["gracenet"]
|
||||
revision = "75cf19382434e82df4dd84953f566b8ad23d6e9e"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/flynn/go-shlex"
|
||||
packages = ["."]
|
||||
revision = "3f9db97f856818214da2e1057f8ad84803971cff"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/getsentry/raven-go"
|
||||
packages = ["."]
|
||||
revision = "ed7bcb39ff10f39ab08e317ce16df282845852fa"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/golang-collections/collections"
|
||||
packages = ["queue"]
|
||||
revision = "604e922904d35e97f98a774db7881f049cd8d970"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/golang/protobuf"
|
||||
packages = [
|
||||
"proto",
|
||||
"ptypes",
|
||||
"ptypes/any",
|
||||
"ptypes/duration",
|
||||
"ptypes/timestamp"
|
||||
]
|
||||
revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/google/uuid"
|
||||
packages = ["."]
|
||||
revision = "064e2069ce9c359c118179501254f67d7d37ba24"
|
||||
version = "0.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gorilla/context"
|
||||
packages = ["."]
|
||||
revision = "08b5f424b9271eedf6f9f0ce86cb9396ed337a42"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gorilla/mux"
|
||||
packages = ["."]
|
||||
revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf"
|
||||
version = "v1.6.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gorilla/websocket"
|
||||
packages = ["."]
|
||||
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/grpc-ecosystem/grpc-opentracing"
|
||||
packages = ["go/otgrpc"]
|
||||
revision = "8e809c8a86450a29b90dcc9efbf062d0fe6d9746"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/lib/pq"
|
||||
packages = [
|
||||
".",
|
||||
"oid"
|
||||
]
|
||||
revision = "90697d60dd844d5ef6ff15135d0203f65d2f53b8"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/mattn/go-colorable"
|
||||
packages = ["."]
|
||||
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
|
||||
version = "v0.0.9"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/mattn/go-isatty"
|
||||
packages = ["."]
|
||||
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
||||
version = "v0.0.3"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/matttproud/golang_protobuf_extensions"
|
||||
packages = ["pbutil"]
|
||||
revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mholt/caddy"
|
||||
packages = [
|
||||
".",
|
||||
"caddyfile",
|
||||
"telemetry"
|
||||
]
|
||||
revision = "d3b731e9255b72d4571a5aac125634cf1b6031dc"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/miekg/dns"
|
||||
packages = ["."]
|
||||
revision = "5a2b9fab83ff0f8bfc99684bd5f43a37abe560f1"
|
||||
version = "v1.0.8"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mitchellh/go-homedir"
|
||||
packages = ["."]
|
||||
revision = "3864e76763d94a6df2f9960b16a20a33da9f9a66"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/opentracing/opentracing-go"
|
||||
packages = [
|
||||
".",
|
||||
"ext",
|
||||
"log"
|
||||
]
|
||||
revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38"
|
||||
version = "v1.0.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/prometheus/client_golang"
|
||||
packages = [
|
||||
"prometheus",
|
||||
"prometheus/promhttp"
|
||||
]
|
||||
revision = "967789050ba94deca04a5e84cce8ad472ce313c1"
|
||||
version = "v0.9.0-pre1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/prometheus/client_model"
|
||||
packages = ["go"]
|
||||
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/prometheus/common"
|
||||
packages = [
|
||||
"expfmt",
|
||||
"internal/bitbucket.org/ww/goautoneg",
|
||||
"model"
|
||||
]
|
||||
revision = "7600349dcfe1abd18d72d3a1770870d9800a7801"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/prometheus/procfs"
|
||||
packages = [
|
||||
".",
|
||||
"internal/util",
|
||||
"nfs",
|
||||
"xfs"
|
||||
]
|
||||
revision = "ae68e2d4c00fed4943b5f6698d504a5fe083da8a"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/rifflock/lfshook"
|
||||
packages = ["."]
|
||||
revision = "bf539943797a1f34c1f502d07de419b5238ae6c6"
|
||||
version = "v2.3"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
|
||||
version = "v1.0.5"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = ["assert"]
|
||||
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
|
||||
version = "v1.2.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"ed25519",
|
||||
"ed25519/internal/edwards25519",
|
||||
"ssh/terminal"
|
||||
]
|
||||
revision = "a49355c7e3f8fe157a85be2f77e6e269a0f89602"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"bpf",
|
||||
"context",
|
||||
"http/httpguts",
|
||||
"http2",
|
||||
"http2/hpack",
|
||||
"idna",
|
||||
"internal/iana",
|
||||
"internal/socket",
|
||||
"internal/timeseries",
|
||||
"ipv4",
|
||||
"ipv6",
|
||||
"trace",
|
||||
"websocket"
|
||||
]
|
||||
revision = "32a936f46389aa10549d60bd7833e54b01685d09"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sync"
|
||||
packages = ["errgroup"]
|
||||
revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = [
|
||||
"unix",
|
||||
"windows",
|
||||
"windows/registry",
|
||||
"windows/svc",
|
||||
"windows/svc/eventlog",
|
||||
"windows/svc/mgr"
|
||||
]
|
||||
revision = "ce36f3865eeb42541ce3f87f32f8462c5687befa"
|
||||
|
||||
[[projects]]
|
||||
name = "golang.org/x/text"
|
||||
packages = [
|
||||
"collate",
|
||||
"collate/build",
|
||||
"internal/colltab",
|
||||
"internal/gen",
|
||||
"internal/tag",
|
||||
"internal/triegen",
|
||||
"internal/ucd",
|
||||
"language",
|
||||
"secure/bidirule",
|
||||
"transform",
|
||||
"unicode/bidi",
|
||||
"unicode/cldr",
|
||||
"unicode/norm",
|
||||
"unicode/rangetable"
|
||||
]
|
||||
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "google.golang.org/genproto"
|
||||
packages = ["googleapis/rpc/status"]
|
||||
revision = "ff3583edef7de132f219f0efc00e097cabcc0ec0"
|
||||
|
||||
[[projects]]
|
||||
name = "google.golang.org/grpc"
|
||||
packages = [
|
||||
".",
|
||||
"balancer",
|
||||
"balancer/base",
|
||||
"balancer/roundrobin",
|
||||
"codes",
|
||||
"connectivity",
|
||||
"credentials",
|
||||
"encoding",
|
||||
"encoding/proto",
|
||||
"grpclog",
|
||||
"internal",
|
||||
"internal/backoff",
|
||||
"internal/channelz",
|
||||
"internal/grpcrand",
|
||||
"keepalive",
|
||||
"metadata",
|
||||
"naming",
|
||||
"peer",
|
||||
"resolver",
|
||||
"resolver/dns",
|
||||
"resolver/passthrough",
|
||||
"stats",
|
||||
"status",
|
||||
"tap",
|
||||
"transport"
|
||||
]
|
||||
revision = "168a6198bcb0ef175f7dacec0b8691fc141dc9b8"
|
||||
version = "v1.13.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "altsrc-parse-durations"
|
||||
name = "gopkg.in/urfave/cli.v2"
|
||||
packages = [
|
||||
".",
|
||||
"altsrc"
|
||||
]
|
||||
revision = "d604b6ffeee878fbf084fd2761466b6649989cee"
|
||||
source = "https://github.com/cbranch/cli"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
|
||||
version = "v2.2.1"
|
||||
|
||||
[[projects]]
|
||||
name = "zombiezen.com/go/capnproto2"
|
||||
packages = [
|
||||
".",
|
||||
"encoding/text",
|
||||
"internal/fulfiller",
|
||||
"internal/nodemap",
|
||||
"internal/packed",
|
||||
"internal/queue",
|
||||
"internal/schema",
|
||||
"internal/strquote",
|
||||
"pogs",
|
||||
"rpc",
|
||||
"rpc/internal/refcount",
|
||||
"schemas",
|
||||
"server",
|
||||
"std/capnp/rpc"
|
||||
]
|
||||
revision = "7cfd211c19c7f5783c695f3654efa46f0df259c3"
|
||||
source = "https://github.com/zombiezen/go-capnproto2"
|
||||
version = "v2.17.1"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "9aac6a0260101988582bfa55d39559e53cf8ccd7e37caad3350aeea768f5bdb2"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
|
||||
[[prune.project]]
|
||||
name = "github.com/cloudflare/brotli-go"
|
||||
unused-packages = false
|
||||
|
||||
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/facebookgo/grace"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/getsentry/raven-go"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/pkg/errors"
|
||||
version = "0.8.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/prometheus/client_golang"
|
||||
version = "0.9.0-pre1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
version = "1.0.3"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "1.2.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "golang.org/x/net"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "golang.org/x/sync"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/urfave/cli.v2"
|
||||
source = "https://github.com/cbranch/cli"
|
||||
branch = "altsrc-parse-durations"
|
||||
|
||||
[[constraint]]
|
||||
name = "zombiezen.com/go/capnproto2"
|
||||
source = "https://github.com/zombiezen/go-capnproto2"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gorilla/websocket"
|
||||
version = "1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/coredns/coredns"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/cloudflare/brotli-go"
|
||||
branch = "master"
|
||||
|
||||
[[override]]
|
||||
name = "github.com/mholt/caddy"
|
||||
branch = "master"
|
||||
@@ -1,50 +1,190 @@
|
||||
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
|
||||
INSTALL_BINDIR := usr/local/bin
|
||||
INSTALL_BINDIR := /usr/bin/
|
||||
MAN_DIR := /usr/share/man/man1/
|
||||
|
||||
EQUINOX_FLAGS = --version="$(VERSION)" \
|
||||
--platforms="$(EQUINOX_BUILD_PLATFORMS)" \
|
||||
--app="$(EQUINOX_APP_ID)" \
|
||||
--token="$(EQUINOX_TOKEN)" \
|
||||
--channel="$(EQUINOX_CHANNEL)"
|
||||
--platforms="$(EQUINOX_BUILD_PLATFORMS)" \
|
||||
--app="$(EQUINOX_APP_ID)" \
|
||||
--token="$(EQUINOX_TOKEN)" \
|
||||
--channel="$(EQUINOX_CHANNEL)"
|
||||
|
||||
ifeq ($(EQUINOX_IS_DRAFT), true)
|
||||
EQUINOX_FLAGS := --draft $(EQUINOX_FLAGS)
|
||||
endif
|
||||
|
||||
LOCAL_ARCH ?= $(shell uname -m)
|
||||
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)
|
||||
TARGET_ARCH ?= arm64
|
||||
else ifeq ($(shell echo $(LOCAL_ARCH) | head -c 4),armv)
|
||||
TARGET_ARCH ?= arm
|
||||
else
|
||||
$(error This system's architecture $(LOCAL_ARCH) isn't supported)
|
||||
endif
|
||||
|
||||
LOCAL_OS ?= $(shell go env GOOS)
|
||||
ifeq ($(LOCAL_OS),linux)
|
||||
TARGET_OS ?= linux
|
||||
else ifeq ($(LOCAL_OS),darwin)
|
||||
TARGET_OS ?= darwin
|
||||
else ifeq ($(LOCAL_OS),windows)
|
||||
TARGET_OS ?= windows
|
||||
else ifeq ($(LOCAL_OS),freebsd)
|
||||
TARGET_OS ?= freebsd
|
||||
else
|
||||
$(error This system's OS $(LOCAL_OS) isn't supported)
|
||||
endif
|
||||
|
||||
ifeq ($(TARGET_OS), windows)
|
||||
EXECUTABLE_PATH=./cloudflared.exe
|
||||
else
|
||||
EXECUTABLE_PATH=./cloudflared
|
||||
endif
|
||||
|
||||
ifeq ($(FLAVOR), centos-7)
|
||||
TARGET_PUBLIC_REPO ?= el7
|
||||
else
|
||||
TARGET_PUBLIC_REPO ?= $(FLAVOR)
|
||||
endif
|
||||
|
||||
.PHONY: all
|
||||
all: cloudflared test
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
go clean
|
||||
|
||||
.PHONY: cloudflared
|
||||
cloudflared:
|
||||
go build -v $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/cloudflared
|
||||
cloudflared: tunnel-deps
|
||||
GOOS=$(TARGET_OS) GOARCH=$(TARGET_ARCH) go build -v -mod=vendor $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/cloudflared
|
||||
|
||||
.PHONY: container
|
||||
container:
|
||||
docker build --build-arg=TARGET_ARCH=$(TARGET_ARCH) --build-arg=TARGET_OS=$(TARGET_OS) -t cloudflare/cloudflared-$(TARGET_OS)-$(TARGET_ARCH):"$(VERSION)" .
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
go test -v -race $(VERSION_FLAGS) ./...
|
||||
test: vet
|
||||
go test -v -mod=vendor -race $(VERSION_FLAGS) ./...
|
||||
|
||||
.PHONY: test-ssh-server
|
||||
test-ssh-server:
|
||||
docker-compose -f ssh_server_tests/docker-compose.yml up
|
||||
|
||||
define publish_package
|
||||
for HOST in $(CF_PKG_HOSTS); do \
|
||||
ssh-keyscan -t rsa $$HOST >> ~/.ssh/known_hosts; \
|
||||
scp -4 cloudflared*.$(1) cfsync@$$HOST:/state/cf-pkg/staging/$(2)/$(TARGET_PUBLIC_REPO)/cloudflared/; \
|
||||
done
|
||||
endef
|
||||
|
||||
.PHONY: publish-deb
|
||||
publish-deb: cloudflared-deb
|
||||
$(call publish_package,deb,apt)
|
||||
|
||||
.PHONY: publish-rpm
|
||||
publish-rpm: cloudflared-rpm
|
||||
$(call publish_package,rpm,yum)
|
||||
|
||||
define build_package
|
||||
mkdir -p $(PACKAGE_DIR)
|
||||
cp cloudflared $(PACKAGE_DIR)/cloudflared
|
||||
cat cloudflared_man_template | sed -e 's/\$${VERSION}/$(VERSION)/; s/\$${DATE}/$(DATE)/' > $(PACKAGE_DIR)/cloudflared.1
|
||||
fakeroot fpm -C $(PACKAGE_DIR) -s dir -t $(1) --$(1)-compression bzip2 \
|
||||
--description 'Cloudflare Argo tunnel daemon' \
|
||||
--vendor 'Cloudflare' \
|
||||
--license 'Cloudflare Service Agreement' \
|
||||
--url 'https://github.com/cloudflare/cloudflared' \
|
||||
-m 'Cloudflare <support@cloudflare.com>' \
|
||||
-a $(TARGET_ARCH) -v $(VERSION) -n cloudflared --after-install postinst.sh --after-remove postrm.sh \
|
||||
cloudflared=$(INSTALL_BINDIR) cloudflared.1=$(MAN_DIR)
|
||||
endef
|
||||
|
||||
.PHONY: cloudflared-deb
|
||||
cloudflared-deb: cloudflared
|
||||
mkdir -p $(PACKAGE_DIR)
|
||||
cp cloudflared $(PACKAGE_DIR)/cloudflared
|
||||
fakeroot fpm -C $(PACKAGE_DIR) -s dir -t deb --deb-compression bzip2 \
|
||||
-a $(GOARCH) -v $(VERSION) -n cloudflared
|
||||
$(call build_package,deb)
|
||||
|
||||
.PHONY: cloudflared-rpm
|
||||
cloudflared-rpm: cloudflared
|
||||
$(call build_package,rpm)
|
||||
|
||||
.PHONY: cloudflared-darwin-amd64.tgz
|
||||
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
|
||||
aws s3 --endpoint-url $(S3_ENDPOINT) cp --acl public-read $(S3_URI)/cloudflared-$$(VERSION)-$1.tgz $(S3_URI)/cloudflared-stable-$1.tgz
|
||||
|
||||
.PHONY: homebrew-release
|
||||
|
||||
.PHONY: homebrew-release
|
||||
homebrew-release: homebrew-upload
|
||||
./publish-homebrew-formula.sh cloudflared-darwin-amd64.tgz $(VERSION) homebrew-cloudflare
|
||||
|
||||
@@ -52,6 +192,37 @@ homebrew-release: homebrew-upload
|
||||
release: bin/equinox
|
||||
bin/equinox release $(EQUINOX_FLAGS) -- $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/cloudflared
|
||||
|
||||
.PHONY: github-release
|
||||
github-release: cloudflared
|
||||
python3 github_release.py --path $(EXECUTABLE_PATH) --release-version $(VERSION)
|
||||
|
||||
.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/
|
||||
|
||||
.PHONY: tunnel-deps
|
||||
tunnel-deps: tunnelrpc/tunnelrpc.capnp.go
|
||||
|
||||
tunnelrpc/tunnelrpc.capnp.go: tunnelrpc/tunnelrpc.capnp
|
||||
which capnp # https://capnproto.org/install.html
|
||||
which capnpc-go # go get zombiezen.com/go/capnproto2/capnpc-go
|
||||
capnp compile -ogo tunnelrpc/tunnelrpc.capnp
|
||||
|
||||
.PHONY: vet
|
||||
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)
|
||||
+513
@@ -1,3 +1,516 @@
|
||||
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
|
||||
|
||||
2020.7.2
|
||||
- 2020-07-02 AUTH-2644: Change install location and add man page
|
||||
- 2020-07-02 TUN-3131: Allow user to specify tunnel credentials path, and remove it in tunnel delete command
|
||||
- 2020-07-03 TUN-3008: Implement cloudflared tunnel cleanup command
|
||||
- 2020-07-02 TUN-3150: cloudflared tunnel list's table should use intelligent column width
|
||||
- 2020-07-07 TUN-3169: Move on to the next address when edge returns duplicate connection. There's no point in trying to connect to the same address since it will be hashed to the same metal. Improve logging of errors from serve tunnel loop, hide useless context cancelled error.
|
||||
- 2020-07-08 beautify package meta information generated by fpm (#218)
|
||||
- 2020-07-06 AUTH-2871: fix rpm builds
|
||||
- 2020-07-08 AUTH-2858: Set file to disable autoupdate
|
||||
- 2020-07-09 AUTH-2872: Adds centos-6 build
|
||||
|
||||
2020.7.1
|
||||
- 2020-07-02 DEVTOOLS-7321: Push GitHub homebrew updates to master
|
||||
- 2020-07-06 TUN-3161: Upgrade golang.org/x/ deps
|
||||
- 2020-06-30 AUTH-2854: Create cloudflared RPMs
|
||||
- 2020-06-26 AUTH-2850 log config file path
|
||||
|
||||
2020.7.0
|
||||
- 2020-06-30 TUN-3140: Add timestamps to terminal log entries
|
||||
- 2020-06-30 AUTH-2860: Fix builds
|
||||
- 2020-06-25 TUN-3007: Implement named tunnel connection registration and unregistration.
|
||||
|
||||
2020.6.6
|
||||
- 2020-06-23 AUTH-2685: Adds script to create release
|
||||
- 2020-06-25 AUTH-2652: Update cloudflare repo
|
||||
- 2020-06-26 AUTH-2718: Add target for publishing deb to pkg.cloudflare repo
|
||||
- 2020-06-26 AUTH-2849 all log output to stderr
|
||||
- 2020-06-17 TUN-3106: Pass NamedTunnel config to StartServer
|
||||
- 2020-06-18 TUN-3107: UnregisterConnection shouldn't wrap nil error as RPC error
|
||||
- 2020-06-17 AUTH-2652: Adds .docker-images to push images to docker hub
|
||||
- 2020-06-18 AUTH-2712 mac package build script and better config file handling when started as a service
|
||||
|
||||
2020.6.5
|
||||
- 2020-06-16 DEVTOOLS-7321: Don't skip macOS builds based on tag
|
||||
- 2020-06-16 fix for a flaky test
|
||||
- 2020-06-16 AUTH-2815 flag check was wrong. stupid oversight
|
||||
- 2020-06-16 TUN-3101: Tunnel list command should only show non-deleted, by default
|
||||
- 2020-06-16 TUN-3066: Command line action for tunnel run
|
||||
- 2020-06-16 TUN-3100 make updater report the right text
|
||||
|
||||
2020.6.4
|
||||
- 2020-06-11 TUN-3085: Pass connection authentication information using TunnelAuth struct
|
||||
- 2020-06-15 TUN-3084: Generate and store tunnel_secret value during tunnel creation
|
||||
- 2020-06-16 AUTH-2815 add the log file to support the config.yaml file
|
||||
- 2020-06-02 TUN-3015: Add a new cap'n'proto RPC interface for connection registration as well as matching client and server implementations. The old interface extends the new one for backward compatibility.
|
||||
|
||||
2020.6.3
|
||||
- 2020-06-15 DEVTOOLS-7321: Add openssh-client pkg for missing ssh-keyscan
|
||||
- 2020-06-15 AUTH-2813 adds back a single file support a cloudflared log file
|
||||
|
||||
2020.6.2
|
||||
- 2020-06-11 AUTH-2648 updated usage text
|
||||
- 2020-06-11 AUTH-2763 don't redirect from curl command
|
||||
- 2020-06-12 TUN-3090: Upgrade crypto dep
|
||||
- 2020-06-11 TUN-3038: Add connections to tunnel list table
|
||||
- 2020-06-12 AUTH-2810 added warn for backwards compatibility sake
|
||||
|
||||
2020.6.1
|
||||
- 2020-06-09 AUTH-2796 fixed windows build
|
||||
|
||||
2020.6.0
|
||||
- 2020-06-05 AUTH-2645 protect against user mistaken flag input
|
||||
- 2020-06-05 AUTH-2687 don't copy config unnecessarily
|
||||
- 2020-06-05 AUTH-2169 make access login page more generic
|
||||
- 2020-06-05 AUTH-2729 added log file and level to cmd flags to match config file settings
|
||||
- 2020-06-08 AUTH-2785 service token flag fix and logger fix
|
||||
- 2020-05-20 AUTH-2682: Create buster build
|
||||
- 2020-05-21 TUN-2928, TUN-2929, TUN-2930: Add tunnel subcommands to interact with tunnel store service
|
||||
- 2020-05-29 Adding support for multi-architecture images and binaries (#184)
|
||||
- 2020-05-29 TUN-3019: Remove declarative tunnel entry code
|
||||
- 2020-05-29 TUN-3020: Remove declarative tunnel related RPC code
|
||||
- 2020-05-13 AUTH-2505 added aliases
|
||||
- 2020-05-14 AUTH-2529 added deprecation text to db-connect command
|
||||
- 2020-05-18 AUTH-2686: Added error handling to tunnel subcommand
|
||||
- 2020-05-04 AUTH-2369: RDP Bastion prototype
|
||||
- 2020-04-29 AUTH-2596 added new logger package and replaced logrus
|
||||
- 2020-04-25 DEVTOOLS-7321: Use SSH key from env for pushing to GitHub
|
||||
- 2020-04-25 DEVTOOLS-7321: Push to a test branch instead of to master
|
||||
- 2020-03-30 DEVTOOLS-7321: Add scripts for macOS builds and homebrew uploads
|
||||
|
||||
2020.5.1
|
||||
- 2020-05-07 TUN-2860: Enable quick reconnect feature by default
|
||||
- 2020-05-07 AUTH-2564: error handling and minor fixes
|
||||
- 2020-05-01 AUTH-2588 add DoH to service mode
|
||||
|
||||
2020.5.0
|
||||
- 2020-05-01 TUN-2943: Copy certutil from edge into cloudflared
|
||||
- 2020-05-05 TUN-2955: Fix connection and goroutine leaks when tunnel conection is terminated on error. Only unregister tunnels that had connected successfully. Close edge connection used to unregister the tunnel. Use buffered channels for error channels where receiver may quit early on context cancellation.
|
||||
- 2020-04-30 TUN-2940: Added delay parameter to stdin reconnect command.
|
||||
- 2020-04-27 TUN-2921: Rework address selection logic to avoid corner cases
|
||||
- 2020-04-28 TUN-2872: Exit with non-0 status code when the binary is updated so launchd will restart the service
|
||||
- 2020-04-13 AUTH-2587 add config watcher and reload logic for access client forwarder
|
||||
|
||||
2020.4.0
|
||||
- 2020-04-10 TUN-2881: Parameterize response meta information header name in the generating function
|
||||
- 2020-04-11 TUN-2894: ResponseMetaHeader should be public
|
||||
- 2020-04-09 TUN-2880: Return metadata about source of the response from cloudflared
|
||||
- 2020-04-04 ARES-899: Fixes DoH client as system resolver. Fixes #91
|
||||
- 2020-03-31 AUTH-2394 added socks5 proxy
|
||||
- 2020-02-24 AUTH-2235 GetTokenIfExists now parses JWT payload for json expiry field to detect if the cached access token is expired
|
||||
|
||||
2020.3.2
|
||||
- 2020-03-31 TUN-2854: Quick Reconnects should be an optional supported feature
|
||||
- 2020-03-30 TUN-2850: Tunnel stripping Cloudflare headers
|
||||
|
||||
2020.3.1
|
||||
- 2020-03-27 TUN-2846: Trigger debug reconnects from stdin commands, not SIGUSR1
|
||||
|
||||
2020.3.0
|
||||
- 2020-03-23 AUTH-2394 fixed header for websockets. Added TCP alias
|
||||
- 2020-03-10 TUN-2797: Fix panic in SetConnDigest by making mutexes values.
|
||||
- 2020-03-13 TUN-2807: cloudflared hello-world shouldn't assume it's my first tunnel
|
||||
- 2020-03-13 TUN-2756: Set connection digest after reconnect.
|
||||
- 2020-03-16 TUN-2812: Tunnel proxies and RPCs can share an edge address
|
||||
- 2020-03-18 TUN-2816: cloudflared metrics server should be more discoverable
|
||||
- 2020-03-19 TUN-2820: Serialized headers for Websockets
|
||||
- 2020-03-19 TUN-2819: cloudflared should close its connections when a signal is sent
|
||||
- 2020-03-19 TUN-2823: Bugfix. cloudflared would hang forever if error occurred.
|
||||
- 2020-03-10 TUN-2796: Implement HTTP2 CONTINUATION headers correctly
|
||||
- 2020-03-02 TUN-2779: update sample HTML pages
|
||||
- 2020-03-04 TUN-2785: Use reconnect token by default
|
||||
- 2020-03-05 TUN-2754: Add ConnDigest to cloudflared and its RPCs
|
||||
- 2020-03-06 TUN-2755: ReconnectTunnel RPC now transmits ConnectionDigest
|
||||
- 2020-03-06 TUN-2761: Use the new header management functions in cloudflared
|
||||
- 2020-03-06 TUN-2788: cloudflared should store one ConnDigest per HA connection
|
||||
- 2020-02-26 TUN-2767: Test for large headers
|
||||
- 2020-02-28 do not terminate tunnel if origin is not reachable on start-up (#177)
|
||||
- 2020-02-28 TUN-2776: Add header serialization feature in cloudflared
|
||||
- 2020-02-21 TUN-2748: Insecure randomness vulnerability in github.com/miekg/dns
|
||||
|
||||
2020.2.1
|
||||
- 2020-02-20 TUN-2745: Rename existing header management functions
|
||||
- 2020-02-21 TUN-2746: Add the new header management functions
|
||||
- 2020-02-25 perf(cloudflared): reuse memory from buffer pool to get better throughput (#161)
|
||||
- 2020-02-25 Tweak HTTP host header. Fixes #107 (#168)
|
||||
- 2020-02-25 TUN-2765: Add list of features to tunnelrpc
|
||||
- 2020-02-19 TUN-2725: Specify in code that --edge is for internal testing only
|
||||
- 2020-02-19 TUN-2703: Muxer.Serve terminates when its context is Done
|
||||
- 2020-02-09 TUN-2717: Function to serialize/deserialize HTTP headers
|
||||
- 2020-02-05 TUN-2714: New edge discovery. Connections try to reconnect to the same edge IP.
|
||||
|
||||
2020.2.0
|
||||
- 2020-01-30 TUN-2651: Fix panic in h2mux reader when a stream error is encountered
|
||||
- 2020-01-27 TUN-2645: Revert "TUN-2645: Turn on reconnect tokens"
|
||||
- 2020-01-28 TUN-2693: Metrics for ReconnectTunnel
|
||||
- 2020-01-28 TUN-2696: Add unknown registerRPCName
|
||||
- 2020-01-28 TUN-2699: Metrics for Authenticate RPCs
|
||||
- 2020-01-28 TUN-2690: cloudflared reconnect uses wrong context
|
||||
- 2020-01-29 TUN-2707: Inconsistent cardinality in tunnel error metrics
|
||||
- 2020-01-13 TUN-2645: Turn on reconnect tokens
|
||||
- 2019-12-23 TUN-2646: Make --edge flag work again for local development
|
||||
|
||||
2019.12.0
|
||||
- 2019-12-11 TUN-2631: only notify that activeStreamMap is closed if ignoreNewStreams=true
|
||||
- 2019-12-17 bug(cloudflared): Set the MaxIdleConnsPerHost of http.Transport to proxy-keepalive-connections (#155)
|
||||
- 2019-12-17 refactor(docker): optimize Dockerfile (#126)
|
||||
- 2019-12-19 Fix timer scheduling for systemd update service (#159)
|
||||
- 2019-12-13 TUN-2637: Manage edge IPs in a region-aware manner
|
||||
- 2019-12-03 bug(cloudflared): nil pointer deference on h2DictWriter Close() (#154)
|
||||
- 2019-12-03 TUN-2608: h2mux.Muxer.Shutdown always returns a non-nil channel
|
||||
- 2019-12-04 TUN-2555: origin/supervisor.go calls Authenticate
|
||||
- 2019-12-06 TUN-2554: cloudflared calls ReconnectTunnel
|
||||
- 2019-11-20 TUN-2575: Constructors + simpler conversions for AuthOutcome
|
||||
- 2019-11-22 Fix Docker build failure (#149)
|
||||
- 2019-11-21 TUN-2573: Refactor TunnelRegistration into PermanentRegistrationError, RetryableRegistrationError and SuccessfulTunnelRegistration
|
||||
- 2019-11-22 TUN-2582: EventDigest field in tunnelrpc
|
||||
- 2019-11-22 Fix "happy eyeballs" not being disabled since Golang 1.12 upgrade * The Dialer.DualStack setting is now ignored and deprecated; RFC 6555 Fast Fallback ("Happy Eyeballs") is now enabled by default. To disable, set Dialer.FallbackDelay to a negative value.
|
||||
- 2019-11-25 TUN-2591: ReconnectTunnel now sends EventDigest
|
||||
- 2019-11-21 TUN-2606: add DialEdge helpers
|
||||
- 2019-11-21 TUN-2607: add RPC stream helpers
|
||||
|
||||
2019.11.3
|
||||
- 2019-11-20 TUN-2562: Update Cloudflare Origin CA RSA root
|
||||
|
||||
2019.11.2
|
||||
- 2019-11-18 TUN-2567: AuthOutcome can be turned back into AuthResponse
|
||||
- 2019-11-18 TUN-2563: Exposes config_version metrics
|
||||
|
||||
2019.11.1
|
||||
- 2019-11-12 Add db-connect, a SQL over HTTPS server
|
||||
- 2019-11-12 TUN-2053: Add a /healthcheck endpoint to the metrics server
|
||||
- 2019-11-13 TUN-2178: public API to create new h2mux.MuxedStreamRequest
|
||||
- 2019-11-13 TUN-2490: respect original representation of HTTP request path
|
||||
- 2019-11-18 TUN-2547: TunnelRPC definitions for Authenticate flow
|
||||
- 2019-11-18 TUN-2551: TunnelRPC definitions for ReconnectTunnel flow
|
||||
- 2019-11-05 TUN-2506: Expose active streams metrics
|
||||
|
||||
2019.11.0
|
||||
- 2019-11-04 TUN-2502: Switch to go modules
|
||||
- 2019-11-04 TUN-2500: Don't send client registration errors to Sentry
|
||||
- 2019-11-04 TUN-2489: Delete stream from activestreammap when read and write are both closed
|
||||
- 2019-11-05 TUN-2505: Terminate stream on receipt of RST_STREAM; MuxedStream.CloseWrite() should terminate the MuxedStream.Write() loop
|
||||
- 2019-10-30 TUN-2451: Log inavlid path
|
||||
- 2019-10-22 TUN-2425: Enable cloudflared to serve multiple Hello World servers by having each of them create its own ServeMux
|
||||
- 2019-10-22 AUTH-2173: Prepends access login url with scheme if one doesnt exist
|
||||
- 2019-10-23 TUN-2460: Configure according to the ClientConfig recevied from a successful Connect
|
||||
- 2019-10-23 AUTH-2177: Reads and writes error streams
|
||||
|
||||
2019.10.4
|
||||
- 2019-10-21 TUN-2450: Remove Brew publishing formula
|
||||
|
||||
2019.10.3
|
||||
- 2019-10-18 Fix #129: Excessive memory usage streaming large files (#142)
|
||||
|
||||
2019.10.2
|
||||
- 2019-10-17 AUTH-2167: Adds CLI option for host key directory
|
||||
|
||||
2019.10.1
|
||||
- 2019-10-17 Adds variable to fix windows build
|
||||
|
||||
2019.10.0
|
||||
- 2019-10-11 AUTH-2105: Dont require --destination arg
|
||||
- 2019-10-14 TUN-2344: log more details: http2.Framer.ErrorDetail() if available, connectionID
|
||||
- 2019-10-16 AUTH-2159: Moves shutdownC close into error handling AUTH-2161: Lowers size of preamble length AUTH-2160: Fixes url parsing logic
|
||||
- 2019-10-16 AUTH-2135: Adds support for IPv6 and tests
|
||||
- 2019-10-02 AUTH-2105: Adds support for local forwarding. Refactor auditlogger creation. AUTH-2088: Adds dynamic destination routing
|
||||
- 2019-10-09 AUTH-2114: Uses short lived cert auth for outgoing client connection
|
||||
- 2019-09-30 AUTH-2089: Revise ssh server to function as a proxy
|
||||
|
||||
2019.9.2
|
||||
- 2019-09-26 TUN-2355: Roll back TUN-2276
|
||||
|
||||
2019.9.1
|
||||
- 2019-09-23 TUN-2334: remove tlsConfig.ServerName special case
|
||||
- 2019-09-23 AUTH-2077: Quotes open browser command in windows
|
||||
- 2019-09-11 AUTH-2050: Adds time.sleep to temporarily avoid hitting tunnel muxer dealock issue
|
||||
- 2019-09-10 AUTH-2056: Writes stderr to its own stream for non-pty connections
|
||||
- 2019-09-16 TUN-2307: Capnp is the only serialization format used in tunnelpogs
|
||||
- 2019-09-18 TUN-2315: Replace Scope with IntentLabel
|
||||
- 2019-09-17 TUN-2309: Split ConnectResult into ConnectError and ConnectSuccess, each implementing its own capnp serialization logic
|
||||
- 2019-09-18 AUTH-2052: Adds tests for SSH server
|
||||
- 2019-09-18 AUTH-2067: Log commands correctly
|
||||
- 2019-09-19 AUTH-2055: Verifies token at edge on access login
|
||||
- 2019-09-04 TUN-2201: change SRV records used by cloudflared
|
||||
- 2019-09-06 TUN-2280: Revert "TUN-2260: add name/group to CapnpConnectParameters, remove Scope"
|
||||
- 2019-09-03 AUTH-1943 hooked up uploader to logger, added timestamp to session logs, add tests
|
||||
- 2019-09-04 AUTH-2036: Refactor user retrieval, shutdown after ssh server stops, add custom version string
|
||||
- 2019-09-06 AUTH-1942 added event log to ssh server
|
||||
- 2019-09-04 AUTH-2037: Adds support for ssh port forwarding
|
||||
- 2019-09-05 TUN-2276: Path encoding broken
|
||||
|
||||
2019.9.0
|
||||
- 2019-09-05 TUN-2279: Revert path encoding fix
|
||||
- 2019-08-30 AUTH-2021 - check error for failing tests
|
||||
- 2019-08-29 AUTH-2030: Support both authorized_key and short lived cert authentication simultaniously without specifiying at start time
|
||||
- 2019-08-29 AUTH-2026: Adds support for non-pty sessions and inline command exec
|
||||
- 2019-08-26 AUTH-1943: Adds session logging
|
||||
- 2019-08-26 TUN-2162: Decomplect OpenStream to allow finer-grained timeouts
|
||||
- 2019-08-29 TUN-2260: add name/group to CapnpConnectParameters, remove Scope
|
||||
|
||||
2019.8.4
|
||||
- 2019-08-30 Fix #111: Add support for specifying a specific HTTP Host: header on the origin. (#114)
|
||||
- 2019-08-22 TUN-2165: Add ClientConfig to tunnelrpc.ConnectResult
|
||||
- 2019-08-20 AUTH-2014: Checks users login shell
|
||||
- 2019-08-26 TUN-2243: Revert "STOR-519: Add db-connect, a SQL over HTTPS server"
|
||||
- 2019-08-27 TUN-2244: Add NO_AUTOUPDATE env var
|
||||
- 2019-08-22 AUTH-2018: Adds support for authorized keys and short lived certs
|
||||
- 2019-08-28 AUTH-2022: Adds ssh timeout configuration
|
||||
- 2019-08-28 TUN-1968: Gracefully diff StreamHandler.UpdateConfig
|
||||
- 2019-08-26 AUTH-2021 - s3 bucket uploading for SSH logs
|
||||
- 2019-08-19 AUTH-2004: Adds static host key support
|
||||
- 2019-07-18 AUTH-1941: Adds initial SSH server implementation
|
||||
|
||||
2019.8.3
|
||||
- 2019-08-20 STOR-519: Add db-connect, a SQL over HTTPS server
|
||||
- 2019-08-20 Release 2019.8.2
|
||||
- 2019-08-20 Revert "AUTH-1941: Adds initial SSH server implementation"
|
||||
- 2019-08-11 TUN-2163: Add GrapQLType method to Scope interface
|
||||
- 2019-08-06 TUN-2152: Requests with a query in the URL are erroneously escaped
|
||||
- 2019-07-18 AUTH-1941: Adds initial SSH server implementation
|
||||
|
||||
2019.8.1
|
||||
- 2019-08-05 TUN-2111: Implement custom serialization logic for FallibleConfig and OriginConfig
|
||||
- 2019-08-06 Revert "TUN-1736: Missing headers when passing an invalid path"
|
||||
|
||||
2019.8.0
|
||||
- 2019-07-11 TUN-1956: Go 1.12 update
|
||||
- 2019-07-24 TUN-1736: Missing headers when passing an invalid path
|
||||
- 2019-07-30 TUN-2117: read group/system-name from CLI, send it to edge
|
||||
- 2019-08-02 TUN-2125: Add PostgresType() to Scope
|
||||
- 2019-08-05 TUN-2147: Implemented ScopeUnmarshaler
|
||||
- 2019-07-31 TUN-2110: Implement custom deserialization logic for OriginConfig
|
||||
- 2019-07-31 AUTH-1972: Deletes token lock file if backoff retry attempts exceeded and intercepts signals until lock is released
|
||||
|
||||
2019.7.0
|
||||
- 2019-05-28 TUN-1913: Define OriginService for each type of origin
|
||||
- 2019-04-29 Build a docker container
|
||||
- 2019-06-12 TUN-1952: Group ClientConfig fields by the component that uses the config, and return the part of the config that failed to be applied
|
||||
- 2019-06-05 TUN-1893: Proxy requests to the origin based on tunnel hostname
|
||||
- 2019-06-17 TUN-1961: Create EdgeConnectionManager to maintain outbound connections to the edge
|
||||
- 2019-06-18 TUN-1885: Reconfigure cloudflared on receiving new ClientConfig
|
||||
- 2019-06-19 TUN-1976: Pass tunnel hostname through header
|
||||
- 2019-06-20 TUN-1982: Load custom origin CA when OriginCAPool is specified
|
||||
- 2019-06-26 TUN-2005: Upgrade logrus
|
||||
- 2019-06-20 TUN-1981: Write response header & body on proxy error to notify eyeballs of failure category
|
||||
- 2019-06-20 TUN-1977: Validate OriginConfig has valid URL, and use scheme to determine if a HTTPOriginService is expecting HTTP or Unix
|
||||
- 2019-06-13 DoH: change the media type to application/dns-message
|
||||
- 2019-06-26 AUTH-1736: Better handling of token revocation
|
||||
|
||||
2019.6.0
|
||||
- 2019-05-17 TUN-1828: Update declarative tunnel config struct
|
||||
- 2019-05-29 Handle exit code on err
|
||||
- 2019-05-29 AUTH-1802: Fixed ssh-config templating
|
||||
- 2019-05-30 TUN-1914: Conflate HTTP and Unix OriginConfig, and add TLS config to WebSocketOriginConfig
|
||||
- 2019-06-03 AUTH-1811: ssh-gen config fixes
|
||||
|
||||
2019.5.0
|
||||
- 2019-04-25 TUN-1781: ServeStream should return early on error
|
||||
- 2019-04-30 TUN-1786: Remove low-level Windows service logging
|
||||
- 2019-05-03 TUN-1807: Send cloudflared version in Connect RPC
|
||||
- 2019-01-23 AUTH-1557: Short Lived Certs
|
||||
- 2019-05-13 TUN-1847: Log a distinct message when OpenStream fails while waiting for response headers
|
||||
- 2019-05-13 AUTH-1706: fixes and testing
|
||||
- 2019-05-22 TUN-1880: Save debug and warn level log to logfile
|
||||
- 2019-05-22 AUTH-1781: fixed race condition for short lived certs, doc required config
|
||||
|
||||
2019.4.1
|
||||
- 2019-03-18 TUN-1626: Create new supervisor to establish connection with origintunneld
|
||||
- 2019-04-04 TUN-1619: Add flag to test declarative tunnels.
|
||||
- 2019-04-05 TUN-1577: decompose carrier.StartServer to make TestStartServer less flappy
|
||||
- 2019-03-29 TUN-1606: Define CloudflaredConfig RPC structure, interface for cloudflared's RPC server
|
||||
- 2019-04-02 TUN-1682: Add context to OpenStream to prevent it from blocking indefinitely.
|
||||
- 2019-04-16 TUN-1732: cloudflared metrics should track userHostnames
|
||||
- 2019-04-17 TUN-1734: Pin packages at exact versions
|
||||
- 2019-04-18 TUN-1669: Update license message in help text. Also fix test
|
||||
|
||||
2019.4.0
|
||||
- 2019-03-28 TUN-1648: ConnectionID is now a UUID
|
||||
- 2019-04-01 TUN-1673: Conflate Hello and Connect RPCs
|
||||
|
||||
2019.3.2
|
||||
- 2019-03-22 TUN-1637: Free tunnels shouldn't require cert.pem
|
||||
- 2019-03-18 TUN-1604: Define Connect RPC call
|
||||
|
||||
2019.3.1
|
||||
- 2019-03-09 Add rdp as a supported protocol in URL validation (#76)
|
||||
- 2019-03-15 TUN-1613: improved cloudflared RegisterTunnel fail metrics
|
||||
- 2019-03-17 TUN-1615: revert miekg/dns to last known working revision
|
||||
|
||||
2019.3.0
|
||||
- 2018-12-28 make http transport aware of proxy from envvar
|
||||
- 2019-02-28 TUN-1559: fix nil dereference in TunnelConfig.CloseConnOnce
|
||||
- 2019-03-04 TUN-1451: Make non-interactive, non-service execution possible on Windows
|
||||
- 2019-03-04 TUN-1562: Refactor connectedSignal to be safe to close multiple times
|
||||
- 2019-02-27 TUN-1550: Add validation timeout for non-responsive origins
|
||||
- 2019-03-06 AUTH-1531: Named flags for ssh service tokens
|
||||
- 2019-02-14 Support unix sockets.
|
||||
- 2019-03-08 TUN-1389: Non-scalar flags in a cloudflared config.yml don't get logged
|
||||
- 2019-03-07 TUN-1522: If we can't get SRV from default resolver, get them from 1.1.1.1 DoT
|
||||
|
||||
2019.2.1
|
||||
- 2019-02-14 TUN-1381: should tell you if you're on the latest version rather than just exiting silently
|
||||
- 2019-02-15 TUN-1467: build with Go 1.11
|
||||
- 2019-02-15 AUTH-1519: Added logging
|
||||
- 2019-02-19 TUN-1525: cloudflared metrics for registration success/fail
|
||||
- 2019-02-19 TUN-1510: Wrap the close() in sync.Once.Do
|
||||
|
||||
2019.2.0
|
||||
- 2019-01-24 AUTH-1462: better curl arg parsing
|
||||
- 2019-02-01 TUN-1456: Only make one UUID
|
||||
- 2019-01-30 cloudflared/linux_service: Add missing /etc/init.d shebang
|
||||
- 2019-02-07 AUTH-1511: Add custom headers for ssh command
|
||||
- 2019-02-01 AUTH-1503: Added RDP support
|
||||
- 2019-02-01 AUTH-1403: Print the paths in the ssh-config instructions
|
||||
|
||||
2019.1.0
|
||||
- 2018-12-10 TUN-1231: Horizontal overflow wrapping on the Hello page
|
||||
- 2018-12-17 TUN-1140: Show usage if invoked with no args or config
|
||||
- 2018-11-06 TUN-632 Filter out common network exceptions from going to Sentry on StartServer
|
||||
- 2019-01-07 TUN-1138: Install cloudflared service directory with 755 permissions
|
||||
- 2019-01-07 TUN-1265: Silent exit when failing to parse config
|
||||
- 2019-01-10 TUN-1350: Enhance error messages with cloudflarestatus.com link, if relevant
|
||||
- 2019-01-16 TUN-1358: Close readyList after Muxer.Serve() has stopped running
|
||||
- 2019-01-24 AUTH-1423: move from stdout to stderr
|
||||
- 2019-01-24 AUTH-1404: reauth if the token is about to expire within 15 minutes
|
||||
- 2019-01-24 AUTH-1459: improved ssh streaming error message
|
||||
- 2019-01-24 AUTH-1211: print all the versions
|
||||
- 2019-01-24 AUTH-1337: fix url path
|
||||
- 2019-01-28 TUN-1418: Rename ProtocolLogger to TransportLogger, and use TransportLogger to log RPC events.
|
||||
- 2019-01-28 TUN-1419: Identify request/response headers/content length with ray ID
|
||||
|
||||
2018.12.1
|
||||
- 2018-12-11 TUN-1270: cloudflared panic (HA metrics missing label)
|
||||
|
||||
2018.12.0
|
||||
- 2018-11-15 TUN-1196: Allow TLS config client CA and root CA to be constructed from multiple certificates
|
||||
- 2018-11-20 TUN-1209: TLS Config Certificates and GetCertificate can both be set
|
||||
- 2018-11-26 TUN-1212: Expose tunnel_id in metrics
|
||||
- 2018-11-30 TUN-1204: remove 'cloudflared hello' command
|
||||
- 2018-12-04 Fix license URL typo
|
||||
- 2018-12-07 TUN-1250: ValidateHTTPService shouldn't follow 302s
|
||||
|
||||
2018.11.0
|
||||
- 2018-10-31 AUTH-1282: Fixed an issue where we were receiving as opposed sending on the channel.
|
||||
- 2018-11-06 TUN-1179: Fix log message in cmd/cloudflared/transfer.Run
|
||||
- 2018-11-13 AUTH-1308: get jwt even when you are already logged in
|
||||
- 2018-11-12 TUN-1190: check URL parse error when starting SSH proxy server
|
||||
- 2018-11-15 AUTH-1320: Fixed request issue and unhide the ssh command
|
||||
|
||||
2018.10.5
|
||||
- 2018-10-18 TUN-968: Flow control for large requests/responses
|
||||
- 2018-10-26 TUN-1158: Windows: use process arguments rather than trivial service arguments
|
||||
- 2018-10-20 #30: Fix the Content-Length header for HTTP2->HTTP1
|
||||
- 2018-10-29 TUN-1160: pass Host header during origin url validation
|
||||
|
||||
2018.10.4
|
||||
- 2018-09-21 AUTH-1070: added SSH/protocol forwarding
|
||||
- 2018-10-19 AUTH-1235: fixed packaging of deb dev file
|
||||
- 2018-10-19 TUN-1097: Host missing from WebSocket request
|
||||
- 2018-10-19 AUTH-1188: UX Review and Changes for CLI SSH Access
|
||||
|
||||
2018.10.3
|
||||
- 2018-10-08 TUN-1099: Bring back changes in 2018.10.1
|
||||
- 2018-10-08 TUN-1098: removed deprecation error
|
||||
- 2018-10-08 TUN-1101: False negatives in Cloudflared error reporting
|
||||
|
||||
2018.10.2
|
||||
- 2018-10-06 TUN-1093: Revert cloudflared to 2018.8.0
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package awsuploader
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
// DirectoryUploadManager is used to manage file uploads on an interval from a directory
|
||||
type DirectoryUploadManager struct {
|
||||
logger logger.Service
|
||||
uploader Uploader
|
||||
rootDirectory string
|
||||
sweepInterval time.Duration
|
||||
ticker *time.Ticker
|
||||
shutdownC chan struct{}
|
||||
workQueue chan string
|
||||
}
|
||||
|
||||
// NewDirectoryUploadManager create a new DirectoryUploadManager
|
||||
// uploader is an Uploader to use as an actual uploading engine
|
||||
// directory is the directory to sweep for files to upload
|
||||
// sweepInterval is how often to iterate the directory and upload the files within
|
||||
func NewDirectoryUploadManager(logger logger.Service, uploader Uploader, directory string, sweepInterval time.Duration, shutdownC chan struct{}) *DirectoryUploadManager {
|
||||
workerCount := 10
|
||||
manager := &DirectoryUploadManager{
|
||||
logger: logger,
|
||||
uploader: uploader,
|
||||
rootDirectory: directory,
|
||||
sweepInterval: sweepInterval,
|
||||
shutdownC: shutdownC,
|
||||
workQueue: make(chan string, workerCount),
|
||||
}
|
||||
|
||||
//start workers
|
||||
for i := 0; i < workerCount; i++ {
|
||||
go manager.worker()
|
||||
}
|
||||
|
||||
return manager
|
||||
}
|
||||
|
||||
// Upload a file using the uploader
|
||||
// This is useful for "out of band" uploads that need to be triggered immediately instead of waiting for the sweep
|
||||
func (m *DirectoryUploadManager) Upload(filepath string) error {
|
||||
return m.uploader.Upload(filepath)
|
||||
}
|
||||
|
||||
// Start the upload ticker to walk the directories
|
||||
func (m *DirectoryUploadManager) Start() {
|
||||
m.ticker = time.NewTicker(m.sweepInterval)
|
||||
go m.run()
|
||||
}
|
||||
|
||||
func (m *DirectoryUploadManager) run() {
|
||||
for {
|
||||
select {
|
||||
case <-m.shutdownC:
|
||||
m.ticker.Stop()
|
||||
return
|
||||
case <-m.ticker.C:
|
||||
m.sweep()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sweep the directory and kick off uploads
|
||||
func (m *DirectoryUploadManager) sweep() {
|
||||
filepath.Walk(m.rootDirectory, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
//30 days ago
|
||||
retentionTime := 30 * (time.Hour * 24)
|
||||
checkTime := time.Now().Add(-time.Duration(retentionTime))
|
||||
|
||||
//delete the file it is stale
|
||||
if info.ModTime().Before(checkTime) {
|
||||
os.Remove(path)
|
||||
return nil
|
||||
}
|
||||
//add the upload to the work queue
|
||||
go func() {
|
||||
m.workQueue <- path
|
||||
}()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// worker handles upload requests
|
||||
func (m *DirectoryUploadManager) worker() {
|
||||
for {
|
||||
select {
|
||||
case <-m.shutdownC:
|
||||
return
|
||||
case filepath := <-m.workQueue:
|
||||
if err := m.Upload(filepath); err != nil {
|
||||
m.logger.Errorf("Cannot upload file to s3 bucket: %s", err)
|
||||
} else {
|
||||
os.Remove(filepath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package awsuploader
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
type MockUploader struct {
|
||||
shouldFail bool
|
||||
}
|
||||
|
||||
func (m *MockUploader) Upload(filepath string) error {
|
||||
if m.shouldFail {
|
||||
return errors.New("upload set to fail")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewMockUploader(shouldFail bool) Uploader {
|
||||
return &MockUploader{shouldFail: shouldFail}
|
||||
}
|
||||
|
||||
func getDirectoryPath(t *testing.T) string {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal("couldn't create the test directory!", err)
|
||||
}
|
||||
return filepath.Join(dir, "uploads")
|
||||
}
|
||||
|
||||
func setupTestDirectory(t *testing.T) string {
|
||||
path := getDirectoryPath(t)
|
||||
os.RemoveAll(path)
|
||||
time.Sleep(100 * time.Millisecond) //short way to wait for the OS to delete the folder
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatal("couldn't create the test directory!", err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func createUploadManager(t *testing.T, shouldFailUpload bool) *DirectoryUploadManager {
|
||||
rootDirectory := setupTestDirectory(t)
|
||||
uploader := NewMockUploader(shouldFailUpload)
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
shutdownC := make(chan struct{})
|
||||
return NewDirectoryUploadManager(logger, uploader, rootDirectory, 1*time.Second, shutdownC)
|
||||
}
|
||||
|
||||
func createFile(t *testing.T, fileName string) (*os.File, string) {
|
||||
path := filepath.Join(getDirectoryPath(t), fileName)
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
t.Fatal("upload to create file for sweep test", err)
|
||||
}
|
||||
return f, path
|
||||
}
|
||||
|
||||
func TestUploadSuccess(t *testing.T) {
|
||||
manager := createUploadManager(t, false)
|
||||
path := filepath.Join(getDirectoryPath(t), "test_file")
|
||||
if err := manager.Upload(path); err != nil {
|
||||
t.Fatal("the upload request method failed", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadFailure(t *testing.T) {
|
||||
manager := createUploadManager(t, true)
|
||||
path := filepath.Join(getDirectoryPath(t), "test_file")
|
||||
if err := manager.Upload(path); err == nil {
|
||||
t.Fatal("the upload request method should have failed and didn't", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSweepSuccess(t *testing.T) {
|
||||
manager := createUploadManager(t, false)
|
||||
f, path := createFile(t, "test_file")
|
||||
defer f.Close()
|
||||
|
||||
manager.Start()
|
||||
time.Sleep(2 * time.Second)
|
||||
if _, err := os.Stat(path); os.IsExist(err) {
|
||||
//the file should have been deleted
|
||||
t.Fatal("the manager failed to delete the file", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSweepFailure(t *testing.T) {
|
||||
manager := createUploadManager(t, true)
|
||||
f, path := createFile(t, "test_file")
|
||||
defer f.Close()
|
||||
|
||||
manager.Start()
|
||||
time.Sleep(2 * time.Second)
|
||||
_, serr := f.Stat()
|
||||
if serr != nil {
|
||||
//the file should still exist
|
||||
os.Remove(path)
|
||||
t.Fatal("the manager failed to delete the file", serr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHighLoad(t *testing.T) {
|
||||
manager := createUploadManager(t, false)
|
||||
for i := 0; i < 30; i++ {
|
||||
f, _ := createFile(t, randomString(6))
|
||||
defer f.Close()
|
||||
}
|
||||
manager.Start()
|
||||
time.Sleep(4 * time.Second)
|
||||
|
||||
directory := getDirectoryPath(t)
|
||||
files, err := ioutil.ReadDir(directory)
|
||||
if err != nil || len(files) > 0 {
|
||||
t.Fatalf("the manager failed to upload all the files: %s files left: %d", err, len(files))
|
||||
}
|
||||
}
|
||||
|
||||
// LowerCase [a-z]
|
||||
const randSet = "abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
// String returns a string of length 'n' from a set of letters 'lset'
|
||||
func randomString(n int) string {
|
||||
b := make([]byte, n)
|
||||
lsetLen := len(randSet)
|
||||
for i := range b {
|
||||
b[i] = randSet[rand.Intn(lsetLen)]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package awsuploader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
)
|
||||
|
||||
//FileUploader aws compliant bucket upload
|
||||
type FileUploader struct {
|
||||
storage *s3.S3
|
||||
bucketName string
|
||||
clientID string
|
||||
secretID string
|
||||
}
|
||||
|
||||
// NewFileUploader creates a new S3 compliant bucket uploader
|
||||
func NewFileUploader(bucketName, region, accessKeyID, secretID, token, s3Host string) (*FileUploader, error) {
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String(region),
|
||||
Credentials: credentials.NewStaticCredentials(accessKeyID, secretID, token),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var storage *s3.S3
|
||||
if s3Host != "" {
|
||||
storage = s3.New(sess, &aws.Config{Endpoint: aws.String(s3Host)})
|
||||
} else {
|
||||
storage = s3.New(sess)
|
||||
}
|
||||
|
||||
return &FileUploader{
|
||||
storage: storage,
|
||||
bucketName: bucketName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Upload a file to the bucket
|
||||
func (u *FileUploader) Upload(filepath string) error {
|
||||
info, err := os.Stat(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, serr := u.storage.PutObjectWithContext(context.Background(), &s3.PutObjectInput{
|
||||
Bucket: aws.String(u.bucketName),
|
||||
Key: aws.String(info.Name()),
|
||||
Body: file,
|
||||
})
|
||||
return serr
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package awsuploader
|
||||
|
||||
// UploadManager is used to manage file uploads on an interval
|
||||
type UploadManager interface {
|
||||
Upload(string) error
|
||||
Start()
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package awsuploader
|
||||
|
||||
// Uploader the functions required to upload to a bucket
|
||||
type Uploader interface {
|
||||
//Upload a file to the bucket
|
||||
Upload(string) error
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Pool struct {
|
||||
// A Pool must not be copied after first use.
|
||||
// https://golang.org/pkg/sync/#Pool
|
||||
buffers sync.Pool
|
||||
}
|
||||
|
||||
func NewPool(bufferSize int) *Pool {
|
||||
return &Pool{
|
||||
buffers: sync.Pool{
|
||||
New: func() interface{} {
|
||||
return make([]byte, bufferSize)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pool) Get() []byte {
|
||||
return p.buffers.Get().([]byte)
|
||||
}
|
||||
|
||||
func (p *Pool) Put(buf []byte) {
|
||||
p.buffers.Put(buf)
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
//Package carrier provides a WebSocket proxy to carry or proxy a connection
|
||||
//from the local client to the edge. See it as a wrapper around any protocol
|
||||
//that it packages up in a WebSocket connection to the edge.
|
||||
package carrier
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type StartOptions struct {
|
||||
OriginURL string
|
||||
Headers http.Header
|
||||
}
|
||||
|
||||
// Connection wraps up all the needed functions to forward over the tunnel
|
||||
type Connection interface {
|
||||
// ServeStream is used to forward data from the client to the edge
|
||||
ServeStream(*StartOptions, io.ReadWriter) error
|
||||
|
||||
// StartServer is used to listen for incoming connections from the edge to the origin
|
||||
StartServer(net.Listener, string, <-chan struct{}) error
|
||||
}
|
||||
|
||||
// StdinoutStream is empty struct for wrapping stdin/stdout
|
||||
// into a single ReadWriter
|
||||
type StdinoutStream struct {
|
||||
}
|
||||
|
||||
// Read will read from Stdin
|
||||
func (c *StdinoutStream) Read(p []byte) (int, error) {
|
||||
return os.Stdin.Read(p)
|
||||
|
||||
}
|
||||
|
||||
// Write will write to Stdout
|
||||
func (c *StdinoutStream) Write(p []byte) (int, error) {
|
||||
return os.Stdout.Write(p)
|
||||
}
|
||||
|
||||
// Helper to allow defering the response close with a check that the resp is not nil
|
||||
func closeRespBody(resp *http.Response) {
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// StartForwarder will setup a listener on a specified address/port and then
|
||||
// forward connections to the origin by calling `Serve()`.
|
||||
func StartForwarder(conn Connection, address string, shutdownC <-chan struct{}, options *StartOptions) error {
|
||||
listener, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to start forwarding server")
|
||||
}
|
||||
return Serve(conn, listener, shutdownC, options)
|
||||
}
|
||||
|
||||
// StartClient will copy the data from stdin/stdout over a WebSocket connection
|
||||
// to the edge (originURL)
|
||||
func StartClient(conn Connection, stream io.ReadWriter, options *StartOptions) error {
|
||||
return conn.ServeStream(options, stream)
|
||||
}
|
||||
|
||||
// Serve accepts incoming connections on the specified net.Listener.
|
||||
// Each connection is handled in a new goroutine: its data is copied over a
|
||||
// WebSocket connection to the edge (originURL).
|
||||
// `Serve` always closes `listener`.
|
||||
func Serve(remoteConn Connection, listener net.Listener, shutdownC <-chan struct{}, options *StartOptions) error {
|
||||
defer listener.Close()
|
||||
errChan := make(chan error)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
// don't block if parent goroutine quit early
|
||||
select {
|
||||
case errChan <- err:
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
go serveConnection(remoteConn, conn, options)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-shutdownC:
|
||||
return nil
|
||||
case err := <-errChan:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// serveConnection handles connections for the Serve() call
|
||||
func serveConnection(remoteConn Connection, c net.Conn, options *StartOptions) {
|
||||
defer c.Close()
|
||||
remoteConn.ServeStream(options, c)
|
||||
}
|
||||
|
||||
// IsAccessResponse checks the http Response to see if the url location
|
||||
// contains the Access structure.
|
||||
func IsAccessResponse(resp *http.Response) bool {
|
||||
if resp == nil || resp.StatusCode != http.StatusFound {
|
||||
return false
|
||||
}
|
||||
|
||||
location, err := resp.Location()
|
||||
if err != nil || location == nil {
|
||||
return false
|
||||
}
|
||||
if strings.HasPrefix(location.Path, "/cdn-cgi/access/login") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// BuildAccessRequest builds an HTTP request with the Access token set
|
||||
func BuildAccessRequest(options *StartOptions, logger logger.Service) (*http.Request, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := token.FetchTokenWithRedirect(req.URL, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We need to create a new request as FetchToken will modify req (boo mutable)
|
||||
// as it has to follow redirect on the API and such, so here we init a new one
|
||||
originRequest, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
originRequest.Header.Set(h2mux.CFAccessTokenHeader, token)
|
||||
|
||||
for k, v := range options.Headers {
|
||||
if len(v) >= 1 {
|
||||
originRequest.Header.Set(k, v[0])
|
||||
}
|
||||
}
|
||||
|
||||
return originRequest, nil
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package carrier
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
ws "github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
// example in Sec-Websocket-Key in rfc6455
|
||||
testSecWebsocketKey = "dGhlIHNhbXBsZSBub25jZQ=="
|
||||
)
|
||||
|
||||
type testStreamer struct {
|
||||
buf *bytes.Buffer
|
||||
l sync.RWMutex
|
||||
}
|
||||
|
||||
func newTestStream() *testStreamer {
|
||||
return &testStreamer{buf: new(bytes.Buffer)}
|
||||
}
|
||||
|
||||
func (s *testStreamer) Read(p []byte) (int, error) {
|
||||
s.l.RLock()
|
||||
defer s.l.RUnlock()
|
||||
return s.buf.Read(p)
|
||||
|
||||
}
|
||||
|
||||
func (s *testStreamer) Write(p []byte) (int, error) {
|
||||
s.l.Lock()
|
||||
defer s.l.Unlock()
|
||||
return s.buf.Write(p)
|
||||
}
|
||||
|
||||
func TestStartClient(t *testing.T) {
|
||||
message := "Good morning Austin! Time for another sunny day in the great state of Texas."
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
wsConn := NewWSConnection(logger, false)
|
||||
ts := newTestWebSocketServer()
|
||||
defer ts.Close()
|
||||
|
||||
buf := newTestStream()
|
||||
options := &StartOptions{
|
||||
OriginURL: "http://" + ts.Listener.Addr().String(),
|
||||
Headers: nil,
|
||||
}
|
||||
err := StartClient(wsConn, buf, options)
|
||||
assert.NoError(t, err)
|
||||
buf.Write([]byte(message))
|
||||
|
||||
readBuffer := make([]byte, len(message))
|
||||
buf.Read(readBuffer)
|
||||
assert.Equal(t, message, string(readBuffer))
|
||||
}
|
||||
|
||||
func TestStartServer(t *testing.T) {
|
||||
listener, err := net.Listen("tcp", "localhost:")
|
||||
if err != nil {
|
||||
t.Fatalf("Error starting listener: %v", err)
|
||||
}
|
||||
message := "Good morning Austin! Time for another sunny day in the great state of Texas."
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
shutdownC := make(chan struct{})
|
||||
wsConn := NewWSConnection(logger, false)
|
||||
ts := newTestWebSocketServer()
|
||||
defer ts.Close()
|
||||
options := &StartOptions{
|
||||
OriginURL: "http://" + ts.Listener.Addr().String(),
|
||||
Headers: nil,
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := Serve(wsConn, listener, shutdownC, options)
|
||||
if err != nil {
|
||||
t.Fatalf("Error running server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
conn, err := net.Dial("tcp", listener.Addr().String())
|
||||
conn.Write([]byte(message))
|
||||
|
||||
readBuffer := make([]byte, len(message))
|
||||
conn.Read(readBuffer)
|
||||
assert.Equal(t, string(readBuffer), message)
|
||||
}
|
||||
|
||||
func TestIsAccessResponse(t *testing.T) {
|
||||
validLocationHeader := http.Header{}
|
||||
validLocationHeader.Add("location", "https://test.cloudflareaccess.com/cdn-cgi/access/login/blahblah")
|
||||
invalidLocationHeader := http.Header{}
|
||||
invalidLocationHeader.Add("location", "https://google.com")
|
||||
testCases := []struct {
|
||||
Description string
|
||||
In *http.Response
|
||||
ExpectedOut bool
|
||||
}{
|
||||
{"nil response", nil, false},
|
||||
{"redirect with no location", &http.Response{StatusCode: http.StatusFound}, false},
|
||||
{"200 ok", &http.Response{StatusCode: http.StatusOK}, false},
|
||||
{"redirect with location", &http.Response{StatusCode: http.StatusFound, Header: validLocationHeader}, true},
|
||||
{"redirect with invalid location", &http.Response{StatusCode: http.StatusFound, Header: invalidLocationHeader}, false},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
if IsAccessResponse(tc.In) != tc.ExpectedOut {
|
||||
t.Fatalf("Failed case %d -- %s", i, tc.Description)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func newTestWebSocketServer() *httptest.Server {
|
||||
upgrader := ws.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, _ := upgrader.Upgrade(w, r, nil)
|
||||
defer conn.Close()
|
||||
for {
|
||||
mt, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if err := conn.WriteMessage(mt, []byte(message)); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func testRequest(t *testing.T, url string, stream io.ReadWriter) *http.Request {
|
||||
req, err := http.NewRequest("GET", url, stream)
|
||||
if err != nil {
|
||||
t.Fatalf("testRequestHeader error")
|
||||
}
|
||||
|
||||
req.Header.Add("Connection", "Upgrade")
|
||||
req.Header.Add("Upgrade", "WebSocket")
|
||||
req.Header.Add("Sec-Websocket-Key", testSecWebsocketKey)
|
||||
req.Header.Add("Sec-Websocket-Protocol", "tunnel-protocol")
|
||||
req.Header.Add("Sec-Websocket-Version", "13")
|
||||
req.Header.Add("User-Agent", "curl/7.59.0")
|
||||
|
||||
return req
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package carrier
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/socks"
|
||||
cfwebsocket "github.com/cloudflare/cloudflared/websocket"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// Websocket is used to carry data via WS binary frames over the tunnel from client to the origin
|
||||
// This implements the functions for glider proxy (sock5) and the carrier interface
|
||||
type Websocket struct {
|
||||
logger logger.Service
|
||||
isSocks bool
|
||||
}
|
||||
|
||||
type wsdialer struct {
|
||||
conn *cfwebsocket.Conn
|
||||
}
|
||||
|
||||
func (d *wsdialer) Dial(address string) (io.ReadWriteCloser, *socks.AddrSpec, error) {
|
||||
local, ok := d.conn.LocalAddr().(*net.TCPAddr)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("not a tcp connection")
|
||||
}
|
||||
|
||||
addr := socks.AddrSpec{IP: local.IP, Port: local.Port}
|
||||
return d.conn, &addr, nil
|
||||
}
|
||||
|
||||
// NewWSConnection returns a new connection object
|
||||
func NewWSConnection(logger logger.Service, isSocks bool) Connection {
|
||||
return &Websocket{
|
||||
logger: logger,
|
||||
isSocks: isSocks,
|
||||
}
|
||||
}
|
||||
|
||||
// ServeStream will create a Websocket client stream connection to the edge
|
||||
// it blocks and writes the raw data from conn over the tunnel
|
||||
func (ws *Websocket) ServeStream(options *StartOptions, conn io.ReadWriter) error {
|
||||
wsConn, err := createWebsocketStream(options, ws.logger)
|
||||
if err != nil {
|
||||
ws.logger.Errorf("failed to connect to %s with error: %s", options.OriginURL, err)
|
||||
return err
|
||||
}
|
||||
defer wsConn.Close()
|
||||
|
||||
if ws.isSocks {
|
||||
dialer := &wsdialer{conn: wsConn}
|
||||
requestHandler := socks.NewRequestHandler(dialer)
|
||||
socksServer := socks.NewConnectionHandler(requestHandler)
|
||||
|
||||
socksServer.Serve(conn)
|
||||
} else {
|
||||
cfwebsocket.Stream(wsConn, conn)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartServer creates a Websocket server to listen for connections.
|
||||
// This is used on the origin (tunnel) side to take data from the muxer and send it to the origin
|
||||
func (ws *Websocket) StartServer(listener net.Listener, remote string, shutdownC <-chan struct{}) error {
|
||||
return cfwebsocket.StartProxyServer(ws.logger, listener, remote, shutdownC, cfwebsocket.DefaultStreamHandler)
|
||||
}
|
||||
|
||||
// createWebsocketStream will create a WebSocket connection to stream data over
|
||||
// It also handles redirects from Access and will present that flow if
|
||||
// the token is not present on the request
|
||||
func createWebsocketStream(options *StartOptions, logger logger.Service) (*cfwebsocket.Conn, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header = options.Headers
|
||||
|
||||
dump, err := httputil.DumpRequest(req, false)
|
||||
logger.Debugf("Websocket request: %s", string(dump))
|
||||
|
||||
wsConn, resp, err := cfwebsocket.ClientConnect(req, nil)
|
||||
defer closeRespBody(resp)
|
||||
|
||||
if err != nil && IsAccessResponse(resp) {
|
||||
wsConn, err = createAccessAuthenticatedStream(options, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cfwebsocket.Conn{Conn: wsConn}, nil
|
||||
}
|
||||
|
||||
// createAccessAuthenticatedStream will try load a token from storage and make
|
||||
// a connection with the token set on the request. If it still get redirect,
|
||||
// this probably means the token in storage is invalid (expired/revoked). If that
|
||||
// happens it deletes the token and runs the connection again, so the user can
|
||||
// login again and generate a new one.
|
||||
func createAccessAuthenticatedStream(options *StartOptions, logger logger.Service) (*websocket.Conn, error) {
|
||||
wsConn, resp, err := createAccessWebSocketStream(options, logger)
|
||||
defer closeRespBody(resp)
|
||||
if err == nil {
|
||||
return wsConn, nil
|
||||
}
|
||||
|
||||
if !IsAccessResponse(resp) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Access Token is invalid for some reason. Go through regen flow
|
||||
originReq, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := token.RemoveTokenIfExists(originReq.URL); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wsConn, resp, err = createAccessWebSocketStream(options, logger)
|
||||
defer closeRespBody(resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return wsConn, nil
|
||||
}
|
||||
|
||||
// createAccessWebSocketStream builds an Access request and makes a connection
|
||||
func createAccessWebSocketStream(options *StartOptions, logger logger.Service) (*websocket.Conn, *http.Response, error) {
|
||||
req, err := BuildAccessRequest(options, logger)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
dump, err := httputil.DumpRequest(req, false)
|
||||
logger.Debugf("Access Websocket request: %s", string(dump))
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package certutil
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type namedTunnelToken struct {
|
||||
ZoneID string `json:"zoneID"`
|
||||
AccountID string `json:"accountID"`
|
||||
ServiceKey string `json:"serviceKey"`
|
||||
}
|
||||
|
||||
type OriginCert struct {
|
||||
PrivateKey interface{}
|
||||
Cert *x509.Certificate
|
||||
ZoneID string
|
||||
ServiceKey string
|
||||
AccountID string
|
||||
}
|
||||
|
||||
func DecodeOriginCert(blocks []byte) (*OriginCert, error) {
|
||||
if len(blocks) == 0 {
|
||||
return nil, fmt.Errorf("Cannot decode empty certificate")
|
||||
}
|
||||
originCert := OriginCert{}
|
||||
block, rest := pem.Decode(blocks)
|
||||
for {
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
switch block.Type {
|
||||
case "PRIVATE KEY":
|
||||
if originCert.PrivateKey != nil {
|
||||
return nil, fmt.Errorf("Found multiple private key in the certificate")
|
||||
}
|
||||
// RSA private key
|
||||
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot parse private key")
|
||||
}
|
||||
originCert.PrivateKey = privateKey
|
||||
case "CERTIFICATE":
|
||||
if originCert.Cert != nil {
|
||||
return nil, fmt.Errorf("Found multiple certificates in the certificate")
|
||||
}
|
||||
cert, err := x509.ParseCertificates(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot parse certificate")
|
||||
} else if len(cert) > 1 {
|
||||
return nil, fmt.Errorf("Found multiple certificates in the certificate")
|
||||
}
|
||||
originCert.Cert = cert[0]
|
||||
case "WARP TOKEN", "ARGO TUNNEL TOKEN":
|
||||
if originCert.ZoneID != "" || originCert.ServiceKey != "" {
|
||||
return nil, fmt.Errorf("Found multiple tokens in the certificate")
|
||||
}
|
||||
// The token is a string,
|
||||
// Try the newer JSON format
|
||||
ntt := namedTunnelToken{}
|
||||
if err := json.Unmarshal(block.Bytes, &ntt); err == nil {
|
||||
originCert.ZoneID = ntt.ZoneID
|
||||
originCert.ServiceKey = ntt.ServiceKey
|
||||
originCert.AccountID = ntt.AccountID
|
||||
} else {
|
||||
// Try the older format, where the zoneID and service key are seperated by
|
||||
// a new line character
|
||||
token := string(block.Bytes)
|
||||
s := strings.Split(token, "\n")
|
||||
if len(s) != 2 {
|
||||
return nil, fmt.Errorf("Cannot parse token")
|
||||
}
|
||||
originCert.ZoneID = s[0]
|
||||
originCert.ServiceKey = s[1]
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("Unknown block %s in the certificate", block.Type)
|
||||
}
|
||||
block, rest = pem.Decode(rest)
|
||||
}
|
||||
|
||||
if originCert.PrivateKey == nil {
|
||||
return nil, fmt.Errorf("Missing private key in the certificate")
|
||||
} else if originCert.Cert == nil {
|
||||
return nil, fmt.Errorf("Missing certificate in the certificate")
|
||||
} else if originCert.ZoneID == "" || originCert.ServiceKey == "" {
|
||||
return nil, fmt.Errorf("Missing token in the certificate")
|
||||
}
|
||||
|
||||
return &originCert, nil
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package certutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLoadOriginCert(t *testing.T) {
|
||||
cert, err := DecodeOriginCert([]byte{})
|
||||
assert.Equal(t, fmt.Errorf("Cannot decode empty certificate"), err)
|
||||
assert.Nil(t, cert)
|
||||
|
||||
blocks, err := ioutil.ReadFile("test-cert-no-key.pem")
|
||||
assert.Nil(t, err)
|
||||
cert, err = DecodeOriginCert(blocks)
|
||||
assert.Equal(t, fmt.Errorf("Missing private key in the certificate"), err)
|
||||
assert.Nil(t, cert)
|
||||
|
||||
blocks, err = ioutil.ReadFile("test-cert-two-certificates.pem")
|
||||
assert.Nil(t, err)
|
||||
cert, err = DecodeOriginCert(blocks)
|
||||
assert.Equal(t, fmt.Errorf("Found multiple certificates in the certificate"), err)
|
||||
assert.Nil(t, cert)
|
||||
|
||||
blocks, err = ioutil.ReadFile("test-cert-unknown-block.pem")
|
||||
assert.Nil(t, err)
|
||||
cert, err = DecodeOriginCert(blocks)
|
||||
assert.Equal(t, fmt.Errorf("Unknown block RSA PRIVATE KEY in the certificate"), err)
|
||||
assert.Nil(t, cert)
|
||||
|
||||
blocks, err = ioutil.ReadFile("test-cert.pem")
|
||||
assert.Nil(t, err)
|
||||
cert, err = DecodeOriginCert(blocks)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, cert)
|
||||
assert.Equal(t, "7b0a4d77dfb881c1a3b7d61ea9443e19", cert.ZoneID)
|
||||
key := "v1.0-58bd4f9e28f7b3c28e05a35ff3e80ab4fd9644ef3fece537eb0d12e2e9258217-183442fbb0bbdb3e571558fec9b5589ebd77aafc87498ee3f09f64a4ad79ffe8791edbae08b36c1d8f1d70a8670de56922dff92b15d214a524f4ebfa1958859e-7ce80f79921312a6022c5d25e2d380f82ceaefe3fbdc43dd13b080e3ef1e26f7"
|
||||
assert.Equal(t, key, cert.ServiceKey)
|
||||
}
|
||||
|
||||
func TestNewlineArgoTunnelToken(t *testing.T) {
|
||||
ArgoTunnelTokenTest(t, "test-argo-tunnel-cert.pem")
|
||||
}
|
||||
|
||||
func TestJSONArgoTunnelToken(t *testing.T) {
|
||||
// The given cert's Argo Tunnel Token was generated by base64 encoding this JSON:
|
||||
// {
|
||||
// "zoneID": "7b0a4d77dfb881c1a3b7d61ea9443e19",
|
||||
// "serviceKey": "test-service-key",
|
||||
// "accountID": "abcdabcdabcdabcd1234567890abcdef"
|
||||
// }
|
||||
ArgoTunnelTokenTest(t, "test-argo-tunnel-cert-json.pem")
|
||||
}
|
||||
|
||||
func ArgoTunnelTokenTest(t *testing.T, path string) {
|
||||
blocks, err := ioutil.ReadFile(path)
|
||||
assert.Nil(t, err)
|
||||
cert, err := DecodeOriginCert(blocks)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, cert)
|
||||
assert.Equal(t, "7b0a4d77dfb881c1a3b7d61ea9443e19", cert.ZoneID)
|
||||
key := "test-service-key"
|
||||
assert.Equal(t, key, cert.ServiceKey)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
|
||||
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
|
||||
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
|
||||
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
|
||||
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
|
||||
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
|
||||
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
|
||||
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
|
||||
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
|
||||
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
|
||||
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
|
||||
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
|
||||
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
|
||||
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
|
||||
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
|
||||
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
|
||||
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
|
||||
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
|
||||
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
|
||||
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
|
||||
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
|
||||
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
|
||||
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
|
||||
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
|
||||
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
|
||||
uQicoQq3yzeQh20wtrtaXzTNmA==
|
||||
-----END PRIVATE KEY-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
|
||||
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
|
||||
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
|
||||
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
|
||||
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
|
||||
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
|
||||
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
|
||||
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
|
||||
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
|
||||
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
|
||||
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
|
||||
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
|
||||
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
|
||||
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
|
||||
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
|
||||
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
|
||||
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
|
||||
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
|
||||
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
|
||||
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
|
||||
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
|
||||
x+Yo/cL8fGfVpPt4UM8=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN ARGO TUNNEL TOKEN-----
|
||||
eyJ6b25lSUQiOiAiN2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkiLCAi
|
||||
c2VydmljZUtleSI6ICJ0ZXN0LXNlcnZpY2Uta2V5IiwgImFjY291bnRJRCI6ICJh
|
||||
YmNkYWJjZGFiY2RhYmNkMTIzNDU2Nzg5MGFiY2RlZiJ9
|
||||
-----END ARGO TUNNEL TOKEN-----
|
||||
@@ -0,0 +1,56 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
|
||||
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
|
||||
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
|
||||
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
|
||||
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
|
||||
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
|
||||
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
|
||||
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
|
||||
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
|
||||
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
|
||||
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
|
||||
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
|
||||
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
|
||||
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
|
||||
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
|
||||
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
|
||||
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
|
||||
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
|
||||
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
|
||||
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
|
||||
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
|
||||
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
|
||||
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
|
||||
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
|
||||
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
|
||||
uQicoQq3yzeQh20wtrtaXzTNmA==
|
||||
-----END PRIVATE KEY-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
|
||||
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
|
||||
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
|
||||
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
|
||||
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
|
||||
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
|
||||
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
|
||||
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
|
||||
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
|
||||
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
|
||||
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
|
||||
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
|
||||
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
|
||||
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
|
||||
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
|
||||
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
|
||||
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
|
||||
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
|
||||
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
|
||||
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
|
||||
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
|
||||
x+Yo/cL8fGfVpPt4UM8=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN ARGO TUNNEL TOKEN-----
|
||||
N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdGVzdC1zZXJ2aWNlLWtl
|
||||
eQ==
|
||||
-----END ARGO TUNNEL TOKEN-----
|
||||
@@ -0,0 +1,33 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
|
||||
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
|
||||
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
|
||||
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
|
||||
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
|
||||
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
|
||||
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
|
||||
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
|
||||
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
|
||||
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
|
||||
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
|
||||
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
|
||||
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
|
||||
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
|
||||
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
|
||||
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
|
||||
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
|
||||
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
|
||||
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
|
||||
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
|
||||
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
|
||||
x+Yo/cL8fGfVpPt4UM8=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN WARP TOKEN-----
|
||||
N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdjEuMC01OGJkNGY5ZTI4
|
||||
ZjdiM2MyOGUwNWEzNWZmM2U4MGFiNGZkOTY0NGVmM2ZlY2U1MzdlYjBkMTJlMmU5
|
||||
MjU4MjE3LTE4MzQ0MmZiYjBiYmRiM2U1NzE1NThmZWM5YjU1ODllYmQ3N2FhZmM4
|
||||
NzQ5OGVlM2YwOWY2NGE0YWQ3OWZmZTg3OTFlZGJhZTA4YjM2YzFkOGYxZDcwYTg2
|
||||
NzBkZTU2OTIyZGZmOTJiMTVkMjE0YTUyNGY0ZWJmYTE5NTg4NTllLTdjZTgwZjc5
|
||||
OTIxMzEyYTYwMjJjNWQyNWUyZDM4MGY4MmNlYWVmZTNmYmRjNDNkZDEzYjA4MGUz
|
||||
ZWYxZTI2Zjc=
|
||||
-----END WARP TOKEN-----
|
||||
@@ -0,0 +1,85 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
|
||||
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
|
||||
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
|
||||
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
|
||||
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
|
||||
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
|
||||
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
|
||||
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
|
||||
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
|
||||
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
|
||||
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
|
||||
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
|
||||
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
|
||||
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
|
||||
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
|
||||
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
|
||||
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
|
||||
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
|
||||
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
|
||||
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
|
||||
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
|
||||
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
|
||||
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
|
||||
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
|
||||
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
|
||||
uQicoQq3yzeQh20wtrtaXzTNmA==
|
||||
-----END PRIVATE KEY-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
|
||||
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
|
||||
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
|
||||
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
|
||||
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
|
||||
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
|
||||
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
|
||||
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
|
||||
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
|
||||
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
|
||||
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
|
||||
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
|
||||
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
|
||||
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
|
||||
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
|
||||
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
|
||||
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
|
||||
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
|
||||
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
|
||||
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
|
||||
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
|
||||
x+Yo/cL8fGfVpPt4UM8=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
|
||||
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
|
||||
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
|
||||
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
|
||||
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
|
||||
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
|
||||
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
|
||||
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
|
||||
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
|
||||
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
|
||||
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
|
||||
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
|
||||
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
|
||||
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
|
||||
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
|
||||
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
|
||||
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
|
||||
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
|
||||
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
|
||||
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
|
||||
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
|
||||
x+Yo/cL8fGfVpPt4UM8=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN WARP TOKEN-----
|
||||
N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdjEuMC01OGJkNGY5ZTI4
|
||||
ZjdiM2MyOGUwNWEzNWZmM2U4MGFiNGZkOTY0NGVmM2ZlY2U1MzdlYjBkMTJlMmU5
|
||||
MjU4MjE3LTE4MzQ0MmZiYjBiYmRiM2U1NzE1NThmZWM5YjU1ODllYmQ3N2FhZmM4
|
||||
NzQ5OGVlM2YwOWY2NGE0YWQ3OWZmZTg3OTFlZGJhZTA4YjM2YzFkOGYxZDcwYTg2
|
||||
NzBkZTU2OTIyZGZmOTJiMTVkMjE0YTUyNGY0ZWJmYTE5NTg4NTllLTdjZTgwZjc5
|
||||
OTIxMzEyYTYwMjJjNWQyNWUyZDM4MGY4MmNlYWVmZTNmYmRjNDNkZDEzYjA4MGUz
|
||||
ZWYxZTI2Zjc=
|
||||
-----END WARP TOKEN-----
|
||||
@@ -0,0 +1,89 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
|
||||
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
|
||||
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
|
||||
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
|
||||
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
|
||||
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
|
||||
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
|
||||
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
|
||||
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
|
||||
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
|
||||
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
|
||||
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
|
||||
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
|
||||
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
|
||||
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
|
||||
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
|
||||
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
|
||||
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
|
||||
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
|
||||
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
|
||||
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
|
||||
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
|
||||
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
|
||||
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
|
||||
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
|
||||
uQicoQq3yzeQh20wtrtaXzTNmA==
|
||||
-----END PRIVATE KEY-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
|
||||
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
|
||||
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
|
||||
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
|
||||
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
|
||||
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
|
||||
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
|
||||
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
|
||||
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
|
||||
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
|
||||
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
|
||||
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
|
||||
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
|
||||
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
|
||||
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
|
||||
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
|
||||
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
|
||||
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
|
||||
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
|
||||
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
|
||||
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
|
||||
x+Yo/cL8fGfVpPt4UM8=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN WARP TOKEN-----
|
||||
N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdjEuMC01OGJkNGY5ZTI4
|
||||
ZjdiM2MyOGUwNWEzNWZmM2U4MGFiNGZkOTY0NGVmM2ZlY2U1MzdlYjBkMTJlMmU5
|
||||
MjU4MjE3LTE4MzQ0MmZiYjBiYmRiM2U1NzE1NThmZWM5YjU1ODllYmQ3N2FhZmM4
|
||||
NzQ5OGVlM2YwOWY2NGE0YWQ3OWZmZTg3OTFlZGJhZTA4YjM2YzFkOGYxZDcwYTg2
|
||||
NzBkZTU2OTIyZGZmOTJiMTVkMjE0YTUyNGY0ZWJmYTE5NTg4NTllLTdjZTgwZjc5
|
||||
OTIxMzEyYTYwMjJjNWQyNWUyZDM4MGY4MmNlYWVmZTNmYmRjNDNkZDEzYjA4MGUz
|
||||
ZWYxZTI2Zjc=
|
||||
-----END WARP TOKEN-----
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
|
||||
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
|
||||
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
|
||||
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
|
||||
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
|
||||
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
|
||||
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
|
||||
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
|
||||
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
|
||||
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
|
||||
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
|
||||
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
|
||||
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
|
||||
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
|
||||
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
|
||||
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
|
||||
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
|
||||
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
|
||||
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
|
||||
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
|
||||
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
|
||||
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
|
||||
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
|
||||
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
|
||||
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
|
||||
uQicoQq3yzeQh20wtrtaXzTNmA==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -0,0 +1,61 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
|
||||
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
|
||||
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
|
||||
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
|
||||
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
|
||||
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
|
||||
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
|
||||
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
|
||||
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
|
||||
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
|
||||
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
|
||||
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
|
||||
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
|
||||
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
|
||||
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
|
||||
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
|
||||
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
|
||||
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
|
||||
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
|
||||
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
|
||||
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
|
||||
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
|
||||
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
|
||||
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
|
||||
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
|
||||
uQicoQq3yzeQh20wtrtaXzTNmA==
|
||||
-----END PRIVATE KEY-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
|
||||
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
|
||||
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
|
||||
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
|
||||
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
|
||||
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
|
||||
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
|
||||
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
|
||||
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
|
||||
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
|
||||
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
|
||||
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
|
||||
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
|
||||
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
|
||||
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
|
||||
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
|
||||
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
|
||||
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
|
||||
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
|
||||
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
|
||||
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
|
||||
x+Yo/cL8fGfVpPt4UM8=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN WARP TOKEN-----
|
||||
N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdjEuMC01OGJkNGY5ZTI4
|
||||
ZjdiM2MyOGUwNWEzNWZmM2U4MGFiNGZkOTY0NGVmM2ZlY2U1MzdlYjBkMTJlMmU5
|
||||
MjU4MjE3LTE4MzQ0MmZiYjBiYmRiM2U1NzE1NThmZWM5YjU1ODllYmQ3N2FhZmM4
|
||||
NzQ5OGVlM2YwOWY2NGE0YWQ3OWZmZTg3OTFlZGJhZTA4YjM2YzFkOGYxZDcwYTg2
|
||||
NzBkZTU2OTIyZGZmOTJiMTVkMjE0YTUyNGY0ZWJmYTE5NTg4NTllLTdjZTgwZjc5
|
||||
OTIxMzEyYTYwMjJjNWQyNWUyZDM4MGY4MmNlYWVmZTNmYmRjNDNkZDEzYjA4MGUz
|
||||
ZWYxZTI2Zjc=
|
||||
-----END WARP TOKEN-----
|
||||
+212
-19
@@ -1,13 +1,13 @@
|
||||
pinned_go: &pinned_go go=1.9.3-1
|
||||
build_dir: &build_dir /cfsetup_build/src/github.com/cloudflare/cloudflared/
|
||||
stretch:
|
||||
pinned_go: &pinned_go go=1.15.2-1
|
||||
build_dir: &build_dir /cfsetup_build
|
||||
default-flavor: stretch
|
||||
stretch: &stretch
|
||||
build:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
post-cache:
|
||||
- export GOPATH=/cfsetup_build/
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared
|
||||
@@ -19,20 +19,54 @@ stretch:
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
post-cache:
|
||||
- export GOPATH=/cfsetup_build/
|
||||
- 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:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
- openssh-client
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make publish-deb
|
||||
release-linux-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
post-cache:
|
||||
- export GOPATH=/cfsetup_build/
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make release
|
||||
github-release-linux-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: &install_pygithub
|
||||
- pip3 install pygithub
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make github-release
|
||||
release-linux-armv6:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
@@ -40,50 +74,209 @@ stretch:
|
||||
- crossbuild-essential-armhf
|
||||
- gcc-arm-linux-gnueabihf
|
||||
post-cache:
|
||||
- export GOPATH=/cfsetup_build/
|
||||
- export GOOS=linux
|
||||
- export GOARCH=arm
|
||||
- export CC=arm-linux-gnueabihf-gcc
|
||||
- make release
|
||||
github-release-linux-armv6:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- crossbuild-essential-armhf
|
||||
- gcc-arm-linux-gnueabihf
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *install_pygithub
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=arm
|
||||
- export CC=arm-linux-gnueabihf-gcc
|
||||
- make github-release
|
||||
release-linux-386:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-multilib
|
||||
post-cache:
|
||||
- export GOPATH=/cfsetup_build/
|
||||
- export GOOS=linux
|
||||
- export GOARCH=386
|
||||
- make release
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-multilib
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=386
|
||||
- make release
|
||||
github-release-linux-386:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-multilib
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *install_pygithub
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=386
|
||||
- make github-release
|
||||
release-windows-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-mingw-w64
|
||||
post-cache:
|
||||
- export GOPATH=/cfsetup_build/
|
||||
- export GOOS=windows
|
||||
- export GOARCH=amd64
|
||||
- export CC=x86_64-w64-mingw32-gcc
|
||||
- make release
|
||||
github-release-windows-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-mingw-w64
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *install_pygithub
|
||||
post-cache:
|
||||
- export GOOS=windows
|
||||
- export GOARCH=amd64
|
||||
- export CC=x86_64-w64-mingw32-gcc
|
||||
- make github-release
|
||||
release-windows-386:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-mingw-w64
|
||||
post-cache:
|
||||
- export GOPATH=/cfsetup_build/
|
||||
- export GOOS=windows
|
||||
- export GOARCH=386
|
||||
- export CC=i686-w64-mingw32-gcc-win32
|
||||
- make release
|
||||
github-release-windows-386:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-mingw-w64
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *install_pygithub
|
||||
post-cache:
|
||||
- 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:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
post-cache:
|
||||
- export GOPATH=/cfsetup_build/
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
# cd to a non-module directory: https://github.com/golang/go/issues/24250
|
||||
- (cd / && go get github.com/BurntSushi/go-sumtype)
|
||||
- export PATH="$HOME/go/bin:$PATH"
|
||||
- make test
|
||||
update-homebrew:
|
||||
builddeps:
|
||||
- openssh-client
|
||||
- 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-7:
|
||||
publish-rpm:
|
||||
build_dir: *build_dir
|
||||
builddeps: &el7_builddeps
|
||||
- https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
|
||||
pre-cache:
|
||||
- yum install -y fakeroot
|
||||
- wget https://golang.org/dl/go1.15.2.linux-amd64.tar.gz -P /tmp/
|
||||
- tar -C /usr/local -xzf /tmp/go1.15.2.linux-amd64.tar.gz
|
||||
post-cache:
|
||||
- export PATH=$PATH:/usr/local/go/bin
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make publish-rpm
|
||||
build-rpm:
|
||||
build_dir: *build_dir
|
||||
builddeps: *el7_builddeps
|
||||
pre-cache:
|
||||
- yum install -y fakeroot
|
||||
- wget https://golang.org/dl/go1.15.2.linux-amd64.tar.gz -P /tmp/
|
||||
- tar -C /usr/local -xzf /tmp/go1.15.2.linux-amd64.tar.gz
|
||||
post-cache:
|
||||
- export PATH=$PATH:/usr/local/go/bin
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared-rpm
|
||||
|
||||
# cfsetup compose
|
||||
default-stack: test_dbconnect
|
||||
test_dbconnect:
|
||||
compose:
|
||||
up-args:
|
||||
- --renew-anon-volumes
|
||||
- --abort-on-container-exit
|
||||
- --exit-code-from=cloudflared
|
||||
files:
|
||||
- dbconnect_tests/dbconnect.yaml
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
.\" Manpage for cloudflared.
|
||||
.TH man 1 ${DATE} "${VERSION}" "cloudflared man page"
|
||||
.SH NAME
|
||||
cloudflared \- creates a connection to the cloudflare edge network
|
||||
.SH DESCRIPTION
|
||||
cloudflared creates a persistent connection between a local service and the Cloudflare network. Once the daemon is running and the Tunnel has been configured, the local service can be locked down to only allow connections from Cloudflare.
|
||||
@@ -0,0 +1,140 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/carrier"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
"github.com/pkg/errors"
|
||||
cli "github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// StartForwarder starts a client side websocket forward
|
||||
func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, logger logger.Service) error {
|
||||
validURLString, err := validation.ValidateUrl(forwarder.Listener)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error validating origin URL")
|
||||
}
|
||||
|
||||
validURL, err := url.Parse(validURLString)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error parsing origin URL")
|
||||
}
|
||||
|
||||
// get the headers from the config file and add to the request
|
||||
headers := make(http.Header)
|
||||
if forwarder.TokenClientID != "" {
|
||||
headers.Set(h2mux.CFAccessClientIDHeader, forwarder.TokenClientID)
|
||||
}
|
||||
|
||||
if forwarder.TokenSecret != "" {
|
||||
headers.Set(h2mux.CFAccessClientSecretHeader, forwarder.TokenSecret)
|
||||
}
|
||||
|
||||
if forwarder.Destination != "" {
|
||||
headers.Add(h2mux.CFJumpDestinationHeader, forwarder.Destination)
|
||||
}
|
||||
|
||||
options := &carrier.StartOptions{
|
||||
OriginURL: forwarder.URL,
|
||||
Headers: headers, //TODO: TUN-2688 support custom headers from config file
|
||||
}
|
||||
|
||||
// we could add a cmd line variable for this bool if we want the SOCK5 server to be on the client side
|
||||
wsConn := carrier.NewWSConnection(logger, false)
|
||||
|
||||
logger.Infof("Start Websocket listener on: %s", validURL.Host)
|
||||
return carrier.StartForwarder(wsConn, validURL.Host, shutdown, options)
|
||||
}
|
||||
|
||||
// ssh will start a WS proxy server for server mode
|
||||
// or copy from stdin/stdout for client mode
|
||||
// useful for proxying other protocols (like ssh) over websockets
|
||||
// (which you can put Access in front of)
|
||||
func ssh(c *cli.Context) error {
|
||||
logDirectory, logLevel := config.FindLogSettings()
|
||||
|
||||
flagLogDirectory := c.String(sshLogDirectoryFlag)
|
||||
if flagLogDirectory != "" {
|
||||
logDirectory = flagLogDirectory
|
||||
}
|
||||
|
||||
flagLogLevel := c.String(sshLogLevelFlag)
|
||||
if flagLogLevel != "" {
|
||||
logLevel = flagLogLevel
|
||||
}
|
||||
|
||||
logger, err := logger.New(logger.DefaultFile(logDirectory), logger.LogLevelString(logLevel))
|
||||
if err != nil {
|
||||
return cliutil.PrintLoggerSetupError("error setting up logger", err)
|
||||
}
|
||||
|
||||
// get the hostname from the cmdline and error out if its not provided
|
||||
rawHostName := c.String(sshHostnameFlag)
|
||||
hostname, err := validation.ValidateHostname(rawHostName)
|
||||
if err != nil || rawHostName == "" {
|
||||
return cli.ShowCommandHelp(c, "ssh")
|
||||
}
|
||||
originURL := ensureURLScheme(hostname)
|
||||
|
||||
// get the headers from the cmdline and add them
|
||||
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
|
||||
if c.IsSet(sshTokenIDFlag) {
|
||||
headers.Set(h2mux.CFAccessClientIDHeader, c.String(sshTokenIDFlag))
|
||||
}
|
||||
if c.IsSet(sshTokenSecretFlag) {
|
||||
headers.Set(h2mux.CFAccessClientSecretHeader, c.String(sshTokenSecretFlag))
|
||||
}
|
||||
|
||||
destination := c.String(sshDestinationFlag)
|
||||
if destination != "" {
|
||||
headers.Add(h2mux.CFJumpDestinationHeader, destination)
|
||||
}
|
||||
|
||||
options := &carrier.StartOptions{
|
||||
OriginURL: originURL,
|
||||
Headers: headers,
|
||||
}
|
||||
|
||||
// we could add a cmd line variable for this bool if we want the SOCK5 server to be on the client side
|
||||
wsConn := carrier.NewWSConnection(logger, false)
|
||||
|
||||
if c.NArg() > 0 || c.IsSet(sshURLFlag) {
|
||||
localForwarder, err := config.ValidateUrl(c, true)
|
||||
if err != nil {
|
||||
logger.Errorf("Error validating origin URL: %s", err)
|
||||
return errors.Wrap(err, "error validating origin URL")
|
||||
}
|
||||
forwarder, err := url.Parse(localForwarder)
|
||||
if err != nil {
|
||||
logger.Errorf("Error validating origin URL: %s", err)
|
||||
return errors.Wrap(err, "error validating origin URL")
|
||||
}
|
||||
|
||||
logger.Infof("Start Websocket listener on: %s", forwarder.Host)
|
||||
err = carrier.StartForwarder(wsConn, forwarder.Host, shutdownC, options)
|
||||
if err != nil {
|
||||
logger.Errorf("Error on Websocket listener: %s", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return carrier.StartClient(wsConn, &carrier.StdinoutStream{}, options)
|
||||
}
|
||||
|
||||
func buildRequestHeaders(values []string) http.Header {
|
||||
headers := make(http.Header)
|
||||
for _, valuePair := range values {
|
||||
split := strings.Split(valuePair, ":")
|
||||
if len(split) > 1 {
|
||||
headers.Add(strings.TrimSpace(split[0]), strings.TrimSpace(split[1]))
|
||||
}
|
||||
}
|
||||
return headers
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBuildRequestHeaders(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
headers.Add("client", "value")
|
||||
headers.Add("secret", "safe-value")
|
||||
|
||||
values := buildRequestHeaders([]string{"client: value", "secret: safe-value", "trash"})
|
||||
assert.Equal(t, headers.Get("client"), values.Get("client"))
|
||||
assert.Equal(t, headers.Get("secret"), values.Get("secret"))
|
||||
}
|
||||
@@ -0,0 +1,492 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/carrier"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/sshgen"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/idna"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
sshHostnameFlag = "hostname"
|
||||
sshDestinationFlag = "destination"
|
||||
sshURLFlag = "url"
|
||||
sshHeaderFlag = "header"
|
||||
sshTokenIDFlag = "service-token-id"
|
||||
sshTokenSecretFlag = "service-token-secret"
|
||||
sshGenCertFlag = "short-lived-cert"
|
||||
sshLogDirectoryFlag = "log-directory"
|
||||
sshLogLevelFlag = "log-level"
|
||||
sshConfigTemplate = `
|
||||
Add to your {{.Home}}/.ssh/config:
|
||||
|
||||
Host {{.Hostname}}
|
||||
{{- if .ShortLivedCerts}}
|
||||
ProxyCommand bash -c '{{.Cloudflared}} access ssh-gen --hostname %h; ssh -tt %r@cfpipe-{{.Hostname}} >&2 <&1'
|
||||
|
||||
Host cfpipe-{{.Hostname}}
|
||||
HostName {{.Hostname}}
|
||||
ProxyCommand {{.Cloudflared}} access ssh --hostname %h
|
||||
IdentityFile ~/.cloudflared/{{.Hostname}}-cf_key
|
||||
CertificateFile ~/.cloudflared/{{.Hostname}}-cf_key-cert.pub
|
||||
{{- else}}
|
||||
ProxyCommand {{.Cloudflared}} access ssh --hostname %h
|
||||
{{end}}
|
||||
`
|
||||
)
|
||||
|
||||
const sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b@sentry.io/189878"
|
||||
|
||||
var (
|
||||
shutdownC chan struct{}
|
||||
graceShutdownC chan struct{}
|
||||
)
|
||||
|
||||
// Init will initialize and store vars from the main program
|
||||
func Init(s, g chan struct{}) {
|
||||
shutdownC, graceShutdownC = s, g
|
||||
}
|
||||
|
||||
// Flags return the global flags for Access related commands (hopefully none)
|
||||
func Flags() []cli.Flag {
|
||||
return []cli.Flag{} // no flags yet.
|
||||
}
|
||||
|
||||
// Commands returns all the Access related subcommands
|
||||
func Commands() []*cli.Command {
|
||||
return []*cli.Command{
|
||||
{
|
||||
Name: "access",
|
||||
Aliases: []string{"forward"},
|
||||
Category: "Access",
|
||||
Usage: "access <subcommand>",
|
||||
Description: `Cloudflare Access protects internal resources by securing, authenticating and monitoring access
|
||||
per-user and by application. With Cloudflare Access, only authenticated users with the required permissions are
|
||||
able to reach sensitive resources. The commands provided here allow you to interact with Access protected
|
||||
applications from the command line.`,
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "login",
|
||||
Action: cliutil.ErrorHandler(login),
|
||||
Usage: "login <url of access application>",
|
||||
Description: `The login subcommand initiates an authentication flow with your identity provider.
|
||||
The subcommand will launch a browser. For headless systems, a url is provided.
|
||||
Once authenticated with your identity provider, the login command will generate a JSON Web Token (JWT)
|
||||
scoped to your identity, the application you intend to reach, and valid for a session duration set by your
|
||||
administrator. cloudflared stores the token in local storage.`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "url",
|
||||
Hidden: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "curl",
|
||||
Action: cliutil.ErrorHandler(curl),
|
||||
Usage: "curl [--allow-request, -ar] <url> [<curl args>...]",
|
||||
Description: `The curl subcommand wraps curl and automatically injects the JWT into a cf-access-token
|
||||
header when using curl to reach an application behind Access.`,
|
||||
ArgsUsage: "allow-request will allow the curl request to continue even if the jwt is not present.",
|
||||
SkipFlagParsing: true,
|
||||
},
|
||||
{
|
||||
Name: "token",
|
||||
Action: cliutil.ErrorHandler(generateToken),
|
||||
Usage: "token -app=<url of access application>",
|
||||
ArgsUsage: "url of Access application",
|
||||
Description: `The token subcommand produces a JWT which can be used to authenticate requests.`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "app",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "tcp",
|
||||
Action: cliutil.ErrorHandler(ssh),
|
||||
Aliases: []string{"rdp", "ssh", "smb"},
|
||||
Usage: "",
|
||||
ArgsUsage: "",
|
||||
Description: `The tcp subcommand sends data over a proxy to the Cloudflare edge.`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: sshHostnameFlag,
|
||||
Aliases: []string{"tunnel-host", "T"},
|
||||
Usage: "specify the hostname of your application.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshDestinationFlag,
|
||||
Usage: "specify the destination address of your SSH server.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshURLFlag,
|
||||
Aliases: []string{"listener", "L"},
|
||||
Usage: "specify the host:port to forward data to Cloudflare edge.",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: sshHeaderFlag,
|
||||
Aliases: []string{"H"},
|
||||
Usage: "specify additional headers you wish to send.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshTokenIDFlag,
|
||||
Aliases: []string{"id"},
|
||||
Usage: "specify an Access service token ID you wish to use.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshTokenSecretFlag,
|
||||
Aliases: []string{"secret"},
|
||||
Usage: "specify an Access service token secret you wish to use.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshLogDirectoryFlag,
|
||||
Aliases: []string{"logfile"}, //added to match the tunnel side
|
||||
Usage: "Save application log to this directory for reporting issues.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshLogLevelFlag,
|
||||
Aliases: []string{"loglevel"}, //added to match the tunnel side
|
||||
Usage: "Application logging level {fatal, error, info, debug}. ",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ssh-config",
|
||||
Action: cliutil.ErrorHandler(sshConfig),
|
||||
Usage: "",
|
||||
Description: `Prints an example configuration ~/.ssh/config`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: sshHostnameFlag,
|
||||
Usage: "specify the hostname of your application.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: sshGenCertFlag,
|
||||
Usage: "specify if you wish to generate short lived certs.",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ssh-gen",
|
||||
Action: cliutil.ErrorHandler(sshGen),
|
||||
Usage: "",
|
||||
Description: `Generates a short lived certificate for given hostname`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: sshHostnameFlag,
|
||||
Usage: "specify the hostname of your application.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// login pops up the browser window to do the actual login and JWT generation
|
||||
func login(c *cli.Context) error {
|
||||
if err := raven.SetDSN(sentryDSN); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
args := c.Args()
|
||||
rawURL := ensureURLScheme(args.First())
|
||||
appURL, err := url.Parse(rawURL)
|
||||
if args.Len() < 1 || err != nil {
|
||||
logger.Errorf("Please provide the url of the Access application\n")
|
||||
return err
|
||||
}
|
||||
if err := verifyTokenAtEdge(appURL, c, logger); err != nil {
|
||||
logger.Errorf("Could not verify token: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
cfdToken, err := token.GetTokenIfExists(appURL)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Unable to find token for provided application.")
|
||||
return err
|
||||
} else if cfdToken == "" {
|
||||
fmt.Fprintln(os.Stderr, "token for provided application was empty.")
|
||||
return errors.New("empty application token")
|
||||
}
|
||||
fmt.Fprintf(os.Stdout, "Successfully fetched your token:\n\n%s\n\n", cfdToken)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureURLScheme prepends a URL with https:// if it doesnt have a scheme. http:// URLs will not be converted.
|
||||
func ensureURLScheme(url string) string {
|
||||
url = strings.Replace(strings.ToLower(url), "http://", "https://", 1)
|
||||
if !strings.HasPrefix(url, "https://") {
|
||||
url = fmt.Sprintf("https://%s", url)
|
||||
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
// curl provides a wrapper around curl, passing Access JWT along in request
|
||||
func curl(c *cli.Context) error {
|
||||
if err := raven.SetDSN(sentryDSN); err != nil {
|
||||
return err
|
||||
}
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
args := c.Args()
|
||||
if args.Len() < 1 {
|
||||
logger.Error("Please provide the access app and command you wish to run.")
|
||||
return errors.New("incorrect args")
|
||||
}
|
||||
|
||||
cmdArgs, allowRequest := parseAllowRequest(args.Slice())
|
||||
appURL, err := getAppURL(cmdArgs, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tok, err := token.GetTokenIfExists(appURL)
|
||||
if err != nil || tok == "" {
|
||||
if allowRequest {
|
||||
logger.Info("You don't have an Access token set. Please run access token <access application> to fetch one.")
|
||||
return shell.Run("curl", cmdArgs...)
|
||||
}
|
||||
tok, err = token.FetchToken(appURL, logger)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to refresh token: %s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cmdArgs = append(cmdArgs, "-H")
|
||||
cmdArgs = append(cmdArgs, fmt.Sprintf("%s: %s", h2mux.CFAccessTokenHeader, tok))
|
||||
return shell.Run("curl", cmdArgs...)
|
||||
}
|
||||
|
||||
// token dumps provided token to stdout
|
||||
func generateToken(c *cli.Context) error {
|
||||
if err := raven.SetDSN(sentryDSN); err != nil {
|
||||
return err
|
||||
}
|
||||
appURL, err := url.Parse(c.String("app"))
|
||||
if err != nil || c.NumFlags() < 1 {
|
||||
fmt.Fprintln(os.Stderr, "Please provide a url.")
|
||||
return err
|
||||
}
|
||||
tok, err := token.GetTokenIfExists(appURL)
|
||||
if err != nil || tok == "" {
|
||||
fmt.Fprintln(os.Stderr, "Unable to find token for provided application. Please run token command to generate token.")
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprint(os.Stdout, tok); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Failed to write token to stdout.")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sshConfig prints an example SSH config to stdout
|
||||
func sshConfig(c *cli.Context) error {
|
||||
genCertBool := c.Bool(sshGenCertFlag)
|
||||
hostname := c.String(sshHostnameFlag)
|
||||
if hostname == "" {
|
||||
hostname = "[your hostname]"
|
||||
}
|
||||
|
||||
type config struct {
|
||||
Home string
|
||||
ShortLivedCerts bool
|
||||
Hostname string
|
||||
Cloudflared string
|
||||
}
|
||||
|
||||
t := template.Must(template.New("sshConfig").Parse(sshConfigTemplate))
|
||||
return t.Execute(os.Stdout, config{Home: os.Getenv("HOME"), ShortLivedCerts: genCertBool, Hostname: hostname, Cloudflared: cloudflaredPath()})
|
||||
}
|
||||
|
||||
// sshGen generates a short lived certificate for provided hostname
|
||||
func sshGen(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
// get the hostname from the cmdline and error out if its not provided
|
||||
rawHostName := c.String(sshHostnameFlag)
|
||||
hostname, err := validation.ValidateHostname(rawHostName)
|
||||
if err != nil || rawHostName == "" {
|
||||
return cli.ShowCommandHelp(c, "ssh-gen")
|
||||
}
|
||||
|
||||
originURL, err := url.Parse(ensureURLScheme(hostname))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// this fetchToken function mutates the appURL param. We should refactor that
|
||||
fetchTokenURL := &url.URL{}
|
||||
*fetchTokenURL = *originURL
|
||||
cfdToken, err := token.FetchTokenWithRedirect(fetchTokenURL, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := sshgen.GenerateShortLivedCertificate(originURL, cfdToken); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getAppURL will pull the appURL needed for fetching a user's Access token
|
||||
func getAppURL(cmdArgs []string, logger logger.Service) (*url.URL, error) {
|
||||
if len(cmdArgs) < 1 {
|
||||
logger.Error("Please provide a valid URL as the first argument to curl.")
|
||||
return nil, errors.New("not a valid url")
|
||||
}
|
||||
|
||||
u, err := processURL(cmdArgs[0])
|
||||
if err != nil {
|
||||
logger.Error("Please provide a valid URL as the first argument to curl.")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return u, err
|
||||
}
|
||||
|
||||
// parseAllowRequest will parse cmdArgs and return a copy of the args and result
|
||||
// of the allow request was present
|
||||
func parseAllowRequest(cmdArgs []string) ([]string, bool) {
|
||||
if len(cmdArgs) > 1 {
|
||||
if cmdArgs[0] == "--allow-request" || cmdArgs[0] == "-ar" {
|
||||
return cmdArgs[1:], true
|
||||
}
|
||||
}
|
||||
|
||||
return cmdArgs, false
|
||||
}
|
||||
|
||||
// processURL will preprocess the string (parse to a url, convert to punycode, etc).
|
||||
func processURL(s string) (*url.URL, error) {
|
||||
u, err := url.ParseRequestURI(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u.Host == "" {
|
||||
return nil, errors.New("not a valid host")
|
||||
}
|
||||
|
||||
host, err := idna.ToASCII(u.Hostname())
|
||||
if err != nil { // we fail to convert to punycode, just return the url we parsed.
|
||||
return u, nil
|
||||
}
|
||||
if u.Port() != "" {
|
||||
u.Host = fmt.Sprintf("%s:%s", host, u.Port())
|
||||
} else {
|
||||
u.Host = host
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// cloudflaredPath pulls the full path of cloudflared on disk
|
||||
func cloudflaredPath() string {
|
||||
for _, p := range strings.Split(os.Getenv("PATH"), ":") {
|
||||
path := fmt.Sprintf("%s/%s", p, "cloudflared")
|
||||
if isFileThere(path) {
|
||||
return path
|
||||
}
|
||||
}
|
||||
return "cloudflared"
|
||||
}
|
||||
|
||||
// isFileThere will check for the presence of candidate path
|
||||
func isFileThere(candidate string) bool {
|
||||
fi, err := os.Stat(candidate)
|
||||
if err != nil || fi.IsDir() || !fi.Mode().IsRegular() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// verifyTokenAtEdge checks for a token on disk, or generates a new one.
|
||||
// Then makes a request to to the origin with the token to ensure it is valid.
|
||||
// Returns nil if token is valid.
|
||||
func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context, logger logger.Service) error {
|
||||
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
|
||||
if c.IsSet(sshTokenIDFlag) {
|
||||
headers.Add(h2mux.CFAccessClientIDHeader, c.String(sshTokenIDFlag))
|
||||
}
|
||||
if c.IsSet(sshTokenSecretFlag) {
|
||||
headers.Add(h2mux.CFAccessClientSecretHeader, c.String(sshTokenSecretFlag))
|
||||
}
|
||||
options := &carrier.StartOptions{OriginURL: appUrl.String(), Headers: headers}
|
||||
|
||||
if valid, err := isTokenValid(options, logger); err != nil {
|
||||
return err
|
||||
} else if valid {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := token.RemoveTokenIfExists(appUrl); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if valid, err := isTokenValid(options, logger); err != nil {
|
||||
return err
|
||||
} else if !valid {
|
||||
return errors.New("failed to verify token")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isTokenValid makes a request to the origin and returns true if the response was not a 302.
|
||||
func isTokenValid(options *carrier.StartOptions, logger logger.Service) (bool, error) {
|
||||
req, err := carrier.BuildAccessRequest(options, logger)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "Could not create access request")
|
||||
}
|
||||
|
||||
// Do not follow redirects
|
||||
client := &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
Timeout: time.Second * 5,
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// A redirect to login means the token was invalid.
|
||||
return !carrier.IsAccessResponse(resp), nil
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package access
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test_ensureURLScheme(t *testing.T) {
|
||||
type args struct {
|
||||
url string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{"no scheme", args{"localhost:123"}, "https://localhost:123"},
|
||||
{"http scheme", args{"http://test"}, "https://test"},
|
||||
{"https scheme", args{"https://test"}, "https://test"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := ensureURLScheme(tt.args.url); got != tt.want {
|
||||
t.Errorf("ensureURLScheme() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
// ForwardServiceType is used to identify what kind of overwatch service this is
|
||||
const ForwardServiceType = "forward"
|
||||
|
||||
// ForwarderService is used to wrap the access package websocket forwarders
|
||||
// into a service model for the overwatch package.
|
||||
// it also holds a reference to the config object that represents its state
|
||||
type ForwarderService struct {
|
||||
forwarder config.Forwarder
|
||||
shutdown chan struct{}
|
||||
logger logger.Service
|
||||
}
|
||||
|
||||
// NewForwardService creates a new forwarder service
|
||||
func NewForwardService(f config.Forwarder, logger logger.Service) *ForwarderService {
|
||||
return &ForwarderService{forwarder: f, shutdown: make(chan struct{}, 1), logger: logger}
|
||||
}
|
||||
|
||||
// Name is used to figure out this service is related to the others (normally the addr it binds to)
|
||||
// e.g. localhost:78641 or 127.0.0.1:2222 since this is a websocket forwarder
|
||||
func (s *ForwarderService) Name() string {
|
||||
return s.forwarder.Listener
|
||||
}
|
||||
|
||||
// Type is used to identify what kind of overwatch service this is
|
||||
func (s *ForwarderService) Type() string {
|
||||
return ForwardServiceType
|
||||
}
|
||||
|
||||
// Hash is used to figure out if this forwarder is the unchanged or not from the config file updates
|
||||
func (s *ForwarderService) Hash() string {
|
||||
return s.forwarder.Hash()
|
||||
}
|
||||
|
||||
// Shutdown stops the websocket listener
|
||||
func (s *ForwarderService) Shutdown() {
|
||||
s.shutdown <- struct{}{}
|
||||
}
|
||||
|
||||
// Run is the run loop that is started by the overwatch service
|
||||
func (s *ForwarderService) Run() error {
|
||||
return access.StartForwarder(s.forwarder, s.shutdown, s.logger)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
)
|
||||
|
||||
// ResolverServiceType is used to identify what kind of overwatch service this is
|
||||
const ResolverServiceType = "resolver"
|
||||
|
||||
// ResolverService is used to wrap the tunneldns package's DNS over HTTP
|
||||
// into a service model for the overwatch package.
|
||||
// it also holds a reference to the config object that represents its state
|
||||
type ResolverService struct {
|
||||
resolver config.DNSResolver
|
||||
shutdown chan struct{}
|
||||
logger logger.Service
|
||||
}
|
||||
|
||||
// NewResolverService creates a new resolver service
|
||||
func NewResolverService(r config.DNSResolver, logger logger.Service) *ResolverService {
|
||||
return &ResolverService{resolver: r,
|
||||
shutdown: make(chan struct{}),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Name is used to figure out this service is related to the others (normally the addr it binds to)
|
||||
// this is just "resolver" since there can only be one DNS resolver running
|
||||
func (s *ResolverService) Name() string {
|
||||
return ResolverServiceType
|
||||
}
|
||||
|
||||
// Type is used to identify what kind of overwatch service this is
|
||||
func (s *ResolverService) Type() string {
|
||||
return ResolverServiceType
|
||||
}
|
||||
|
||||
// Hash is used to figure out if this forwarder is the unchanged or not from the config file updates
|
||||
func (s *ResolverService) Hash() string {
|
||||
return s.resolver.Hash()
|
||||
}
|
||||
|
||||
// Shutdown stops the tunneldns listener
|
||||
func (s *ResolverService) Shutdown() {
|
||||
s.shutdown <- struct{}{}
|
||||
}
|
||||
|
||||
// Run is the run loop that is started by the overwatch service
|
||||
func (s *ResolverService) Run() error {
|
||||
// create a listener
|
||||
l, err := tunneldns.CreateListener(s.resolver.AddressOrDefault(), s.resolver.PortOrDefault(),
|
||||
s.resolver.UpstreamsOrDefault(), s.resolver.BootstrapsOrDefault(), s.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// start the listener.
|
||||
readySignal := make(chan struct{})
|
||||
err = l.Start(readySignal)
|
||||
if err != nil {
|
||||
l.Stop()
|
||||
return err
|
||||
}
|
||||
<-readySignal
|
||||
s.logger.Infof("start resolver on: %s:%d", s.resolver.AddressOrDefault(), s.resolver.PortOrDefault())
|
||||
|
||||
// wait for shutdown signal
|
||||
<-s.shutdown
|
||||
s.logger.Infof("shutdown on: %s:%d", s.resolver.AddressOrDefault(), s.resolver.PortOrDefault())
|
||||
return l.Stop()
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/overwatch"
|
||||
)
|
||||
|
||||
// AppService is the main service that runs when no command lines flags are passed to cloudflared
|
||||
// it manages all the running services such as tunnels, forwarders, DNS resolver, etc
|
||||
type AppService struct {
|
||||
configManager config.Manager
|
||||
serviceManager overwatch.Manager
|
||||
shutdownC chan struct{}
|
||||
configUpdateChan chan config.Root
|
||||
logger logger.Service
|
||||
}
|
||||
|
||||
// NewAppService creates a new AppService with needed supporting services
|
||||
func NewAppService(configManager config.Manager, serviceManager overwatch.Manager, shutdownC chan struct{}, logger logger.Service) *AppService {
|
||||
return &AppService{
|
||||
configManager: configManager,
|
||||
serviceManager: serviceManager,
|
||||
shutdownC: shutdownC,
|
||||
configUpdateChan: make(chan config.Root),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Run starts the run loop to handle config updates and run forwarders, tunnels, etc
|
||||
func (s *AppService) Run() error {
|
||||
go s.actionLoop()
|
||||
return s.configManager.Start(s)
|
||||
}
|
||||
|
||||
// Shutdown kills all the running services
|
||||
func (s *AppService) Shutdown() error {
|
||||
s.configManager.Shutdown()
|
||||
s.shutdownC <- struct{}{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConfigDidUpdate is a delegate notification from the config manager
|
||||
// it is trigger when the config file has been updated and now the service needs
|
||||
// to update its services accordingly
|
||||
func (s *AppService) ConfigDidUpdate(c config.Root) {
|
||||
s.configUpdateChan <- c
|
||||
}
|
||||
|
||||
// actionLoop handles the actions from running processes
|
||||
func (s *AppService) actionLoop() {
|
||||
for {
|
||||
select {
|
||||
case c := <-s.configUpdateChan:
|
||||
s.handleConfigUpdate(c)
|
||||
case <-s.shutdownC:
|
||||
for _, service := range s.serviceManager.Services() {
|
||||
service.Shutdown()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AppService) handleConfigUpdate(c config.Root) {
|
||||
// handle the client forward listeners
|
||||
activeServices := map[string]struct{}{}
|
||||
for _, f := range c.Forwarders {
|
||||
service := NewForwardService(f, s.logger)
|
||||
s.serviceManager.Add(service)
|
||||
activeServices[service.Name()] = struct{}{}
|
||||
}
|
||||
|
||||
// handle resolver changes
|
||||
if c.Resolver.Enabled {
|
||||
service := NewResolverService(c.Resolver, s.logger)
|
||||
s.serviceManager.Add(service)
|
||||
activeServices[service.Name()] = struct{}{}
|
||||
|
||||
}
|
||||
|
||||
// TODO: TUN-1451 - tunnels
|
||||
|
||||
// remove any services that are no longer active
|
||||
for _, service := range s.serviceManager.Services() {
|
||||
if _, ok := activeServices[service.Name()]; !ok {
|
||||
s.serviceManager.Remove(service.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package buildinfo
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
type BuildInfo struct {
|
||||
GoOS string `json:"go_os"`
|
||||
GoVersion string `json:"go_version"`
|
||||
GoArch string `json:"go_arch"`
|
||||
CloudflaredVersion string `json:"cloudflared_version"`
|
||||
}
|
||||
|
||||
func GetBuildInfo(cloudflaredVersion string) *BuildInfo {
|
||||
return &BuildInfo{
|
||||
GoOS: runtime.GOOS,
|
||||
GoVersion: runtime.Version(),
|
||||
GoArch: runtime.GOARCH,
|
||||
CloudflaredVersion: cloudflaredVersion,
|
||||
}
|
||||
}
|
||||
|
||||
func (bi *BuildInfo) Log(logger logger.Service) {
|
||||
logger.Infof("Version %s", bi.CloudflaredVersion)
|
||||
logger.Infof("GOOS: %s, GOVersion: %s, GoArch: %s", bi.GoOS, bi.GoVersion, bi.GoArch)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package cliutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
type usageError string
|
||||
|
||||
func (ue usageError) Error() string {
|
||||
return string(ue)
|
||||
}
|
||||
|
||||
func UsageError(format string, args ...interface{}) error {
|
||||
if len(args) == 0 {
|
||||
return usageError(format)
|
||||
} else {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
return usageError(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensures exit with error code if actionFunc returns an error
|
||||
func ErrorHandler(actionFunc cli.ActionFunc) cli.ActionFunc {
|
||||
return func(ctx *cli.Context) error {
|
||||
err := actionFunc(ctx)
|
||||
if err != nil {
|
||||
if _, ok := err.(usageError); ok {
|
||||
msg := fmt.Sprintf("%s\nSee 'cloudflared %s --help'.", err.Error(), ctx.Command.FullName())
|
||||
return cli.Exit(msg, -1)
|
||||
}
|
||||
// os.Exits with error code if err is cli.ExitCoder or cli.MultiError
|
||||
cli.HandleExitCoder(err)
|
||||
err = cli.Exit(err.Error(), 1)
|
||||
}
|
||||
logger.SharedWriteManager.Shutdown()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// PrintLoggerSetupError returns an error to stdout to notify when a logger can't start
|
||||
func PrintLoggerSetupError(msg string, err error) error {
|
||||
l, le := logger.New()
|
||||
if le != nil {
|
||||
log.Printf("%s: %s", msg, err)
|
||||
} else {
|
||||
l.Errorf("%s: %s", msg, err)
|
||||
}
|
||||
|
||||
return errors.Wrap(err, msg)
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultConfigFiles is the file names from which we attempt to read configuration.
|
||||
DefaultConfigFiles = []string{"config.yml", "config.yaml"}
|
||||
|
||||
// DefaultUnixConfigLocation is the primary location to find a config file
|
||||
DefaultUnixConfigLocation = "/usr/local/etc/cloudflared"
|
||||
|
||||
// DefaultUnixLogLocation is the primary location to find log files
|
||||
DefaultUnixLogLocation = "/var/log/cloudflared"
|
||||
|
||||
// Launchd doesn't set root env variables, so there is default
|
||||
// Windows default config dir was ~/cloudflare-warp in documentation; let's keep it compatible
|
||||
defaultUserConfigDirs = []string{"~/.cloudflared", "~/.cloudflare-warp", "~/cloudflare-warp"}
|
||||
defaultNixConfigDirs = []string{"/etc/cloudflared", DefaultUnixConfigLocation}
|
||||
)
|
||||
|
||||
const DefaultCredentialFile = "cert.pem"
|
||||
|
||||
// DefaultConfigDirectory returns the default directory of the config file
|
||||
func DefaultConfigDirectory() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
path := os.Getenv("CFDPATH")
|
||||
if path == "" {
|
||||
path = filepath.Join(os.Getenv("ProgramFiles(x86)"), "cloudflared")
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) { //doesn't exist, so return an empty failure string
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
return DefaultUnixConfigLocation
|
||||
}
|
||||
|
||||
// DefaultLogDirectory returns the default directory for log files
|
||||
func DefaultLogDirectory() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return DefaultConfigDirectory()
|
||||
}
|
||||
return DefaultUnixLogLocation
|
||||
}
|
||||
|
||||
// DefaultConfigPath returns the default location of a config file
|
||||
func DefaultConfigPath() string {
|
||||
dir := DefaultConfigDirectory()
|
||||
if dir == "" {
|
||||
return DefaultConfigFiles[0]
|
||||
}
|
||||
return filepath.Join(dir, DefaultConfigFiles[0])
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// ignore missing files
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
f.Close()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// FindInputSourceContext pulls the input source from the config flag.
|
||||
func FindInputSourceContext(context *cli.Context) (altsrc.InputSourceContext, error) {
|
||||
if context.String("config") != "" {
|
||||
return altsrc.NewYamlSourceFromFile(context.String("config"))
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// FindDefaultConfigPath returns the first path that contains a config file.
|
||||
// If none of the combination of DefaultConfigSearchDirectories() and DefaultConfigFiles
|
||||
// contains a config file, return empty string.
|
||||
func FindDefaultConfigPath() string {
|
||||
for _, configDir := range DefaultConfigSearchDirectories() {
|
||||
for _, configFile := range DefaultConfigFiles {
|
||||
dirPath, err := homedir.Expand(configDir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(dirPath, configFile)
|
||||
if ok, _ := FileExists(path); ok {
|
||||
return path
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// FindOrCreateConfigPath returns the first path that contains a config file
|
||||
// or creates one in the primary default path if it doesn't exist
|
||||
func FindOrCreateConfigPath() string {
|
||||
path := FindDefaultConfigPath()
|
||||
|
||||
if path == "" {
|
||||
// create the default directory if it doesn't exist
|
||||
path = DefaultConfigPath()
|
||||
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// write a new config file out
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
logDir := DefaultLogDirectory()
|
||||
os.MkdirAll(logDir, os.ModePerm) //try and create it. Doesn't matter if it succeed or not, only byproduct will be no logs
|
||||
|
||||
c := Root{
|
||||
LogDirectory: logDir,
|
||||
}
|
||||
if err := yaml.NewEncoder(file).Encode(&c); err != nil {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
// FindLogSettings gets the log directory and level from the config file
|
||||
func FindLogSettings() (string, string) {
|
||||
configPath := FindOrCreateConfigPath()
|
||||
defaultDirectory := DefaultLogDirectory()
|
||||
defaultLevel := "info"
|
||||
|
||||
file, err := os.Open(configPath)
|
||||
if err != nil {
|
||||
return defaultDirectory, defaultLevel
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var config Root
|
||||
if err := yaml.NewDecoder(file).Decode(&config); err != nil {
|
||||
return defaultDirectory, defaultLevel
|
||||
}
|
||||
|
||||
directory := defaultDirectory
|
||||
if config.LogDirectory != "" {
|
||||
directory = config.LogDirectory
|
||||
}
|
||||
|
||||
level := defaultLevel
|
||||
if config.LogLevel != "" {
|
||||
level = config.LogLevel
|
||||
}
|
||||
return directory, level
|
||||
}
|
||||
|
||||
// ValidateUnixSocket ensures --unix-socket param is used exclusively
|
||||
// i.e. it fails if a user specifies both --url and --unix-socket
|
||||
func ValidateUnixSocket(c *cli.Context) (string, error) {
|
||||
if c.IsSet("unix-socket") && (c.IsSet("url") || c.NArg() > 0) {
|
||||
return "", errors.New("--unix-socket must be used exclusivly.")
|
||||
}
|
||||
return c.String("unix-socket"), nil
|
||||
}
|
||||
|
||||
// ValidateUrl will validate url flag correctness. It can be either from --url or argument
|
||||
// Notice ValidateUnixSocket, it will enforce --unix-socket is not used with --url or argument
|
||||
func ValidateUrl(c *cli.Context, allowFromArgs bool) (string, error) {
|
||||
var url = c.String("url")
|
||||
if allowFromArgs && c.NArg() > 0 {
|
||||
if c.IsSet("url") {
|
||||
return "", errors.New("Specified origin urls using both --url and argument. Decide which one you want, I can only support one.")
|
||||
}
|
||||
url = c.Args().Get(0)
|
||||
}
|
||||
validUrl, err := validation.ValidateUrl(url)
|
||||
return validUrl, err
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/watcher"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Notifier sends out config updates
|
||||
type Notifier interface {
|
||||
ConfigDidUpdate(Root)
|
||||
}
|
||||
|
||||
// Manager is the base functions of the config manager
|
||||
type Manager interface {
|
||||
Start(Notifier) error
|
||||
Shutdown()
|
||||
}
|
||||
|
||||
// FileManager watches the yaml config for changes
|
||||
// sends updates to the service to reconfigure to match the updated config
|
||||
type FileManager struct {
|
||||
watcher watcher.Notifier
|
||||
notifier Notifier
|
||||
configPath string
|
||||
logger logger.Service
|
||||
ReadConfig func(string) (Root, error)
|
||||
}
|
||||
|
||||
// NewFileManager creates a config manager
|
||||
func NewFileManager(watcher watcher.Notifier, configPath string, logger logger.Service) (*FileManager, error) {
|
||||
m := &FileManager{
|
||||
watcher: watcher,
|
||||
configPath: configPath,
|
||||
logger: logger,
|
||||
ReadConfig: readConfigFromPath,
|
||||
}
|
||||
err := watcher.Add(configPath)
|
||||
return m, err
|
||||
}
|
||||
|
||||
// Start starts the runloop to watch for config changes
|
||||
func (m *FileManager) Start(notifier Notifier) error {
|
||||
m.notifier = notifier
|
||||
|
||||
// update the notifier with a fresh config on start
|
||||
config, err := m.GetConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
notifier.ConfigDidUpdate(config)
|
||||
|
||||
m.watcher.Start(m)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConfig reads the yaml file from the disk
|
||||
func (m *FileManager) GetConfig() (Root, error) {
|
||||
return m.ReadConfig(m.configPath)
|
||||
}
|
||||
|
||||
// Shutdown stops the watcher
|
||||
func (m *FileManager) Shutdown() {
|
||||
m.watcher.Shutdown()
|
||||
}
|
||||
|
||||
func readConfigFromPath(configPath string) (Root, error) {
|
||||
if configPath == "" {
|
||||
return Root{}, errors.New("unable to find config file")
|
||||
}
|
||||
|
||||
file, err := os.Open(configPath)
|
||||
if err != nil {
|
||||
return Root{}, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var config Root
|
||||
if err := yaml.NewDecoder(file).Decode(&config); err != nil {
|
||||
return Root{}, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// File change notifications from the watcher
|
||||
|
||||
// WatcherItemDidChange triggers when the yaml config is updated
|
||||
// sends the updated config to the service to reload its state
|
||||
func (m *FileManager) WatcherItemDidChange(filepath string) {
|
||||
config, err := m.GetConfig()
|
||||
if err != nil {
|
||||
m.logger.Errorf("Failed to read new config: %s", err)
|
||||
return
|
||||
}
|
||||
m.logger.Info("Config file has been updated")
|
||||
m.notifier.ConfigDidUpdate(config)
|
||||
}
|
||||
|
||||
// WatcherDidError notifies of errors with the file watcher
|
||||
func (m *FileManager) WatcherDidError(err error) {
|
||||
m.logger.Errorf("Config watcher encountered an error: %s", err)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/watcher"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type mockNotifier struct {
|
||||
configs []Root
|
||||
}
|
||||
|
||||
func (n *mockNotifier) ConfigDidUpdate(c Root) {
|
||||
n.configs = append(n.configs, c)
|
||||
}
|
||||
|
||||
type mockFileWatcher struct {
|
||||
path string
|
||||
notifier watcher.Notification
|
||||
ready chan struct{}
|
||||
}
|
||||
|
||||
func (w *mockFileWatcher) Start(n watcher.Notification) {
|
||||
w.notifier = n
|
||||
w.ready <- struct{}{}
|
||||
}
|
||||
|
||||
func (w *mockFileWatcher) Add(string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *mockFileWatcher) Shutdown() {
|
||||
|
||||
}
|
||||
|
||||
func (w *mockFileWatcher) TriggerChange() {
|
||||
w.notifier.WatcherItemDidChange(w.path)
|
||||
}
|
||||
|
||||
func TestConfigChanged(t *testing.T) {
|
||||
filePath := "config.yaml"
|
||||
f, err := os.Create(filePath)
|
||||
assert.NoError(t, err)
|
||||
defer func() {
|
||||
f.Close()
|
||||
os.Remove(filePath)
|
||||
}()
|
||||
c := &Root{
|
||||
Forwarders: []Forwarder{
|
||||
{
|
||||
URL: "test.daltoniam.com",
|
||||
Listener: "127.0.0.1:8080",
|
||||
},
|
||||
},
|
||||
}
|
||||
configRead := func(configPath string) (Root, error) {
|
||||
return *c, nil
|
||||
}
|
||||
wait := make(chan struct{})
|
||||
w := &mockFileWatcher{path: filePath, ready: wait}
|
||||
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
service, err := NewFileManager(w, filePath, logger)
|
||||
service.ReadConfig = configRead
|
||||
assert.NoError(t, err)
|
||||
|
||||
n := &mockNotifier{}
|
||||
go service.Start(n)
|
||||
|
||||
<-wait
|
||||
c.Forwarders = append(c.Forwarders, Forwarder{URL: "add.daltoniam.com", Listener: "127.0.0.1:8081"})
|
||||
w.TriggerChange()
|
||||
|
||||
service.Shutdown()
|
||||
|
||||
assert.Len(t, n.configs, 2, "did not get 2 config updates as expected")
|
||||
assert.Len(t, n.configs[0].Forwarders, 1, "not the amount of forwarders expected")
|
||||
assert.Len(t, n.configs[1].Forwarders, 2, "not the amount of forwarders expected")
|
||||
|
||||
assert.Equal(t, n.configs[0].Forwarders[0].Hash(), c.Forwarders[0].Hash(), "forwarder hashes don't match")
|
||||
assert.Equal(t, n.configs[1].Forwarders[0].Hash(), c.Forwarders[0].Hash(), "forwarder hashes don't match")
|
||||
assert.Equal(t, n.configs[1].Forwarders[1].Hash(), c.Forwarders[1].Hash(), "forwarder hashes don't match")
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Forwarder represents a client side listener to forward traffic to the edge
|
||||
type Forwarder struct {
|
||||
URL string `json:"url"`
|
||||
Listener string `json:"listener"`
|
||||
TokenClientID string `json:"service_token_id" yaml:"serviceTokenID"`
|
||||
TokenSecret string `json:"secret_token_id" yaml:"serviceTokenSecret"`
|
||||
Destination string `json:"destination"`
|
||||
}
|
||||
|
||||
// Tunnel represents a tunnel that should be started
|
||||
type Tunnel struct {
|
||||
URL string `json:"url"`
|
||||
Origin string `json:"origin"`
|
||||
ProtocolType string `json:"type"`
|
||||
}
|
||||
|
||||
// DNSResolver represents a client side DNS resolver
|
||||
type DNSResolver struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Address string `json:"address,omitempty"`
|
||||
Port uint16 `json:"port,omitempty"`
|
||||
Upstreams []string `json:"upstreams,omitempty"`
|
||||
Bootstraps []string `json:"bootstraps,omitempty"`
|
||||
}
|
||||
|
||||
// Root is the base options to configure the service
|
||||
type Root struct {
|
||||
LogDirectory string `json:"log_directory" yaml:"logDirectory,omitempty"`
|
||||
LogLevel string `json:"log_level" yaml:"logLevel,omitempty"`
|
||||
Forwarders []Forwarder `json:"forwarders,omitempty" yaml:"forwarders,omitempty"`
|
||||
Tunnels []Tunnel `json:"tunnels,omitempty" yaml:"tunnels,omitempty"`
|
||||
Resolver DNSResolver `json:"resolver,omitempty" yaml:"resolver,omitempty"`
|
||||
}
|
||||
|
||||
// Hash returns the computed values to see if the forwarder values change
|
||||
func (f *Forwarder) Hash() string {
|
||||
h := md5.New()
|
||||
io.WriteString(h, f.URL)
|
||||
io.WriteString(h, f.Listener)
|
||||
io.WriteString(h, f.TokenClientID)
|
||||
io.WriteString(h, f.TokenSecret)
|
||||
io.WriteString(h, f.Destination)
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
// Hash returns the computed values to see if the forwarder values change
|
||||
func (r *DNSResolver) Hash() string {
|
||||
h := md5.New()
|
||||
io.WriteString(h, r.Address)
|
||||
io.WriteString(h, strings.Join(r.Bootstraps, ","))
|
||||
io.WriteString(h, strings.Join(r.Upstreams, ","))
|
||||
io.WriteString(h, fmt.Sprintf("%d", r.Port))
|
||||
io.WriteString(h, fmt.Sprintf("%v", r.Enabled))
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
// EnabledOrDefault returns the enabled property
|
||||
func (r *DNSResolver) EnabledOrDefault() bool {
|
||||
return r.Enabled
|
||||
}
|
||||
|
||||
// AddressOrDefault returns the address or returns the default if empty
|
||||
func (r *DNSResolver) AddressOrDefault() string {
|
||||
if r.Address != "" {
|
||||
return r.Address
|
||||
}
|
||||
return "localhost"
|
||||
}
|
||||
|
||||
// PortOrDefault return the port or returns the default if 0
|
||||
func (r *DNSResolver) PortOrDefault() uint16 {
|
||||
if r.Port > 0 {
|
||||
return r.Port
|
||||
}
|
||||
return 53
|
||||
}
|
||||
|
||||
// UpstreamsOrDefault returns the upstreams or returns the default if empty
|
||||
func (r *DNSResolver) UpstreamsOrDefault() []string {
|
||||
if len(r.Upstreams) > 0 {
|
||||
return r.Upstreams
|
||||
}
|
||||
return []string{"https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"}
|
||||
}
|
||||
|
||||
// BootstrapsOrDefault returns the bootstraps or returns the default if empty
|
||||
func (r *DNSResolver) BootstrapsOrDefault() []string {
|
||||
if len(r.Bootstraps) > 0 {
|
||||
return r.Bootstraps
|
||||
}
|
||||
return []string{"https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"}
|
||||
}
|
||||
@@ -1,318 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
"gopkg.in/urfave/cli.v2/altsrc"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultConfigFiles = []string{"config.yml", "config.yaml"}
|
||||
|
||||
// Launchd doesn't set root env variables, so there is default
|
||||
// Windows default config dir was ~/cloudflare-warp in documentation; let's keep it compatible
|
||||
defaultConfigDirs = []string{"~/.cloudflared", "~/.cloudflare-warp", "~/cloudflare-warp", "/usr/local/etc/cloudflared", "/etc/cloudflared"}
|
||||
)
|
||||
|
||||
const defaultCredentialFile = "cert.pem"
|
||||
|
||||
func fileExists(path string) (bool, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// ignore missing files
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
f.Close()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// returns the first path that contains a cert.pem file. If none of the defaultConfigDirs
|
||||
// (differs by OS for legacy reasons) contains a cert.pem file, return empty string
|
||||
func findDefaultOriginCertPath() string {
|
||||
for _, defaultConfigDir := range defaultConfigDirs {
|
||||
originCertPath, _ := homedir.Expand(filepath.Join(defaultConfigDir, defaultCredentialFile))
|
||||
if ok, _ := fileExists(originCertPath); ok {
|
||||
return originCertPath
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// returns the first path that contains a config file. If none of the combination of
|
||||
// defaultConfigDirs (differs by OS for legacy reasons) and defaultConfigFiles
|
||||
// contains a config file, return empty string
|
||||
func findDefaultConfigPath() string {
|
||||
for _, configDir := range defaultConfigDirs {
|
||||
for _, configFile := range defaultConfigFiles {
|
||||
dirPath, err := homedir.Expand(configDir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(dirPath, configFile)
|
||||
if ok, _ := fileExists(path); ok {
|
||||
return path
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func findInputSourceContext(context *cli.Context) (altsrc.InputSourceContext, error) {
|
||||
if context.String("config") != "" {
|
||||
return altsrc.NewYamlSourceFromFile(context.String("config"))
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func generateRandomClientID() string {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
id := make([]byte, 32)
|
||||
r.Read(id)
|
||||
return hex.EncodeToString(id)
|
||||
}
|
||||
|
||||
func enoughOptionsSet(c *cli.Context) bool {
|
||||
// For cloudflared to work, the user needs to at least provide a hostname,
|
||||
// or runs as stand alone DNS proxy .
|
||||
// When using sudo, use -E flag to preserve env vars
|
||||
if c.NumFlags() == 0 && c.NArg() == 0 && os.Getenv("TUNNEL_HOSTNAME") == "" && os.Getenv("TUNNEL_DNS") == "" {
|
||||
if isRunningFromTerminal() {
|
||||
logger.Errorf("No arguments were provided. You need to at least specify the hostname for this tunnel. See %s", quickStartUrl)
|
||||
logger.Infof("If you want to run Argo Tunnel client as a stand alone DNS proxy, run with --proxy-dns option or set TUNNEL_DNS environment variable.")
|
||||
} else {
|
||||
logger.Errorf("You need to specify all the options in a configuration file, or use environment variables. See %s and %s", serviceUrl, argumentsUrl)
|
||||
logger.Infof("If you want to run Argo Tunnel client as a stand alone DNS proxy, specify proxy-dns option in the configuration file, or set TUNNEL_DNS environment variable.")
|
||||
}
|
||||
cli.ShowAppHelp(c)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func handleDeprecatedOptions(c *cli.Context) error {
|
||||
// Fail if the user provided an old authentication method
|
||||
if c.IsSet("api-key") || c.IsSet("api-email") || c.IsSet("api-ca-key") {
|
||||
logger.Error("You don't need to give us your api-key anymore. Please use the new login method. Just run cloudflared login")
|
||||
return fmt.Errorf("Client provided deprecated options")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateUrl(c *cli.Context) (string, error) {
|
||||
var url = c.String("url")
|
||||
if c.NArg() > 0 {
|
||||
if !c.IsSet("url") {
|
||||
return "", errors.New("Please specify an origin URL.")
|
||||
}
|
||||
}
|
||||
validUrl, err := validation.ValidateUrl(url)
|
||||
return validUrl, err
|
||||
}
|
||||
|
||||
func logClientOptions(c *cli.Context) {
|
||||
flags := make(map[string]interface{})
|
||||
for _, flag := range c.LocalFlagNames() {
|
||||
flags[flag] = c.Generic(flag)
|
||||
}
|
||||
if len(flags) > 0 {
|
||||
logger.Infof("Flags %v", flags)
|
||||
}
|
||||
|
||||
envs := make(map[string]string)
|
||||
// Find env variables for Argo Tunnel
|
||||
for _, env := range os.Environ() {
|
||||
// All Argo Tunnel env variables start with TUNNEL_
|
||||
if strings.Contains(env, "TUNNEL_") {
|
||||
vars := strings.Split(env, "=")
|
||||
if len(vars) == 2 {
|
||||
envs[vars[0]] = vars[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(envs) > 0 {
|
||||
logger.Infof("Environmental variables %v", envs)
|
||||
}
|
||||
}
|
||||
|
||||
func dnsProxyStandAlone(c *cli.Context) bool {
|
||||
return c.IsSet("proxy-dns") && (!c.IsSet("hostname") && !c.IsSet("tag") && !c.IsSet("hello-world"))
|
||||
}
|
||||
|
||||
func getOriginCert(c *cli.Context) ([]byte, error) {
|
||||
if c.String("origincert") == "" {
|
||||
logger.Warnf("Cannot determine default origin certificate path. No file %s in %v", defaultCredentialFile, defaultConfigDirs)
|
||||
if isRunningFromTerminal() {
|
||||
logger.Errorf("You need to specify the origin certificate path with --origincert option, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", argumentsUrl)
|
||||
return nil, fmt.Errorf("Client didn't specify origincert path when running from terminal")
|
||||
} else {
|
||||
logger.Errorf("You need to specify the origin certificate path by specifying the origincert option in the configuration file, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", serviceUrl)
|
||||
return nil, fmt.Errorf("Client didn't specify origincert path")
|
||||
}
|
||||
}
|
||||
// Check that the user has acquired a certificate using the login command
|
||||
originCertPath, err := homedir.Expand(c.String("origincert"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Cannot resolve path %s", c.String("origincert"))
|
||||
return nil, fmt.Errorf("Cannot resolve path %s", c.String("origincert"))
|
||||
}
|
||||
ok, err := fileExists(originCertPath)
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot check if origin cert exists at path %s", c.String("origincert"))
|
||||
return nil, fmt.Errorf("Cannot check if origin cert exists at path %s", c.String("origincert"))
|
||||
}
|
||||
if !ok {
|
||||
logger.Errorf(`Cannot find a valid certificate for your origin at the path:
|
||||
|
||||
%s
|
||||
|
||||
If the path above is wrong, specify the path with the -origincert option.
|
||||
If you don't have a certificate signed by Cloudflare, run the command:
|
||||
|
||||
%s login
|
||||
`, originCertPath, os.Args[0])
|
||||
return nil, fmt.Errorf("Cannot find a valid certificate at the path %s", originCertPath)
|
||||
}
|
||||
// Easier to send the certificate as []byte via RPC than decoding it at this point
|
||||
originCert, err := ioutil.ReadFile(originCertPath)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Cannot read %s to load origin certificate", originCertPath)
|
||||
return nil, fmt.Errorf("Cannot read %s to load origin certificate", originCertPath)
|
||||
}
|
||||
return originCert, nil
|
||||
}
|
||||
|
||||
func prepareTunnelConfig(c *cli.Context, buildInfo *origin.BuildInfo, logger, protoLogger *logrus.Logger) (*origin.TunnelConfig, error) {
|
||||
hostname, err := validation.ValidateHostname(c.String("hostname"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Invalid hostname")
|
||||
return nil, errors.Wrap(err, "Invalid hostname")
|
||||
}
|
||||
clientID := c.String("id")
|
||||
if !c.IsSet("id") {
|
||||
clientID = generateRandomClientID()
|
||||
}
|
||||
|
||||
tags, err := NewTagSliceFromCLI(c.StringSlice("tag"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Tag parse failure")
|
||||
return nil, errors.Wrap(err, "Tag parse failure")
|
||||
}
|
||||
|
||||
tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID})
|
||||
|
||||
url, err := validateUrl(c)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error validating url")
|
||||
return nil, errors.Wrap(err, "Error validating url")
|
||||
}
|
||||
logger.Infof("Proxying tunnel requests to %s", url)
|
||||
|
||||
originCert, err := getOriginCert(c)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error getting origin cert")
|
||||
}
|
||||
|
||||
originCertPool, err := loadCertPool(c, logger)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error loading cert pool")
|
||||
return nil, errors.Wrap(err, "Error loading cert pool")
|
||||
}
|
||||
|
||||
tunnelMetrics := origin.NewTunnelMetrics()
|
||||
httpTransport := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: c.Duration("proxy-connect-timeout"),
|
||||
KeepAlive: c.Duration("proxy-tcp-keepalive"),
|
||||
DualStack: !c.Bool("proxy-no-happy-eyeballs"),
|
||||
}).DialContext,
|
||||
MaxIdleConns: 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")},
|
||||
}
|
||||
|
||||
if !c.IsSet("hello-world") && c.IsSet("origin-server-name") {
|
||||
httpTransport.TLSClientConfig.ServerName = c.String("origin-server-name")
|
||||
}
|
||||
|
||||
return &origin.TunnelConfig{
|
||||
EdgeAddrs: c.StringSlice("edge"),
|
||||
OriginUrl: url,
|
||||
Hostname: hostname,
|
||||
OriginCert: originCert,
|
||||
TlsConfig: tlsconfig.CreateTunnelConfig(c, c.StringSlice("edge")),
|
||||
ClientTlsConfig: httpTransport.TLSClientConfig,
|
||||
Retries: c.Uint("retries"),
|
||||
HeartbeatInterval: c.Duration("heartbeat-interval"),
|
||||
MaxHeartbeats: c.Uint64("heartbeat-count"),
|
||||
ClientID: clientID,
|
||||
BuildInfo: buildInfo,
|
||||
ReportedVersion: Version,
|
||||
LBPool: c.String("lb-pool"),
|
||||
Tags: tags,
|
||||
HAConnections: c.Int("ha-connections"),
|
||||
HTTPTransport: httpTransport,
|
||||
Metrics: tunnelMetrics,
|
||||
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
|
||||
ProtocolLogger: protoLogger,
|
||||
Logger: logger,
|
||||
IsAutoupdated: c.Bool("is-autoupdated"),
|
||||
GracePeriod: c.Duration("grace-period"),
|
||||
RunFromTerminal: isRunningFromTerminal(),
|
||||
NoChunkedEncoding: c.Bool("no-chunked-encoding"),
|
||||
CompressionQuality: c.Uint64("compression-quality"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func loadCertPool(c *cli.Context, logger *logrus.Logger) (*x509.CertPool, error) {
|
||||
const originCAPoolFlag = "origin-ca-pool"
|
||||
originCAPoolFilename := c.String(originCAPoolFlag)
|
||||
var originCustomCAPool []byte
|
||||
|
||||
if originCAPoolFilename != "" {
|
||||
var err error
|
||||
originCustomCAPool, err = ioutil.ReadFile(originCAPoolFilename)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("unable to read the file %s for --%s", originCAPoolFilename, originCAPoolFlag))
|
||||
}
|
||||
}
|
||||
|
||||
originCertPool, err := tlsconfig.LoadOriginCertPool(originCustomCAPool)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error loading the certificate pool")
|
||||
}
|
||||
|
||||
// Windows users should be notified that they can use the flag
|
||||
if runtime.GOOS == "windows" && originCAPoolFilename == "" {
|
||||
logger.Infof("cloudflared does not support loading the system root certificate pool on Windows. Please use the --%s to specify it", originCAPoolFlag)
|
||||
}
|
||||
|
||||
return originCertPool, nil
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
// Package encrypter is suitable for encrypting messages you would like to securely share between two points.
|
||||
// Useful for providing end to end encryption (E2EE). It uses Box (NaCl) for encrypting the messages.
|
||||
// tldr is it uses Elliptic Curves (Curve25519) for the keys, XSalsa20 and Poly1305 for encryption.
|
||||
// You can read more here https://godoc.org/golang.org/x/crypto/nacl/box.
|
||||
//
|
||||
// msg := []byte("super safe message.")
|
||||
// alice, err := New("alice_priv_key.pem", "alice_pub_key.pem")
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// bob, err := New("bob_priv_key.pem", "bob_pub_key.pem")
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// encrypted, err := alice.Encrypt(msg, bob.PublicKey())
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// data, err := bob.Decrypt(encrypted, alice.PublicKey())
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Println(string(data))
|
||||
package encrypter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
)
|
||||
|
||||
// Encrypter represents a keypair value with auxiliary functions to make
|
||||
// doing encryption and decryption easier
|
||||
type Encrypter struct {
|
||||
privateKey *[32]byte
|
||||
publicKey *[32]byte
|
||||
}
|
||||
|
||||
// New returns a new encrypter with initialized keypair
|
||||
func New(privateKey, publicKey string) (*Encrypter, error) {
|
||||
e := &Encrypter{}
|
||||
pubKey, key, err := e.fetchOrGenerateKeys(privateKey, publicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.privateKey, e.publicKey = key, pubKey
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// PublicKey returns a base64 encoded public key. Useful for transport (like in HTTP requests)
|
||||
func (e *Encrypter) PublicKey() string {
|
||||
return base64.URLEncoding.EncodeToString(e.publicKey[:])
|
||||
}
|
||||
|
||||
// Decrypt data that was encrypted using our publicKey. It will use our privateKey and the sender's publicKey to decrypt
|
||||
// data is an encrypted buffer of data, mostly like from the Encrypt function. Messages contain the nonce data on the front
|
||||
// of the message.
|
||||
// senderPublicKey is a base64 encoded version of the sender's public key (most likely from the PublicKey function).
|
||||
// The return value is the decrypted buffer or an error.
|
||||
func (e *Encrypter) Decrypt(data []byte, senderPublicKey string) ([]byte, error) {
|
||||
var decryptNonce [24]byte
|
||||
copy(decryptNonce[:], data[:24]) // we pull the nonce from the front of the actual message.
|
||||
pubKey, err := e.decodePublicKey(senderPublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decrypted, ok := box.Open(nil, data[24:], &decryptNonce, pubKey, e.privateKey)
|
||||
if !ok {
|
||||
return nil, errors.New("failed to decrypt message")
|
||||
}
|
||||
return decrypted, nil
|
||||
}
|
||||
|
||||
// Encrypt data using our privateKey and the recipient publicKey
|
||||
// data is a buffer of data that we would like to encrypt. Messages will have the nonce added to front
|
||||
// as they have to unique for each message shared.
|
||||
// recipientPublicKey is a base64 encoded version of the sender's public key (most likely from the PublicKey function).
|
||||
// The return value is the encrypted buffer or an error.
|
||||
func (e *Encrypter) Encrypt(data []byte, recipientPublicKey string) ([]byte, error) {
|
||||
var nonce [24]byte
|
||||
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pubKey, err := e.decodePublicKey(recipientPublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// This encrypts msg and adds the nonce to the front of the message, since the nonce has to be
|
||||
// the same for encrypting and decrypting
|
||||
return box.Seal(nonce[:], data, &nonce, pubKey, e.privateKey), nil
|
||||
}
|
||||
|
||||
// WriteKeys keys will take the currently initialized keypair and write them to provided filenames
|
||||
func (e *Encrypter) WriteKeys(privateKey, publicKey string) error {
|
||||
if err := e.writeKey(e.privateKey[:], "BOX PRIVATE KEY", privateKey); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.writeKey(e.publicKey[:], "PUBLIC KEY", publicKey)
|
||||
}
|
||||
|
||||
// fetchOrGenerateKeys will either load or create a keypair if it doesn't exist
|
||||
func (e *Encrypter) fetchOrGenerateKeys(privateKey, publicKey string) (*[32]byte, *[32]byte, error) {
|
||||
key, err := e.fetchKey(privateKey)
|
||||
if os.IsNotExist(err) {
|
||||
return box.GenerateKey(rand.Reader)
|
||||
} else if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pub, err := e.fetchKey(publicKey)
|
||||
if os.IsNotExist(err) {
|
||||
return box.GenerateKey(rand.Reader)
|
||||
} else if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return pub, key, nil
|
||||
}
|
||||
|
||||
// writeKey will write a key to disk in DER format (it's a standard pem key)
|
||||
func (e *Encrypter) writeKey(key []byte, pemType, filename string) error {
|
||||
data := pem.EncodeToMemory(&pem.Block{
|
||||
Type: pemType,
|
||||
Bytes: key,
|
||||
})
|
||||
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = f.Write(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchKey will load a a DER formatted key from disk
|
||||
func (e *Encrypter) fetchKey(filename string) (*[32]byte, error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
io.Copy(buf, f)
|
||||
|
||||
p, _ := pem.Decode(buf.Bytes())
|
||||
if p == nil {
|
||||
return nil, errors.New("Failed to decode key")
|
||||
}
|
||||
var newKey [32]byte
|
||||
copy(newKey[:], p.Bytes)
|
||||
|
||||
return &newKey, nil
|
||||
}
|
||||
|
||||
// decodePublicKey will base64 decode the provided key to the box representation
|
||||
func (e *Encrypter) decodePublicKey(key string) (*[32]byte, error) {
|
||||
pub, err := base64.URLEncoding.DecodeString(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var newKey [32]byte
|
||||
copy(newKey[:], pub)
|
||||
return &newKey, nil
|
||||
}
|
||||
@@ -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{}) {
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/hello"
|
||||
)
|
||||
|
||||
|
||||
func helloWorld(c *cli.Context) error {
|
||||
address := fmt.Sprintf(":%d", c.Int("port"))
|
||||
listener, err := hello.CreateTLSListener(address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer listener.Close()
|
||||
err = hello.StartHelloWorldServer(logger, listener, nil)
|
||||
return err
|
||||
}
|
||||
@@ -7,7 +7,11 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
cli "gopkg.in/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 "github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
@@ -18,19 +22,25 @@ func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
&cli.Command{
|
||||
Name: "install",
|
||||
Usage: "Install Argo Tunnel as a system service",
|
||||
Action: installLinuxService,
|
||||
Action: cliutil.ErrorHandler(installLinuxService),
|
||||
},
|
||||
&cli.Command{
|
||||
Name: "uninstall",
|
||||
Usage: "Uninstall the Argo Tunnel service",
|
||||
Action: uninstallLinuxService,
|
||||
Action: cliutil.ErrorHandler(uninstallLinuxService),
|
||||
},
|
||||
},
|
||||
})
|
||||
app.Run(os.Args)
|
||||
}
|
||||
|
||||
const serviceConfigDir = "/etc/cloudflared"
|
||||
// The directory and files that are used by the service.
|
||||
// These are hard-coded in the templates below.
|
||||
const (
|
||||
serviceConfigDir = "/etc/cloudflared"
|
||||
serviceConfigFile = "config.yml"
|
||||
serviceCredentialFile = "cert.pem"
|
||||
)
|
||||
|
||||
var systemdTemplates = []ServiceTemplate{
|
||||
{
|
||||
@@ -57,7 +67,7 @@ Description=Update Argo Tunnel
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/bin/bash -c '{{ .Path }} update; code=$?; if [ $code -eq 64 ]; then systemctl restart cloudflared; exit 0; fi; exit $code'
|
||||
ExecStart=/bin/bash -c '{{ .Path }} update; code=$?; if [ $code -eq 11 ]; then systemctl restart cloudflared; exit 0; fi; exit $code'
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -66,7 +76,7 @@ ExecStart=/bin/bash -c '{{ .Path }} update; code=$?; if [ $code -eq 64 ]; then s
|
||||
Description=Update Argo Tunnel
|
||||
|
||||
[Timer]
|
||||
OnUnitActiveSec=1d
|
||||
OnCalendar=daily
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -77,7 +87,8 @@ WantedBy=timers.target
|
||||
var sysvTemplate = ServiceTemplate{
|
||||
Path: "/etc/init.d/cloudflared",
|
||||
FileMode: 0755,
|
||||
Content: `# For RedHat and cousins:
|
||||
Content: `#!/bin/sh
|
||||
# For RedHat and cousins:
|
||||
# chkconfig: 2345 99 01
|
||||
# description: Argo Tunnel agent
|
||||
# processname: {{.Path}}
|
||||
@@ -110,10 +121,6 @@ case "$1" in
|
||||
echo "Starting $name"
|
||||
$cmd >> "$stdout_log" 2>> "$stderr_log" &
|
||||
echo $! > "$pid_file"
|
||||
if ! is_running; then
|
||||
echo "Unable to start, see $stdout_log and $stderr_log"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
stop)
|
||||
@@ -174,59 +181,90 @@ func isSystemd() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
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 {
|
||||
if err := copyCredential(srcCredentialPath, destCredentialPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
srcConfigPath := filepath.Join(userConfigDir, userConfigFile)
|
||||
destConfigPath := filepath.Join(serviceConfigDir, serviceConfigFile)
|
||||
if srcConfigPath != destConfigPath {
|
||||
if err := copyConfig(srcConfigPath, destConfigPath); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("Copied %s to %s", srcConfigPath, destConfigPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func installLinuxService(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
etPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error determining executable path: %v", err)
|
||||
}
|
||||
templateArgs := ServiceTemplateArgs{Path: etPath}
|
||||
|
||||
defaultConfigDir := filepath.Dir(c.String("config"))
|
||||
defaultConfigFile := filepath.Base(c.String("config"))
|
||||
if err = copyCredentials(serviceConfigDir, defaultConfigDir, defaultConfigFile, defaultCredentialFile); err != nil {
|
||||
logger.WithError(err).Infof("Failed to copy user configuration. Before running the service, ensure that %s contains two files, %s and %s",
|
||||
serviceConfigDir, defaultCredentialFile, defaultConfigFiles[0])
|
||||
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
|
||||
}
|
||||
|
||||
switch {
|
||||
case isSystemd():
|
||||
logger.Infof("Using Systemd")
|
||||
return installSystemd(&templateArgs)
|
||||
return installSystemd(&templateArgs, logger)
|
||||
default:
|
||||
logger.Infof("Using Sysv")
|
||||
return installSysv(&templateArgs)
|
||||
return installSysv(&templateArgs, logger)
|
||||
}
|
||||
}
|
||||
|
||||
func installSystemd(templateArgs *ServiceTemplateArgs) error {
|
||||
func installSystemd(templateArgs *ServiceTemplateArgs, logger logger.Service) error {
|
||||
for _, serviceTemplate := range systemdTemplates {
|
||||
err := serviceTemplate.Generate(templateArgs)
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("error generating service template")
|
||||
logger.Errorf("error generating service template: %s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := runCommand("systemctl", "enable", "cloudflared.service"); err != nil {
|
||||
logger.WithError(err).Infof("systemctl enable cloudflared.service error")
|
||||
logger.Errorf("systemctl enable cloudflared.service error: %s", err)
|
||||
return err
|
||||
}
|
||||
if err := runCommand("systemctl", "start", "cloudflared-update.timer"); err != nil {
|
||||
logger.WithError(err).Infof("systemctl start cloudflared-update.timer error")
|
||||
logger.Errorf("systemctl start cloudflared-update.timer error: %s", err)
|
||||
return err
|
||||
}
|
||||
logger.Infof("systemctl daemon-reload")
|
||||
return runCommand("systemctl", "daemon-reload")
|
||||
}
|
||||
|
||||
func installSysv(templateArgs *ServiceTemplateArgs) error {
|
||||
func installSysv(templateArgs *ServiceTemplateArgs, logger logger.Service) error {
|
||||
confPath, err := sysvTemplate.ResolvePath()
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("error resolving system path")
|
||||
logger.Errorf("error resolving system path: %s", err)
|
||||
return err
|
||||
}
|
||||
if err := sysvTemplate.Generate(templateArgs); err != nil {
|
||||
logger.WithError(err).Infof("error generating system template")
|
||||
logger.Errorf("error generating system template: %s", err)
|
||||
return err
|
||||
}
|
||||
for _, i := range [...]string{"2", "3", "4", "5"} {
|
||||
@@ -243,28 +281,33 @@ func installSysv(templateArgs *ServiceTemplateArgs) error {
|
||||
}
|
||||
|
||||
func uninstallLinuxService(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
switch {
|
||||
case isSystemd():
|
||||
logger.Infof("Using Systemd")
|
||||
return uninstallSystemd()
|
||||
return uninstallSystemd(logger)
|
||||
default:
|
||||
logger.Infof("Using Sysv")
|
||||
return uninstallSysv()
|
||||
return uninstallSysv(logger)
|
||||
}
|
||||
}
|
||||
|
||||
func uninstallSystemd() error {
|
||||
func uninstallSystemd(logger logger.Service) error {
|
||||
if err := runCommand("systemctl", "disable", "cloudflared.service"); err != nil {
|
||||
logger.WithError(err).Infof("systemctl disable cloudflared.service error")
|
||||
logger.Errorf("systemctl disable cloudflared.service error: %s", err)
|
||||
return err
|
||||
}
|
||||
if err := runCommand("systemctl", "stop", "cloudflared-update.timer"); err != nil {
|
||||
logger.WithError(err).Infof("systemctl stop cloudflared-update.timer error")
|
||||
logger.Errorf("systemctl stop cloudflared-update.timer error: %s", err)
|
||||
return err
|
||||
}
|
||||
for _, serviceTemplate := range systemdTemplates {
|
||||
if err := serviceTemplate.Remove(); err != nil {
|
||||
logger.WithError(err).Infof("error removing service template")
|
||||
logger.Errorf("error removing service template: %s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -272,9 +315,9 @@ func uninstallSystemd() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func uninstallSysv() error {
|
||||
func uninstallSysv(logger logger.Service) error {
|
||||
if err := sysvTemplate.Remove(); err != nil {
|
||||
logger.WithError(err).Infof("error removing service template")
|
||||
logger.Errorf("error removing service template: %s", err)
|
||||
return err
|
||||
}
|
||||
for _, i := range [...]string{"2", "3", "4", "5"} {
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
|
||||
"github.com/rifflock/lfshook"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var logger = log.CreateLogger()
|
||||
|
||||
func configMainLogger(c *cli.Context) error {
|
||||
logLevel, err := logrus.ParseLevel(c.String("loglevel"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Unknown logging level specified")
|
||||
return errors.Wrap(err, "Unknown logging level specified")
|
||||
}
|
||||
logger.SetLevel(logLevel)
|
||||
return nil
|
||||
}
|
||||
|
||||
func configProtoLogger(c *cli.Context) (*logrus.Logger, error) {
|
||||
protoLogLevel, err := logrus.ParseLevel(c.String("proto-loglevel"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Fatal("Unknown protocol logging level specified")
|
||||
return nil, errors.Wrap(err, "Unknown protocol logging level specified")
|
||||
}
|
||||
protoLogger := logrus.New()
|
||||
protoLogger.Level = protoLogLevel
|
||||
return protoLogger, nil
|
||||
}
|
||||
|
||||
func initLogFile(c *cli.Context, loggers ...*logrus.Logger) error {
|
||||
filePath, err := homedir.Expand(c.String("logfile"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Cannot resolve logfile path")
|
||||
}
|
||||
|
||||
fileMode := os.O_WRONLY | os.O_APPEND | os.O_CREATE | os.O_TRUNC
|
||||
// do not truncate log file if the client has been autoupdated
|
||||
if c.Bool("is-autoupdated") {
|
||||
fileMode = os.O_WRONLY | os.O_APPEND | os.O_CREATE
|
||||
}
|
||||
f, err := os.OpenFile(filePath, fileMode, 0664)
|
||||
if err != nil {
|
||||
errors.Wrap(err, fmt.Sprintf("Cannot open file %s", filePath))
|
||||
}
|
||||
defer f.Close()
|
||||
pathMap := lfshook.PathMap{
|
||||
logrus.InfoLevel: filePath,
|
||||
logrus.ErrorLevel: filePath,
|
||||
logrus.FatalLevel: filePath,
|
||||
logrus.PanicLevel: filePath,
|
||||
}
|
||||
|
||||
for _, l := range loggers {
|
||||
l.Hooks.Add(lfshook.NewHook(pathMap, &logrus.JSONFormatter{}))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base32"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
const baseLoginURL = "https://dash.cloudflare.com/warp"
|
||||
const baseCertStoreURL = "https://login.cloudflarewarp.com"
|
||||
const clientTimeout = time.Minute * 20
|
||||
|
||||
func login(c *cli.Context) error {
|
||||
configPath, err := homedir.Expand(defaultConfigDirs[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ok, err := fileExists(configPath)
|
||||
if !ok && err == nil {
|
||||
// create config directory if doesn't already exist
|
||||
err = os.Mkdir(configPath, 0700)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := filepath.Join(configPath, defaultCredentialFile)
|
||||
fileInfo, err := os.Stat(path)
|
||||
if err == nil && fileInfo.Size() > 0 {
|
||||
fmt.Fprintf(os.Stderr, `You have an existing certificate at %s which login would overwrite.
|
||||
If this is intentional, please move or delete that file then run this command again.
|
||||
`, path)
|
||||
return nil
|
||||
}
|
||||
if err != nil && err.(*os.PathError).Err != syscall.ENOENT {
|
||||
return err
|
||||
}
|
||||
|
||||
// for local debugging
|
||||
baseURL := baseCertStoreURL
|
||||
if c.IsSet("url") {
|
||||
baseURL = c.String("url")
|
||||
}
|
||||
// Generate a random post URL
|
||||
certURL := baseURL + generateRandomPath()
|
||||
loginURL, err := url.Parse(baseLoginURL)
|
||||
if err != nil {
|
||||
// shouldn't happen, URL is hardcoded
|
||||
return err
|
||||
}
|
||||
loginURL.RawQuery = "callback=" + url.QueryEscape(certURL)
|
||||
|
||||
err = open(loginURL.String())
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, `Please open the following URL and log in with your Cloudflare account:
|
||||
|
||||
%s
|
||||
|
||||
Leave cloudflared running to install the certificate automatically.
|
||||
`, loginURL.String())
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, `A browser window should have opened at the following URL:
|
||||
|
||||
%s
|
||||
|
||||
If the browser failed to open, open it yourself and visit the URL above.
|
||||
|
||||
`, loginURL.String())
|
||||
}
|
||||
|
||||
if download(certURL, path) {
|
||||
fmt.Fprintf(os.Stderr, `You have successfully logged in.
|
||||
If you wish to copy your credentials to a server, they have been saved to:
|
||||
%s
|
||||
`, path)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, `Failed to write the certificate due to the following error:
|
||||
%v
|
||||
|
||||
Your browser will download the certificate instead. You will have to manually
|
||||
copy it to the following path:
|
||||
|
||||
%s
|
||||
|
||||
`, err, path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateRandomPath generates a random URL to associate with the certificate.
|
||||
func generateRandomPath() string {
|
||||
randomBytes := make([]byte, 40)
|
||||
_, err := rand.Read(randomBytes)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return "/" + base32.StdEncoding.EncodeToString(randomBytes)
|
||||
}
|
||||
|
||||
// open opens the specified URL in the default browser of the user.
|
||||
func open(url string) error {
|
||||
var cmd string
|
||||
var args []string
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
cmd = "cmd"
|
||||
args = []string{"/c", "start"}
|
||||
case "darwin":
|
||||
cmd = "open"
|
||||
default: // "linux", "freebsd", "openbsd", "netbsd"
|
||||
cmd = "xdg-open"
|
||||
}
|
||||
args = append(args, url)
|
||||
return exec.Command(cmd, args...).Start()
|
||||
}
|
||||
|
||||
func download(certURL, filePath string) bool {
|
||||
client := &http.Client{Timeout: clientTimeout}
|
||||
// attempt a (long-running) certificate get
|
||||
for i := 0; i < 20; i++ {
|
||||
ok, err := tryDownload(client, certURL, filePath)
|
||||
if ok {
|
||||
putSuccess(client, certURL)
|
||||
return true
|
||||
}
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error fetching certificate")
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func tryDownload(client *http.Client, certURL, filePath string) (ok bool, err error) {
|
||||
resp, err := client.Get(certURL)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == 404 {
|
||||
return false, nil
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
return false, fmt.Errorf("Unexpected HTTP error code %d", resp.StatusCode)
|
||||
}
|
||||
if resp.Header.Get("Content-Type") != "application/x-pem-file" {
|
||||
return false, fmt.Errorf("Unexpected content type %s", resp.Header.Get("Content-Type"))
|
||||
}
|
||||
// write response
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer file.Close()
|
||||
written, err := io.Copy(file, resp.Body)
|
||||
switch {
|
||||
case err != nil:
|
||||
return false, err
|
||||
case resp.ContentLength != written && resp.ContentLength != -1:
|
||||
return false, fmt.Errorf("Short read (%d bytes) from server while writing certificate", written)
|
||||
default:
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
func putSuccess(client *http.Client, certURL string) {
|
||||
// indicate success to the relay server
|
||||
req, err := http.NewRequest("PUT", certURL+"/ok", nil)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("HTTP request error")
|
||||
return
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("HTTP error")
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
logger.Errorf("Unexpected HTTP error code %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,10 @@ 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"
|
||||
)
|
||||
|
||||
@@ -23,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),
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -60,7 +62,7 @@ func newLaunchdTemplate(installPath, stdoutPath, stderrPath string) *ServiceTemp
|
||||
<false/>
|
||||
</dict>
|
||||
<key>ThrottleInterval</key>
|
||||
<integer>20</integer>
|
||||
<integer>5</integer>
|
||||
</dict>
|
||||
</plist>`, launchdIdentifier, stdoutPath, stderrPath),
|
||||
}
|
||||
@@ -105,6 +107,11 @@ func stderrPath() (string, error) {
|
||||
}
|
||||
|
||||
func installLaunchd(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
if isRootUser() {
|
||||
logger.Infof("Installing Argo Tunnel client as a system launch daemon. " +
|
||||
"Argo Tunnel client will run at boot")
|
||||
@@ -116,35 +123,38 @@ func installLaunchd(c *cli.Context) error {
|
||||
}
|
||||
etPath, err := os.Executable()
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Error determining executable path")
|
||||
logger.Errorf("Error determining executable path: %s", err)
|
||||
return fmt.Errorf("Error determining executable path: %v", err)
|
||||
}
|
||||
installPath, err := installPath()
|
||||
if err != nil {
|
||||
logger.Errorf("Error determining install path: %s", err)
|
||||
return errors.Wrap(err, "Error determining install path")
|
||||
}
|
||||
stdoutPath, err := stdoutPath()
|
||||
if err != nil {
|
||||
logger.Errorf("error determining stdout path: %s", err)
|
||||
return errors.Wrap(err, "error determining stdout path")
|
||||
}
|
||||
stderrPath, err := stderrPath()
|
||||
if err != nil {
|
||||
logger.Errorf("error determining stderr path: %s", err)
|
||||
return errors.Wrap(err, "error determining stderr path")
|
||||
}
|
||||
launchdTemplate := newLaunchdTemplate(installPath, stdoutPath, stderrPath)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("error creating launchd template")
|
||||
logger.Errorf("error creating launchd template: %s", err)
|
||||
return errors.Wrap(err, "error creating launchd template")
|
||||
}
|
||||
templateArgs := ServiceTemplateArgs{Path: etPath}
|
||||
err = launchdTemplate.Generate(&templateArgs)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("error generating launchd template")
|
||||
logger.Errorf("error generating launchd template: %s", err)
|
||||
return err
|
||||
}
|
||||
plistPath, err := launchdTemplate.ResolvePath()
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("error resolving launchd template path")
|
||||
logger.Errorf("error resolving launchd template path: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -153,6 +163,11 @@ func installLaunchd(c *cli.Context) error {
|
||||
}
|
||||
|
||||
func uninstallLaunchd(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
if isRootUser() {
|
||||
logger.Infof("Uninstalling Argo Tunnel as a system launch daemon")
|
||||
} else {
|
||||
@@ -176,12 +191,12 @@ func uninstallLaunchd(c *cli.Context) error {
|
||||
}
|
||||
plistPath, err := launchdTemplate.ResolvePath()
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("error resolving launchd template path")
|
||||
logger.Errorf("error resolving launchd template path: %s", err)
|
||||
return err
|
||||
}
|
||||
err = runCommand("launchctl", "unload", plistPath)
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("error unloading")
|
||||
logger.Errorf("error unloading: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
+147
-594
@@ -2,48 +2,48 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime/trace"
|
||||
"sync"
|
||||
"syscall"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/sqlgateway"
|
||||
"github.com/cloudflare/cloudflared/hello"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||
log "github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/metrics"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
"github.com/cloudflare/cloudflared/overwatch"
|
||||
"github.com/cloudflare/cloudflared/watcher"
|
||||
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
raven "github.com/getsentry/raven-go"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
cli "github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
"gopkg.in/urfave/cli.v2/altsrc"
|
||||
|
||||
"github.com/coreos/go-systemd/daemon"
|
||||
"github.com/facebookgo/grace/gracenet"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b:3e8827f6f9f740738eb11138f7bebb68@sentry.io/189878"
|
||||
developerPortal = "https://developers.cloudflare.com/argo-tunnel"
|
||||
quickStartUrl = developerPortal + "/quickstart/quickstart/"
|
||||
serviceUrl = developerPortal + "/reference/service/"
|
||||
argumentsUrl = developerPortal + "/reference/arguments/"
|
||||
licenseUrl = developerPortal + "/licence/"
|
||||
versionText = "Print the version"
|
||||
)
|
||||
|
||||
var (
|
||||
Version = "DEV"
|
||||
BuildTime = "unknown"
|
||||
// Mostly network errors that we don't want reported back to Sentry, this is done by substring match.
|
||||
ignoredErrors = []string{
|
||||
"connection reset by peer",
|
||||
"An existing connection was forcibly closed by the remote host.",
|
||||
"use of closed connection",
|
||||
"You need to enable Argo Smart Routing",
|
||||
"3001 connection closed",
|
||||
"3002 connection dropped",
|
||||
"rpc exception: dial tcp",
|
||||
"rpc exception: EOF",
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
metrics.RegisterBuildInfo(BuildTime, Version)
|
||||
raven.SetDSN(sentryDSN)
|
||||
raven.SetRelease(Version)
|
||||
|
||||
// Force shutdown channel used by the app. When closed, app must terminate.
|
||||
@@ -53,590 +53,92 @@ func main() {
|
||||
// Windows service manager closes this channel when it receives stop command.
|
||||
graceShutdownC := make(chan struct{})
|
||||
|
||||
cli.VersionFlag = &cli.BoolFlag{
|
||||
Name: "version",
|
||||
Aliases: []string{"v", "V"},
|
||||
Usage: versionText,
|
||||
}
|
||||
|
||||
app := &cli.App{}
|
||||
app.Name = "cloudflared"
|
||||
app.Copyright = fmt.Sprintf(`(c) %d Cloudflare Inc.
|
||||
Use is subject to the license agreement at %s`, time.Now().Year(), licenseUrl)
|
||||
app.Usage = "Cloudflare reverse tunnelling proxy agent"
|
||||
app.ArgsUsage = "origin-url"
|
||||
app.Usage = "Cloudflare's command-line tool and agent"
|
||||
app.UsageText = "cloudflared [global options] [command] [command options]"
|
||||
app.Copyright = fmt.Sprintf(
|
||||
`(c) %d Cloudflare Inc.
|
||||
Your installation of cloudflared software constitutes a symbol of your signature indicating that you accept
|
||||
the terms of the Cloudflare License (https://developers.cloudflare.com/argo-tunnel/license/),
|
||||
Terms (https://www.cloudflare.com/terms/) and Privacy Policy (https://www.cloudflare.com/privacypolicy/).`,
|
||||
time.Now().Year(),
|
||||
)
|
||||
app.Version = fmt.Sprintf("%s (built %s)", Version, BuildTime)
|
||||
app.Description = `A reverse tunnel proxy agent that connects to Cloudflare's infrastructure.
|
||||
Upon connecting, you are assigned a unique subdomain on cftunnel.com.
|
||||
You need to specify a hostname on a zone you control.
|
||||
A DNS record will be created to CNAME your hostname to the unique subdomain on cftunnel.com.
|
||||
app.Description = `cloudflared connects your machine or user identity to Cloudflare's global network.
|
||||
You can use it to authenticate a session to reach an API behind Access, route web traffic to this machine,
|
||||
and configure access control.`
|
||||
app.Flags = flags()
|
||||
app.Action = action(Version, shutdownC, graceShutdownC)
|
||||
app.Before = tunnel.Before
|
||||
app.Commands = commands(cli.ShowVersion)
|
||||
|
||||
Requests made to Cloudflare's servers for your hostname will be proxied
|
||||
through the tunnel to your local webserver.`
|
||||
app.Flags = []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "config",
|
||||
Usage: "Specifies a config file in YAML format.",
|
||||
Value: findDefaultConfigPath(),
|
||||
},
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "autoupdate-freq",
|
||||
Usage: "Autoupdate frequency. Default is 24h.",
|
||||
Value: time.Hour * 24,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "no-autoupdate",
|
||||
Usage: "Disable periodic check for updates, restarting the server with the new version.",
|
||||
Value: false,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "is-autoupdated",
|
||||
Usage: "Signal the new process that Argo Tunnel client has been autoupdated",
|
||||
Value: false,
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
|
||||
Name: "edge",
|
||||
Usage: "Address of the Cloudflare tunnel server.",
|
||||
EnvVars: []string{"TUNNEL_EDGE"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "cacert",
|
||||
Usage: "Certificate Authority authenticating the Cloudflare tunnel connection.",
|
||||
EnvVars: []string{"TUNNEL_CACERT"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "no-tls-verify",
|
||||
Usage: "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.",
|
||||
EnvVars: []string{"NO_TLS_VERIFY"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "origincert",
|
||||
Usage: "Path to the certificate generated for your origin when you run cloudflared login.",
|
||||
EnvVars: []string{"TUNNEL_ORIGIN_CERT"},
|
||||
Value: findDefaultOriginCertPath(),
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "origin-ca-pool",
|
||||
Usage: "Path to the CA for the certificate of your origin. This option should be used only if your certificate is not signed by Cloudflare.",
|
||||
EnvVars: []string{"TUNNEL_ORIGIN_CA_POOL"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "url",
|
||||
Value: "https://localhost:8080",
|
||||
Usage: "Connect to the local webserver at `URL`.",
|
||||
EnvVars: []string{"TUNNEL_URL"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "hostname",
|
||||
Usage: "Set a hostname on a Cloudflare zone to route traffic through this tunnel.",
|
||||
EnvVars: []string{"TUNNEL_HOSTNAME"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "origin-server-name",
|
||||
Usage: "Hostname on the origin server certificate.",
|
||||
EnvVars: []string{"TUNNEL_ORIGIN_SERVER_NAME"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "id",
|
||||
Usage: "A unique identifier used to tie connections to this tunnel instance.",
|
||||
EnvVars: []string{"TUNNEL_ID"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "lb-pool",
|
||||
Usage: "The name of a (new/existing) load balancing pool to add this origin to.",
|
||||
EnvVars: []string{"TUNNEL_LB_POOL"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "api-key",
|
||||
Usage: "This parameter has been deprecated since version 2017.10.1.",
|
||||
EnvVars: []string{"TUNNEL_API_KEY"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "api-email",
|
||||
Usage: "This parameter has been deprecated since version 2017.10.1.",
|
||||
EnvVars: []string{"TUNNEL_API_EMAIL"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "api-ca-key",
|
||||
Usage: "This parameter has been deprecated since version 2017.10.1.",
|
||||
EnvVars: []string{"TUNNEL_API_CA_KEY"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "metrics",
|
||||
Value: "localhost:",
|
||||
Usage: "Listen address for metrics reporting.",
|
||||
EnvVars: []string{"TUNNEL_METRICS"},
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "metrics-update-freq",
|
||||
Usage: "Frequency to update tunnel metrics",
|
||||
Value: time.Second * 5,
|
||||
EnvVars: []string{"TUNNEL_METRICS_UPDATE_FREQ"},
|
||||
}),
|
||||
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
|
||||
Name: "tag",
|
||||
Usage: "Custom tags used to identify this tunnel, in format `KEY=VALUE`. Multiple tags may be specified",
|
||||
EnvVars: []string{"TUNNEL_TAG"},
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "heartbeat-interval",
|
||||
Usage: "Minimum idle time before sending a heartbeat.",
|
||||
Value: time.Second * 5,
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewUint64Flag(&cli.Uint64Flag{
|
||||
Name: "heartbeat-count",
|
||||
Usage: "Minimum number of unacked heartbeats to send before closing the connection.",
|
||||
Value: 5,
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "loglevel",
|
||||
Value: "info",
|
||||
Usage: "Application logging level {panic, fatal, error, warn, info, debug}",
|
||||
EnvVars: []string{"TUNNEL_LOGLEVEL"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "proto-loglevel",
|
||||
Value: "warn",
|
||||
Usage: "Protocol logging level {panic, fatal, error, warn, info, debug}",
|
||||
EnvVars: []string{"TUNNEL_PROTO_LOGLEVEL"},
|
||||
}),
|
||||
altsrc.NewUintFlag(&cli.UintFlag{
|
||||
Name: "retries",
|
||||
Value: 5,
|
||||
Usage: "Maximum number of retries for connection/protocol errors.",
|
||||
EnvVars: []string{"TUNNEL_RETRIES"},
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "hello-world",
|
||||
Value: false,
|
||||
Usage: "Run Hello World Server",
|
||||
EnvVars: []string{"TUNNEL_HELLO_WORLD"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "pidfile",
|
||||
Usage: "Write the application's PID to this file after first successful connection.",
|
||||
EnvVars: []string{"TUNNEL_PIDFILE"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "logfile",
|
||||
Usage: "Save application log to this file for reporting issues.",
|
||||
EnvVars: []string{"TUNNEL_LOGFILE"},
|
||||
}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{
|
||||
Name: "ha-connections",
|
||||
Value: 4,
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "proxy-connect-timeout",
|
||||
Usage: "HTTP proxy timeout for establishing a new connection",
|
||||
Value: time.Second * 30,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "proxy-tls-timeout",
|
||||
Usage: "HTTP proxy timeout for completing a TLS handshake",
|
||||
Value: time.Second * 10,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "proxy-tcp-keepalive",
|
||||
Usage: "HTTP proxy TCP keepalive duration",
|
||||
Value: time.Second * 30,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "proxy-no-happy-eyeballs",
|
||||
Usage: "HTTP proxy should disable \"happy eyeballs\" for IPv4/v6 fallback",
|
||||
}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{
|
||||
Name: "proxy-keepalive-connections",
|
||||
Usage: "HTTP proxy maximum keepalive connection pool size",
|
||||
Value: 100,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "proxy-keepalive-timeout",
|
||||
Usage: "HTTP proxy timeout for closing an idle connection",
|
||||
Value: time.Second * 90,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "proxy-dns",
|
||||
Usage: "Run a DNS over HTTPS proxy server.",
|
||||
EnvVars: []string{"TUNNEL_DNS"},
|
||||
}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{
|
||||
Name: "proxy-dns-port",
|
||||
Value: 53,
|
||||
Usage: "Listen on given port for the DNS over HTTPS proxy server.",
|
||||
EnvVars: []string{"TUNNEL_DNS_PORT"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "proxy-dns-address",
|
||||
Usage: "Listen address for the DNS over HTTPS proxy server.",
|
||||
Value: "localhost",
|
||||
EnvVars: []string{"TUNNEL_DNS_ADDRESS"},
|
||||
}),
|
||||
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
|
||||
Name: "proxy-dns-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"},
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "grace-period",
|
||||
Usage: "Duration to accept new requests after cloudflared receives first SIGINT/SIGTERM. A second SIGINT/SIGTERM will force cloudflared to shutdown immediately.",
|
||||
Value: time.Second * 30,
|
||||
EnvVars: []string{"TUNNEL_GRACE_PERIOD"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewUintFlag(&cli.UintFlag{
|
||||
Name: "compression-quality",
|
||||
Value: 0,
|
||||
Usage: "Use cross-stream compression instead HTTP compression. 0-off, 1-low, 2-medium, >=3-high",
|
||||
EnvVars: []string{"TUNNEL_COMPRESSION_LEVEL"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "no-chunked-encoding",
|
||||
Usage: "Disables chunked transfer encoding; useful if you are running a WSGI server.",
|
||||
EnvVars: []string{"TUNNEL_NO_CHUNKED_ENCODING"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "trace-output",
|
||||
Usage: "Name of trace output file, generated when cloudflared stops.",
|
||||
EnvVars: []string{"TUNNEL_TRACE_OUTPUT"},
|
||||
}),
|
||||
}
|
||||
app.Action = func(c *cli.Context) (err error) {
|
||||
tags := make(map[string]string)
|
||||
tags["hostname"] = c.String("hostname")
|
||||
raven.SetTagsContext(tags)
|
||||
raven.CapturePanic(func() { err = startServer(c, shutdownC, graceShutdownC) }, nil)
|
||||
if err != nil {
|
||||
raven.CaptureError(err, nil)
|
||||
}
|
||||
return err
|
||||
}
|
||||
app.Before = func(context *cli.Context) error {
|
||||
if context.String("config") == "" {
|
||||
logger.Warnf("Cannot determine default configuration path. No file %v in %v", defaultConfigFiles, defaultConfigDirs)
|
||||
}
|
||||
inputSource, err := findInputSourceContext(context)
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("Cannot load configuration from %s", context.String("config"))
|
||||
return err
|
||||
} else if inputSource != nil {
|
||||
err := altsrc.ApplyInputSourceValues(context, inputSource, app.Flags)
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("Cannot apply configuration from %s", context.String("config"))
|
||||
return err
|
||||
}
|
||||
logger.Infof("Applied configuration from %s", context.String("config"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
app.Commands = []*cli.Command{
|
||||
{
|
||||
Name: "update",
|
||||
Action: update,
|
||||
Usage: "Update the agent if a new version exists",
|
||||
ArgsUsage: " ",
|
||||
Description: `Looks for a new version on the offical download server.
|
||||
If a new version exists, updates the agent binary and quits.
|
||||
Otherwise, does nothing.
|
||||
|
||||
To determine if an update happened in a script, check for error code 64.`,
|
||||
},
|
||||
{
|
||||
Name: "login",
|
||||
Action: login,
|
||||
Usage: "Generate a configuration file with your login details",
|
||||
ArgsUsage: " ",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "url",
|
||||
Hidden: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "hello",
|
||||
Action: helloWorld,
|
||||
Usage: "Run a simple \"Hello World\" server for testing Argo Tunnel.",
|
||||
Flags: []cli.Flag{
|
||||
&cli.IntFlag{
|
||||
Name: "port",
|
||||
Usage: "Listen on the selected port.",
|
||||
Value: 8080,
|
||||
},
|
||||
},
|
||||
ArgsUsage: " ", // can't be the empty string or we get the default output
|
||||
},
|
||||
{
|
||||
Name: "proxy-dns",
|
||||
Action: tunneldns.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"},
|
||||
},
|
||||
},
|
||||
ArgsUsage: " ", // can't be the empty string or we get the default output
|
||||
},
|
||||
{
|
||||
Name: "db",
|
||||
Action: func(c *cli.Context) error {
|
||||
tags := make(map[string]string)
|
||||
tags["hostname"] = c.String("hostname")
|
||||
raven.SetTagsContext(tags)
|
||||
|
||||
fmt.Printf("\nSQL Database Password: ")
|
||||
pass, err := terminal.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
|
||||
go sqlgateway.StartProxy(c, logger, string(pass))
|
||||
|
||||
raven.CapturePanic(func() { err = startServer(c, shutdownC, graceShutdownC) }, nil)
|
||||
if err != nil {
|
||||
raven.CaptureError(err, nil)
|
||||
}
|
||||
return err
|
||||
},
|
||||
Before: func(c *cli.Context) error {
|
||||
if c.String("config") == "" {
|
||||
logger.Warnf("Cannot determine default configuration path. No file %v in %v", defaultConfigFiles, defaultConfigDirs)
|
||||
}
|
||||
inputSource, err := findInputSourceContext(c)
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("Cannot load configuration from %s", c.String("config"))
|
||||
return err
|
||||
} else if inputSource != nil {
|
||||
err := altsrc.ApplyInputSourceValues(c, inputSource, app.Flags)
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("Cannot apply configuration from %s", c.String("config"))
|
||||
return err
|
||||
}
|
||||
logger.Infof("Applied configuration from %s", c.String("config"))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Usage: "SQL Gateway is an SQL over HTTP reverse proxy",
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "db",
|
||||
Value: true,
|
||||
Usage: "Enable the SQL Gateway Proxy",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "address",
|
||||
Value: "",
|
||||
Usage: "Database connection string: db://user:pass",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
tunnel.Init(Version, shutdownC, graceShutdownC) // we need this to support the tunnel sub command...
|
||||
access.Init(shutdownC, graceShutdownC)
|
||||
runApp(app, shutdownC, graceShutdownC)
|
||||
}
|
||||
|
||||
func startServer(c *cli.Context, shutdownC, graceShutdownC chan struct{}) error {
|
||||
var wg sync.WaitGroup
|
||||
listeners := gracenet.Net{}
|
||||
errC := make(chan error)
|
||||
connectedSignal := make(chan struct{})
|
||||
dnsReadySignal := make(chan struct{})
|
||||
func commands(version func(c *cli.Context)) []*cli.Command {
|
||||
cmds := []*cli.Command{
|
||||
{
|
||||
Name: "update",
|
||||
Action: cliutil.ErrorHandler(updater.Update),
|
||||
Usage: "Update the agent if a new version exists",
|
||||
ArgsUsage: " ",
|
||||
Description: `Looks for a new version on the official download server.
|
||||
If a new version exists, updates the agent binary and quits.
|
||||
Otherwise, does nothing.
|
||||
|
||||
// check whether client provides enough flags or env variables. If not, print help.
|
||||
if ok := enoughOptionsSet(c); !ok {
|
||||
return nil
|
||||
To determine if an update happened in a script, check for error code 64.`,
|
||||
},
|
||||
{
|
||||
Name: "version",
|
||||
Action: func(c *cli.Context) (err error) {
|
||||
version(c)
|
||||
return nil
|
||||
},
|
||||
Usage: versionText,
|
||||
Description: versionText,
|
||||
},
|
||||
}
|
||||
cmds = append(cmds, tunnel.Commands()...)
|
||||
cmds = append(cmds, access.Commands()...)
|
||||
return cmds
|
||||
}
|
||||
|
||||
if err := configMainLogger(c); err != nil {
|
||||
return errors.Wrap(err, "Error configuring logger")
|
||||
}
|
||||
func flags() []cli.Flag {
|
||||
flags := tunnel.Flags()
|
||||
return append(flags, access.Flags()...)
|
||||
}
|
||||
|
||||
protoLogger, err := configProtoLogger(c)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error configuring protocol logger")
|
||||
}
|
||||
func isEmptyInvocation(c *cli.Context) bool {
|
||||
return c.NArg() == 0 && c.NumFlags() == 0
|
||||
}
|
||||
|
||||
if c.IsSet("trace-output") {
|
||||
tmpTraceFile, err := ioutil.TempFile("", "trace")
|
||||
func action(version string, shutdownC, graceShutdownC chan struct{}) cli.ActionFunc {
|
||||
return 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.TunnelCommand(c) }, nil)
|
||||
exitCode := 0
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to create new temporary file to save trace output")
|
||||
handleError(err)
|
||||
exitCode = 1
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := tmpTraceFile.Close(); err != nil {
|
||||
logger.WithError(err).Error("Failed to close trace output file %s", tmpTraceFile.Name())
|
||||
}
|
||||
if err := os.Rename(tmpTraceFile.Name(), c.String("trace-output")); err != nil {
|
||||
logger.WithError(err).Errorf("Failed to rename temporary trace output file %s to %s", tmpTraceFile.Name(), c.String("trace-output"))
|
||||
} else {
|
||||
os.Remove(tmpTraceFile.Name())
|
||||
}
|
||||
}()
|
||||
|
||||
if err := trace.Start(tmpTraceFile); err != nil {
|
||||
logger.WithError(err).Error("Failed to start trace")
|
||||
return errors.Wrap(err, "Error starting tracing")
|
||||
}
|
||||
defer trace.Stop()
|
||||
// we already handle error printing, so we pass an empty string so we
|
||||
// don't have to print again.
|
||||
return cli.Exit("", exitCode)
|
||||
}
|
||||
|
||||
if c.String("logfile") != "" {
|
||||
if err := initLogFile(c, logger, protoLogger); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := handleDeprecatedOptions(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buildInfo := origin.GetBuildInfo()
|
||||
logger.Infof("Build info: %+v", *buildInfo)
|
||||
logger.Infof("Version %s", Version)
|
||||
logClientOptions(c)
|
||||
|
||||
if c.IsSet("proxy-dns") {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errC <- runDNSProxyServer(c, dnsReadySignal, shutdownC)
|
||||
}()
|
||||
} else {
|
||||
close(dnsReadySignal)
|
||||
}
|
||||
|
||||
// Wait for proxy-dns to come up (if used)
|
||||
<-dnsReadySignal
|
||||
|
||||
// update needs to be after DNS proxy is up to resolve equinox server address
|
||||
if isAutoupdateEnabled(c) {
|
||||
logger.Infof("Autoupdate frequency is set to %v", c.Duration("autoupdate-freq"))
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errC <- autoupdate(c.Duration("autoupdate-freq"), &listeners, shutdownC)
|
||||
}()
|
||||
}
|
||||
|
||||
metricsListener, err := listeners.Listen("tcp", c.String("metrics"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error opening metrics server listener")
|
||||
return errors.Wrap(err, "Error opening metrics server listener")
|
||||
}
|
||||
defer metricsListener.Close()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errC <- metrics.ServeMetrics(metricsListener, shutdownC, logger)
|
||||
}()
|
||||
|
||||
go notifySystemd(connectedSignal)
|
||||
if c.IsSet("pidfile") {
|
||||
go writePidFile(connectedSignal, c.String("pidfile"))
|
||||
}
|
||||
|
||||
// Serve DNS proxy stand-alone if no hostname or tag or app is going to run
|
||||
if dnsProxyStandAlone(c) {
|
||||
close(connectedSignal)
|
||||
// no grace period, handle SIGINT/SIGTERM immediately
|
||||
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, 0)
|
||||
}
|
||||
|
||||
if c.IsSet("hello-world") {
|
||||
helloListener, err := hello.CreateTLSListener("127.0.0.1:")
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Cannot start Hello World Server")
|
||||
return errors.Wrap(err, "Cannot start Hello World Server")
|
||||
}
|
||||
defer helloListener.Close()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
hello.StartHelloWorldServer(logger, helloListener, shutdownC)
|
||||
}()
|
||||
c.Set("url", "https://"+helloListener.Addr().String())
|
||||
}
|
||||
|
||||
tunnelConfig, err := prepareTunnelConfig(c, buildInfo, logger, protoLogger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errC <- origin.StartTunnelDaemon(tunnelConfig, graceShutdownC, connectedSignal)
|
||||
}()
|
||||
|
||||
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, c.Duration("grace-period"))
|
||||
}
|
||||
|
||||
func waitToShutdown(wg *sync.WaitGroup,
|
||||
errC chan error,
|
||||
shutdownC, graceShutdownC chan struct{},
|
||||
gracePeriod time.Duration,
|
||||
) error {
|
||||
var err error
|
||||
if gracePeriod > 0 {
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceShutdownC, gracePeriod)
|
||||
} else {
|
||||
err = waitForSignal(errC, shutdownC)
|
||||
close(graceShutdownC)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Quitting due to error")
|
||||
} else {
|
||||
logger.Info("Quitting...")
|
||||
}
|
||||
// Wait for clean exit, discarding all errors
|
||||
go func() {
|
||||
for range errC {
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
return err
|
||||
}
|
||||
|
||||
func notifySystemd(waitForSignal chan struct{}) {
|
||||
<-waitForSignal
|
||||
daemon.SdNotify(false, "READY=1")
|
||||
}
|
||||
|
||||
func writePidFile(waitForSignal chan struct{}, pidFile string) {
|
||||
<-waitForSignal
|
||||
file, err := os.Create(pidFile)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Unable to write pid to %s", pidFile)
|
||||
}
|
||||
defer file.Close()
|
||||
fmt.Fprintf(file, "%d", os.Getpid())
|
||||
}
|
||||
|
||||
func userHomeDir() (string, error) {
|
||||
@@ -646,8 +148,59 @@ func userHomeDir() (string, error) {
|
||||
// use with sudo.
|
||||
homeDir, err := homedir.Dir()
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Cannot determine home directory for the user")
|
||||
return "", errors.Wrap(err, "Cannot determine home directory for the user")
|
||||
}
|
||||
return homeDir, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
errorMessage := err.Error()
|
||||
for _, ignoredErrorMessage := range ignoredErrors {
|
||||
if strings.Contains(errorMessage, ignoredErrorMessage) {
|
||||
return
|
||||
}
|
||||
}
|
||||
raven.CaptureError(err, nil)
|
||||
}
|
||||
|
||||
// cloudflared was started without any flags
|
||||
func handleServiceMode(shutdownC chan struct{}) error {
|
||||
defer log.SharedWriteManager.Shutdown()
|
||||
logDirectory, logLevel := config.FindLogSettings()
|
||||
|
||||
logger, err := log.New(log.DefaultFile(logDirectory), log.LogLevelString(logLevel))
|
||||
if err != nil {
|
||||
return cliutil.PrintLoggerSetupError("error setting up logger", err)
|
||||
}
|
||||
logger.Infof("logging to directory: %s", logDirectory)
|
||||
|
||||
// start the main run loop that reads from the config file
|
||||
f, err := watcher.NewFile()
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot load config file: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
configPath := config.FindOrCreateConfigPath()
|
||||
configManager, err := config.NewFileManager(f, configPath, logger)
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot setup config file for monitoring: %s", err)
|
||||
return err
|
||||
}
|
||||
logger.Infof("monitoring config file at: %s", configPath)
|
||||
|
||||
serviceCallback := func(t string, name string, err error) {
|
||||
if err != nil {
|
||||
logger.Errorf("%s service: %s encountered an error: %s", t, name, err)
|
||||
}
|
||||
}
|
||||
serviceManager := overwatch.NewAppManager(serviceCallback)
|
||||
|
||||
appService := NewAppService(configManager, serviceManager, shutdownC, logger)
|
||||
if err := appService.Run(); err != nil {
|
||||
logger.Errorf("Failed to start app service: %s", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package path
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
// GenerateFilePathFromURL will return a filepath for given access application url
|
||||
func GenerateFilePathFromURL(url *url.URL, suffix string) (string, error) {
|
||||
configPath, err := homedir.Expand(config.DefaultConfigSearchDirectories()[0])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ok, err := config.FileExists(configPath)
|
||||
if !ok && err == nil {
|
||||
// create config directory if doesn't already exist
|
||||
err = os.Mkdir(configPath, 0700)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
name := strings.Replace(fmt.Sprintf("%s%s-%s", url.Hostname(), url.EscapedPath(), suffix), "/", "-", -1)
|
||||
return filepath.Join(configPath, name), nil
|
||||
}
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
@@ -73,30 +73,25 @@ func runCommand(command string, args ...string) error {
|
||||
cmd := exec.Command(command, args...)
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("error getting stderr pipe")
|
||||
return fmt.Errorf("error getting stderr pipe: %v", err)
|
||||
}
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("error starting %s", command)
|
||||
return fmt.Errorf("error starting %s: %v", command, err)
|
||||
}
|
||||
commandErr, _ := ioutil.ReadAll(stderr)
|
||||
if len(commandErr) > 0 {
|
||||
logger.Errorf("%s: %s", command, commandErr)
|
||||
}
|
||||
|
||||
_, _ = ioutil.ReadAll(stderr)
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("%s returned error", command)
|
||||
return fmt.Errorf("%s returned with error: %v", command, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureConfigDirExists(configDir string) error {
|
||||
ok, err := fileExists(configDir)
|
||||
ok, err := config.FileExists(configDir)
|
||||
if !ok && err == nil {
|
||||
err = os.Mkdir(configDir, 0700)
|
||||
err = os.Mkdir(configDir, 0755)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -119,8 +114,7 @@ func openFile(path string, create bool) (file *os.File, exists bool, err error)
|
||||
return file, false, err
|
||||
}
|
||||
|
||||
func copyCertificate(srcConfigDir, destConfigDir, credentialFile string) error {
|
||||
destCredentialPath := filepath.Join(destConfigDir, credentialFile)
|
||||
func copyCredential(srcCredentialPath, destCredentialPath string) error {
|
||||
destFile, exists, err := openFile(destCredentialPath, true)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -130,7 +124,6 @@ func copyCertificate(srcConfigDir, destConfigDir, credentialFile string) error {
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
srcCredentialPath := filepath.Join(srcConfigDir, credentialFile)
|
||||
srcFile, _, err := openFile(srcCredentialPath, false)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -146,28 +139,17 @@ func copyCertificate(srcConfigDir, destConfigDir, credentialFile string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyCredentials(serviceConfigDir, defaultConfigDir, defaultConfigFile, defaultCredentialFile string) error {
|
||||
if err := ensureConfigDirExists(serviceConfigDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := copyCertificate(defaultConfigDir, serviceConfigDir, defaultCredentialFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func copyConfig(srcConfigPath, destConfigPath string) error {
|
||||
// Copy or create config
|
||||
destConfigPath := filepath.Join(serviceConfigDir, defaultConfigFile)
|
||||
destFile, exists, err := openFile(destConfigPath, true)
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("cannot open %s", destConfigPath)
|
||||
return err
|
||||
return fmt.Errorf("cannot open %s with error: %s", destConfigPath, err)
|
||||
} else if exists {
|
||||
// config already exists, do nothing
|
||||
return nil
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
srcConfigPath := filepath.Join(defaultConfigDir, defaultConfigFile)
|
||||
srcFile, _, err := openFile(srcConfigPath, false)
|
||||
if err != nil {
|
||||
fmt.Println("Your service needs a config file that at least specifies the hostname option.")
|
||||
@@ -185,7 +167,6 @@ func copyCredentials(serviceConfigDir, defaultConfigDir, defaultConfigFile, defa
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to copy %s to %s: %v", srcConfigPath, destConfigPath, err)
|
||||
}
|
||||
logger.Infof("Copied %s to %s", srcConfigPath, destConfigPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
//+build darwin
|
||||
|
||||
package shell
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func getBrowserCmd(url string) *exec.Cmd {
|
||||
return exec.Command("open", url)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//+build !windows,!darwin,!linux,!netbsd,!freebsd,!openbsd
|
||||
|
||||
package shell
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func getBrowserCmd(url string) *exec.Cmd {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//+build linux freebsd openbsd netbsd
|
||||
|
||||
package shell
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func getBrowserCmd(url string) *exec.Cmd {
|
||||
return exec.Command("xdg-open", url)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//+build windows
|
||||
|
||||
package shell
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func getBrowserCmd(url string) *exec.Cmd {
|
||||
cmd := exec.Command("cmd")
|
||||
// CmdLine is only defined when compiling for windows.
|
||||
// Empty string is the cmd proc "Title". Needs to be included because the start command will interpret the first
|
||||
// quoted string as that field and we want to quote the URL.
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{CmdLine: fmt.Sprintf(`/c start "" "%s"`, url)}
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// OpenBrowser opens the specified URL in the default browser of the user
|
||||
func OpenBrowser(url string) error {
|
||||
return getBrowserCmd(url).Start()
|
||||
}
|
||||
|
||||
// Run will kick off a shell task and pipe the results to the respective std pipes
|
||||
func Run(cmd string, args ...string) error {
|
||||
c := exec.Command(cmd, args...)
|
||||
stderr, err := c.StderrPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
io.Copy(os.Stderr, stderr)
|
||||
}()
|
||||
|
||||
stdout, err := c.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
io.Copy(os.Stdout, stdout)
|
||||
}()
|
||||
return c.Run()
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/path"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/transfer"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
)
|
||||
|
||||
const (
|
||||
keyName = "token"
|
||||
)
|
||||
|
||||
type lock struct {
|
||||
lockFilePath string
|
||||
backoff *origin.BackoffHandler
|
||||
sigHandler *signalHandler
|
||||
}
|
||||
|
||||
type signalHandler struct {
|
||||
sigChannel chan os.Signal
|
||||
signals []os.Signal
|
||||
}
|
||||
|
||||
type jwtPayload struct {
|
||||
Aud []string `json:"aud"`
|
||||
Email string `json:"email"`
|
||||
Exp int `json:"exp"`
|
||||
Iat int `json:"iat"`
|
||||
Nbf int `json:"nbf"`
|
||||
Iss string `json:"iss"`
|
||||
Type string `json:"type"`
|
||||
Subt string `json:"sub"`
|
||||
}
|
||||
|
||||
func (p jwtPayload) isExpired() bool {
|
||||
return int(time.Now().Unix()) > p.Exp
|
||||
}
|
||||
|
||||
func (s *signalHandler) register(handler func()) {
|
||||
s.sigChannel = make(chan os.Signal, 1)
|
||||
signal.Notify(s.sigChannel, s.signals...)
|
||||
go func(s *signalHandler) {
|
||||
for range s.sigChannel {
|
||||
handler()
|
||||
}
|
||||
}(s)
|
||||
}
|
||||
|
||||
func (s *signalHandler) deregister() {
|
||||
signal.Stop(s.sigChannel)
|
||||
close(s.sigChannel)
|
||||
}
|
||||
|
||||
func errDeleteTokenFailed(lockFilePath string) error {
|
||||
return fmt.Errorf("failed to acquire a new Access token. Please try to delete %s", lockFilePath)
|
||||
}
|
||||
|
||||
// newLock will get a new file lock
|
||||
func newLock(path string) *lock {
|
||||
lockPath := path + ".lock"
|
||||
return &lock{
|
||||
lockFilePath: lockPath,
|
||||
backoff: &origin.BackoffHandler{MaxRetries: 7},
|
||||
sigHandler: &signalHandler{
|
||||
signals: []os.Signal{syscall.SIGINT, syscall.SIGTERM},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lock) Acquire() error {
|
||||
// Intercept SIGINT and SIGTERM to release lock before exiting
|
||||
l.sigHandler.register(func() {
|
||||
l.deleteLockFile()
|
||||
os.Exit(0)
|
||||
})
|
||||
|
||||
// Check for a path.lock file
|
||||
// if the lock file exists; start polling
|
||||
// if not, create the lock file and go through the normal flow.
|
||||
// See AUTH-1736 for the reason why we do all this
|
||||
for isTokenLocked(l.lockFilePath) {
|
||||
if l.backoff.Backoff(context.Background()) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := l.deleteLockFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Create a lock file so other processes won't also try to get the token at
|
||||
// the same time
|
||||
if err := ioutil.WriteFile(l.lockFilePath, []byte{}, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *lock) deleteLockFile() error {
|
||||
if err := os.Remove(l.lockFilePath); err != nil && !os.IsNotExist(err) {
|
||||
return errDeleteTokenFailed(l.lockFilePath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *lock) Release() error {
|
||||
defer l.sigHandler.deregister()
|
||||
return l.deleteLockFile()
|
||||
}
|
||||
|
||||
// isTokenLocked checks to see if there is another process attempting to get the token already
|
||||
func isTokenLocked(lockFilePath string) bool {
|
||||
exists, err := config.FileExists(lockFilePath)
|
||||
return exists && err == nil
|
||||
}
|
||||
|
||||
// FetchTokenWithRedirect will either load a stored token or generate a new one
|
||||
// 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, 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, useHostOnly bool, logger logger.Service) (string, error) {
|
||||
if token, err := GetTokenIfExists(appURL); token != "" && err == nil {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
path, err := path.GenerateFilePathFromURL(appURL, keyName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fileLock := newLock(path)
|
||||
|
||||
err = fileLock.Acquire()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer fileLock.Release()
|
||||
|
||||
// check to see if another process has gotten a token while we waited for the lock
|
||||
if token, err := GetTokenIfExists(appURL); token != "" && err == nil {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// this weird parameter is the resource name (token) and the key/value
|
||||
// we want to send to the transfer service. the key is token and the value
|
||||
// is blank (basically just the id generated in the transfer service)
|
||||
token, err := transfer.Run(appURL, keyName, keyName, "", path, true, useHostOnly, logger)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(token), nil
|
||||
}
|
||||
|
||||
// GetTokenIfExists will return the token from local storage if it exists and not expired
|
||||
func GetTokenIfExists(url *url.URL) (string, error) {
|
||||
path, err := path.GenerateFilePathFromURL(url, keyName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
token, err := jose.ParseJWT(string(content))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var payload jwtPayload
|
||||
err = json.Unmarshal(token.Payload, &payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if payload.isExpired() {
|
||||
err := os.Remove(path)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token.Encode(), nil
|
||||
}
|
||||
|
||||
// RemoveTokenIfExists removes the a token from local storage if it exists
|
||||
func RemoveTokenIfExists(url *url.URL) error {
|
||||
path, err := path.GenerateFilePathFromURL(url, keyName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSignalHandler(t *testing.T) {
|
||||
sigHandler := signalHandler{signals: []os.Signal{syscall.SIGUSR1}}
|
||||
handlerRan := false
|
||||
done := make(chan struct{})
|
||||
timer := time.NewTimer(time.Second)
|
||||
sigHandler.register(func(){
|
||||
handlerRan = true
|
||||
done <- struct{}{}
|
||||
})
|
||||
|
||||
p, err := os.FindProcess(os.Getpid())
|
||||
require.Nil(t, err)
|
||||
p.Signal(syscall.SIGUSR1)
|
||||
|
||||
// Blocks for up to one second to make sure the handler callback runs before the assert.
|
||||
select {
|
||||
case <- done:
|
||||
assert.True(t, handlerRan)
|
||||
case <- timer.C:
|
||||
t.Fail()
|
||||
}
|
||||
sigHandler.deregister()
|
||||
}
|
||||
|
||||
func TestSignalHandlerClose(t *testing.T) {
|
||||
sigHandler := signalHandler{signals: []os.Signal{syscall.SIGUSR1}}
|
||||
done := make(chan struct{})
|
||||
timer := time.NewTimer(time.Second)
|
||||
sigHandler.register(func(){done <- struct{}{}})
|
||||
sigHandler.deregister()
|
||||
|
||||
p, err := os.FindProcess(os.Getpid())
|
||||
require.Nil(t, err)
|
||||
p.Signal(syscall.SIGUSR1)
|
||||
select {
|
||||
case <- done:
|
||||
t.Fail()
|
||||
case <- timer.C:
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package transfer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/encrypter"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
baseStoreURL = "https://login.argotunnel.com/"
|
||||
clientTimeout = time.Second * 60
|
||||
)
|
||||
|
||||
// Run does the transfer "dance" with the end result downloading the supported resource.
|
||||
// The expanded description is run is encapsulation of shared business logic needed
|
||||
// to request a resource (token/cert/etc) from the transfer service (loginhelper).
|
||||
// The "dance" we refer to is building a HTTP request, opening that in a browser waiting for
|
||||
// the user to complete an action, while it long polls in the background waiting for an
|
||||
// action to be completed to download the resource.
|
||||
func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncrypt bool, 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, useHostOnly)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// See AUTH-1423 for why we use stderr (the way git wraps ssh)
|
||||
err = shell.OpenBrowser(requestURL)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Please open the following URL and log in with your Cloudflare account:\n\n%s\n\nLeave cloudflared running to download the %s automatically.\n", requestURL, resourceName)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "A browser window should have opened at the following URL:\n\n%s\n\nIf the browser failed to open, please visit the URL above directly in your browser.\n", requestURL)
|
||||
}
|
||||
|
||||
var resourceData []byte
|
||||
|
||||
if shouldEncrypt {
|
||||
buf, key, err := transferRequest(baseStoreURL+"transfer/"+encrypterClient.PublicKey(), logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decodedBuf, err := base64.StdEncoding.DecodeString(string(buf))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decrypted, err := encrypterClient.Decrypt(decodedBuf, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resourceData = decrypted
|
||||
} else {
|
||||
buf, _, err := transferRequest(baseStoreURL+encrypterClient.PublicKey(), logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resourceData = buf
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(path, resourceData, 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resourceData, nil
|
||||
}
|
||||
|
||||
// BuildRequestURL creates a request suitable for a resource transfer.
|
||||
// it will return a constructed url based off the base url and query key/value provided.
|
||||
// cli will build a url for cli transfer request.
|
||||
func buildRequestURL(baseURL *url.URL, key, value string, cli, 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
|
||||
}
|
||||
q.Set("redirect_url", baseURL.String()) // we add the token as a query param on both the redirect_url and the main url
|
||||
baseURL.RawQuery = q.Encode() // and this actual baseURL.
|
||||
baseURL.Path = "cdn-cgi/access/cli"
|
||||
return baseURL.String(), nil
|
||||
}
|
||||
|
||||
// transferRequest downloads the requested resource from the request URL
|
||||
func transferRequest(requestURL string, logger logger.Service) ([]byte, string, error) {
|
||||
client := &http.Client{Timeout: clientTimeout}
|
||||
const pollAttempts = 10
|
||||
// we do "long polling" on the endpoint to get the resource.
|
||||
for i := 0; i < pollAttempts; i++ {
|
||||
buf, key, err := poll(client, requestURL, logger)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
} else if len(buf) > 0 {
|
||||
if err := putSuccess(client, requestURL); err != nil {
|
||||
logger.Errorf("Failed to update resource success: %s", err)
|
||||
}
|
||||
return buf, key, nil
|
||||
}
|
||||
}
|
||||
return nil, "", errors.New("Failed to fetch resource")
|
||||
}
|
||||
|
||||
// poll the endpoint for the request resource, waiting for the user interaction
|
||||
func poll(client *http.Client, requestURL string, logger logger.Service) ([]byte, string, error) {
|
||||
resp, err := client.Get(requestURL)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// ignore everything other than server errors as the resource
|
||||
// may not exist until the user does the interaction
|
||||
if resp.StatusCode >= 500 {
|
||||
return nil, "", fmt.Errorf("error on request %d", resp.StatusCode)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
logger.Info("Waiting for login...")
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if _, err := io.Copy(buf, resp.Body); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return buf.Bytes(), resp.Header.Get("service-public-key"), nil
|
||||
}
|
||||
|
||||
// putSuccess tells the server we successfully downloaded the resource
|
||||
func putSuccess(client *http.Client, requestURL string) error {
|
||||
req, err := http.NewRequest("PUT", requestURL+"/ok", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("HTTP Response Status Code: %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHostnameFromURI(t *testing.T) {
|
||||
assert.Equal(t, "awesome.warptunnels.horse:22", hostnameFromURI("ssh://awesome.warptunnels.horse:22"))
|
||||
assert.Equal(t, "awesome.warptunnels.horse:22", hostnameFromURI("ssh://awesome.warptunnels.horse"))
|
||||
assert.Equal(t, "awesome.warptunnels.horse:2222", hostnameFromURI("ssh://awesome.warptunnels.horse:2222"))
|
||||
assert.Equal(t, "localhost:3389", hostnameFromURI("rdp://localhost"))
|
||||
assert.Equal(t, "localhost:3390", hostnameFromURI("rdp://localhost:3390"))
|
||||
assert.Equal(t, "", hostnameFromURI("trash"))
|
||||
assert.Equal(t, "", hostnameFromURI("https://awesomesauce.com"))
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
var (
|
||||
developerPortal = "https://developers.cloudflare.com/argo-tunnel"
|
||||
quickStartUrl = developerPortal + "/quickstart/quickstart/"
|
||||
serviceUrl = developerPortal + "/reference/service/"
|
||||
argumentsUrl = developerPortal + "/reference/arguments/"
|
||||
)
|
||||
|
||||
// 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.DefaultConfigSearchDirectories() {
|
||||
originCertPath, _ := homedir.Expand(filepath.Join(defaultConfigDir, config.DefaultCredentialFile))
|
||||
if ok, _ := config.FileExists(originCertPath); ok {
|
||||
return originCertPath
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func generateRandomClientID(logger logger.Service) (string, error) {
|
||||
u, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
logger.Errorf("couldn't create UUID for client ID %s", err)
|
||||
return "", err
|
||||
}
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func logClientOptions(c *cli.Context, logger logger.Service) {
|
||||
flags := make(map[string]interface{})
|
||||
for _, flag := range c.LocalFlagNames() {
|
||||
flags[flag] = c.Generic(flag)
|
||||
}
|
||||
|
||||
sliceFlags := []string{"header", "tag", "proxy-dns-upstream", "upstream", "edge"}
|
||||
for _, sliceFlag := range sliceFlags {
|
||||
if len(c.StringSlice(sliceFlag)) > 0 {
|
||||
flags[sliceFlag] = strings.Join(c.StringSlice(sliceFlag), ", ")
|
||||
}
|
||||
}
|
||||
|
||||
if len(flags) > 0 {
|
||||
logger.Infof("Environment variables %v", flags)
|
||||
}
|
||||
|
||||
envs := make(map[string]string)
|
||||
// Find env variables for Argo Tunnel
|
||||
for _, env := range os.Environ() {
|
||||
// All Argo Tunnel env variables start with TUNNEL_
|
||||
if strings.Contains(env, "TUNNEL_") {
|
||||
vars := strings.Split(env, "=")
|
||||
if len(vars) == 2 {
|
||||
envs[vars[0]] = vars[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(envs) > 0 {
|
||||
logger.Infof("Environmental variables %v", envs)
|
||||
}
|
||||
}
|
||||
|
||||
func dnsProxyStandAlone(c *cli.Context) bool {
|
||||
return c.IsSet("proxy-dns") && (!c.IsSet("hostname") && !c.IsSet("tag") && !c.IsSet("hello-world"))
|
||||
}
|
||||
|
||||
func findOriginCert(c *cli.Context, logger logger.Service) (string, error) {
|
||||
originCertPath := c.String("origincert")
|
||||
if originCertPath == "" {
|
||||
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")
|
||||
} else {
|
||||
logger.Errorf("You need to specify the origin certificate path by specifying the origincert option in the configuration file, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", serviceUrl)
|
||||
return "", fmt.Errorf("Client didn't specify origincert path")
|
||||
}
|
||||
}
|
||||
var err error
|
||||
originCertPath, err = homedir.Expand(originCertPath)
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot resolve path %s: %s", originCertPath, err)
|
||||
return "", fmt.Errorf("Cannot resolve path %s", originCertPath)
|
||||
}
|
||||
// Check that the user has acquired a certificate using the login command
|
||||
ok, err := config.FileExists(originCertPath)
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot check if origin cert exists at path %s", originCertPath)
|
||||
return "", fmt.Errorf("Cannot check if origin cert exists at path %s", originCertPath)
|
||||
}
|
||||
if !ok {
|
||||
logger.Errorf(`Cannot find a valid certificate for your origin at the path:
|
||||
|
||||
%s
|
||||
|
||||
If the path above is wrong, specify the path with the -origincert option.
|
||||
If you don't have a certificate signed by Cloudflare, run the command:
|
||||
|
||||
%s login
|
||||
`, originCertPath, os.Args[0])
|
||||
return "", fmt.Errorf("Cannot find a valid certificate at the path %s", originCertPath)
|
||||
}
|
||||
|
||||
return originCertPath, nil
|
||||
}
|
||||
|
||||
func readOriginCert(originCertPath string, logger logger.Service) ([]byte, error) {
|
||||
logger.Debugf("Reading origin cert from %s", originCertPath)
|
||||
|
||||
// Easier to send the certificate as []byte via RPC than decoding it at this point
|
||||
originCert, err := ioutil.ReadFile(originCertPath)
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot read %s to load origin certificate: %s", originCertPath, err)
|
||||
return nil, fmt.Errorf("Cannot read %s to load origin certificate", originCertPath)
|
||||
}
|
||||
return originCert, nil
|
||||
}
|
||||
|
||||
func getOriginCert(c *cli.Context, logger logger.Service) ([]byte, error) {
|
||||
if originCertPath, err := findOriginCert(c, logger); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return readOriginCert(originCertPath, logger)
|
||||
}
|
||||
}
|
||||
|
||||
func prepareTunnelConfig(
|
||||
c *cli.Context,
|
||||
buildInfo *buildinfo.BuildInfo,
|
||||
version string,
|
||||
logger logger.Service,
|
||||
transportLogger logger.Service,
|
||||
namedTunnel *origin.NamedTunnelConfig,
|
||||
) (*origin.TunnelConfig, error) {
|
||||
compatibilityMode := namedTunnel == nil
|
||||
|
||||
hostname, err := validation.ValidateHostname(c.String("hostname"))
|
||||
if err != nil {
|
||||
logger.Errorf("Invalid hostname: %s", err)
|
||||
return nil, errors.Wrap(err, "Invalid hostname")
|
||||
}
|
||||
isFreeTunnel := hostname == ""
|
||||
clientID := c.String("id")
|
||||
if !c.IsSet("id") {
|
||||
clientID, err = generateRandomClientID(logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
tags, err := NewTagSliceFromCLI(c.StringSlice("tag"))
|
||||
if err != nil {
|
||||
logger.Errorf("Tag parse failure: %s", err)
|
||||
return nil, errors.Wrap(err, "Tag parse failure")
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error getting origin cert")
|
||||
}
|
||||
}
|
||||
|
||||
originCertPool, err := tlsconfig.LoadOriginCA(c, logger)
|
||||
if err != nil {
|
||||
logger.Errorf("Error loading cert pool: %s", err)
|
||||
return nil, errors.Wrap(err, "Error loading cert pool")
|
||||
}
|
||||
|
||||
tunnelMetrics := origin.NewTunnelMetrics()
|
||||
httpTransport := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
MaxIdleConns: c.Int("proxy-keepalive-connections"),
|
||||
MaxIdleConnsPerHost: c.Int("proxy-keepalive-connections"),
|
||||
IdleConnTimeout: c.Duration("proxy-keepalive-timeout"),
|
||||
TLSHandshakeTimeout: c.Duration("proxy-tls-timeout"),
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
TLSClientConfig: &tls.Config{RootCAs: originCertPool, InsecureSkipVerify: c.IsSet("no-tls-verify")},
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{
|
||||
Timeout: c.Duration("proxy-connect-timeout"),
|
||||
KeepAlive: c.Duration("proxy-tcp-keepalive"),
|
||||
}
|
||||
if c.Bool("proxy-no-happy-eyeballs") {
|
||||
dialer.FallbackDelay = -1 // As of Golang 1.12, a negative delay disables "happy eyeballs"
|
||||
}
|
||||
dialContext := dialer.DialContext
|
||||
|
||||
if c.IsSet("unix-socket") {
|
||||
unixSocket, err := config.ValidateUnixSocket(c)
|
||||
if err != nil {
|
||||
logger.Errorf("Error validating --unix-socket: %s", err)
|
||||
return nil, errors.Wrap(err, "Error validating --unix-socket")
|
||||
}
|
||||
|
||||
logger.Infof("Proxying tunnel requests to unix:%s", unixSocket)
|
||||
httpTransport.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
// if --unix-socket specified, enforce network type "unix"
|
||||
return dialContext(ctx, "unix", unixSocket)
|
||||
}
|
||||
} else {
|
||||
logger.Infof("Proxying tunnel requests to %s", originURL)
|
||||
httpTransport.DialContext = dialContext
|
||||
}
|
||||
|
||||
if !c.IsSet("hello-world") && c.IsSet("origin-server-name") {
|
||||
httpTransport.TLSClientConfig.ServerName = c.String("origin-server-name")
|
||||
}
|
||||
// If tunnel running in bastion mode, a connection to origin will not exist until initiated by the client.
|
||||
if !c.IsSet(bastionFlag) {
|
||||
if err = validation.ValidateHTTPService(originURL, hostname, httpTransport); err != nil {
|
||||
logger.Errorf("unable to connect to the origin: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
toEdgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c)
|
||||
if err != nil {
|
||||
logger.Errorf("unable to create TLS config to connect with edge: %s", err)
|
||||
return nil, errors.Wrap(err, "unable to create TLS config to connect with edge")
|
||||
}
|
||||
|
||||
if namedTunnel != nil {
|
||||
clientUUID, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can't generate clientUUID")
|
||||
}
|
||||
namedTunnel.Client = tunnelpogs.ClientInfo{
|
||||
ClientID: clientUUID[:],
|
||||
Features: []string{origin.FeatureSerializedHeaders},
|
||||
Version: version,
|
||||
Arch: fmt.Sprintf("%s_%s", buildInfo.GoOS, buildInfo.GoArch),
|
||||
}
|
||||
}
|
||||
|
||||
return &origin.TunnelConfig{
|
||||
BuildInfo: buildInfo,
|
||||
ClientID: clientID,
|
||||
ClientTlsConfig: httpTransport.TLSClientConfig,
|
||||
CompressionQuality: c.Uint64("compression-quality"),
|
||||
EdgeAddrs: c.StringSlice("edge"),
|
||||
GracePeriod: c.Duration("grace-period"),
|
||||
HAConnections: c.Int("ha-connections"),
|
||||
HTTPTransport: httpTransport,
|
||||
HeartbeatInterval: c.Duration("heartbeat-interval"),
|
||||
Hostname: hostname,
|
||||
HTTPHostHeader: c.String("http-host-header"),
|
||||
IncidentLookup: origin.NewIncidentLookup(),
|
||||
IsAutoupdated: c.Bool("is-autoupdated"),
|
||||
IsFreeTunnel: isFreeTunnel,
|
||||
LBPool: c.String("lb-pool"),
|
||||
Logger: logger,
|
||||
TransportLogger: transportLogger,
|
||||
MaxHeartbeats: c.Uint64("heartbeat-count"),
|
||||
Metrics: tunnelMetrics,
|
||||
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
|
||||
NoChunkedEncoding: c.Bool("no-chunked-encoding"),
|
||||
OriginCert: originCert,
|
||||
OriginUrl: originURL,
|
||||
ReportedVersion: version,
|
||||
Retries: c.Uint("retries"),
|
||||
RunFromTerminal: isRunningFromTerminal(),
|
||||
Tags: tags,
|
||||
TlsConfig: toEdgeTLSConfig,
|
||||
NamedTunnel: namedTunnel,
|
||||
ReplaceExisting: c.Bool("force"),
|
||||
// turn off use of reconnect token and auth refresh when using named tunnels
|
||||
UseReconnectToken: compatibilityMode && c.Bool("use-reconnect-token"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func isRunningFromTerminal() bool {
|
||||
return terminal.IsTerminal(int(os.Stdout.Fd()))
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
// +build ignore
|
||||
// TODO: Remove the above build tag and include this test when we start compiling with Golang 1.10.0+
|
||||
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Generated using `openssl req -newkey rsa:512 -nodes -x509 -days 3650`
|
||||
var samplePEM = []byte(`
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIB4DCCAYoCCQCb/H0EUrdXEjANBgkqhkiG9w0BAQsFADB3MQswCQYDVQQGEwJV
|
||||
UzEOMAwGA1UECAwFVGV4YXMxDzANBgNVBAcMBkF1c3RpbjEZMBcGA1UECgwQQ2xv
|
||||
dWRmbGFyZSwgSW5jLjEZMBcGA1UECwwQUHJvZHVjdCBTdHJhdGVneTERMA8GA1UE
|
||||
AwwIVGVzdCBPbmUwHhcNMTgwNDI2MTYxMDUxWhcNMjgwNDIzMTYxMDUxWjB3MQsw
|
||||
CQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxDzANBgNVBAcMBkF1c3RpbjEZMBcG
|
||||
A1UECgwQQ2xvdWRmbGFyZSwgSW5jLjEZMBcGA1UECwwQUHJvZHVjdCBTdHJhdGVn
|
||||
eTERMA8GA1UEAwwIVGVzdCBPbmUwXDANBgkqhkiG9w0BAQEFAANLADBIAkEAwVQD
|
||||
K0SJ25UFLznm2pU3zhzMEvpDEofHVNnCjk4mlDrtVop7PkKZ8pDEmuQANltUrxC8
|
||||
yHBE2wXMv+GlH+bDtwIDAQABMA0GCSqGSIb3DQEBCwUAA0EAjVYQzozIFPkt/HRY
|
||||
uUoZ8zEHIDICb0syFf5VAjm9AgTwIPzUmD+c5vl6LWDnxq7L45nLCzhhQ6YmiwDz
|
||||
X7Wcyg==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIB4DCCAYoCCQDZfCdAJ+mwzDANBgkqhkiG9w0BAQsFADB3MQswCQYDVQQGEwJV
|
||||
UzEOMAwGA1UECAwFVGV4YXMxDzANBgNVBAcMBkF1c3RpbjEZMBcGA1UECgwQQ2xv
|
||||
dWRmbGFyZSwgSW5jLjEZMBcGA1UECwwQUHJvZHVjdCBTdHJhdGVneTERMA8GA1UE
|
||||
AwwIVGVzdCBUd28wHhcNMTgwNDI2MTYxMTIwWhcNMjgwNDIzMTYxMTIwWjB3MQsw
|
||||
CQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxDzANBgNVBAcMBkF1c3RpbjEZMBcG
|
||||
A1UECgwQQ2xvdWRmbGFyZSwgSW5jLjEZMBcGA1UECwwQUHJvZHVjdCBTdHJhdGVn
|
||||
eTERMA8GA1UEAwwIVGVzdCBUd28wXDANBgkqhkiG9w0BAQEFAANLADBIAkEAoHKp
|
||||
ROVK3zCSsH7ocYeyRAML4V7SFAbZcb4WIwDnE08oMBVRkQVcW5tqEkvG3RiClfzV
|
||||
wZIJ3CfqKIeSNSDU9wIDAQABMA0GCSqGSIb3DQEBCwUAA0EAJw2gUbnPiq4C2p5b
|
||||
iWzlA9Q7aKo+VQ4H7IZS7tTccr59nVjvH/TG3eWujpnocr4TOqW9M3CK1DF9mUGP
|
||||
3pQ3Jg==
|
||||
-----END CERTIFICATE-----
|
||||
`)
|
||||
|
||||
var systemCertPoolSubjects []*pkix.Name
|
||||
|
||||
type certificateFixture struct {
|
||||
ou string
|
||||
cn string
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
systemCertPool, err := x509.SystemCertPool()
|
||||
if isUnrecoverableError(err) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if systemCertPool == nil {
|
||||
// On Windows, let's just assume the system cert pool was empty
|
||||
systemCertPool = x509.NewCertPool()
|
||||
}
|
||||
|
||||
systemCertPoolSubjects, err = getCertPoolSubjects(systemCertPool)
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestLoadOriginCertPoolJustSystemPool(t *testing.T) {
|
||||
certPoolSubjects := loadCertPoolSubjects(t, nil)
|
||||
extraSubjects := subjectSubtract(systemCertPoolSubjects, certPoolSubjects)
|
||||
|
||||
// Remove extra subjects from the cert pool
|
||||
var filteredSystemCertPoolSubjects []*pkix.Name
|
||||
|
||||
t.Log(extraSubjects)
|
||||
|
||||
OUTER:
|
||||
for _, subject := range certPoolSubjects {
|
||||
for _, extraSubject := range extraSubjects {
|
||||
if subject == extraSubject {
|
||||
t.Log(extraSubject)
|
||||
continue OUTER
|
||||
}
|
||||
}
|
||||
|
||||
filteredSystemCertPoolSubjects = append(filteredSystemCertPoolSubjects, subject)
|
||||
}
|
||||
|
||||
assert.Equal(t, len(filteredSystemCertPoolSubjects), len(systemCertPoolSubjects))
|
||||
|
||||
difference := subjectSubtract(systemCertPoolSubjects, filteredSystemCertPoolSubjects)
|
||||
assert.Equal(t, 0, len(difference))
|
||||
}
|
||||
|
||||
func TestLoadOriginCertPoolCFCertificates(t *testing.T) {
|
||||
certPoolSubjects := loadCertPoolSubjects(t, nil)
|
||||
|
||||
extraSubjects := subjectSubtract(systemCertPoolSubjects, certPoolSubjects)
|
||||
|
||||
expected := []*certificateFixture{
|
||||
{ou: "CloudFlare Origin SSL ECC Certificate Authority"},
|
||||
{ou: "CloudFlare Origin SSL Certificate Authority"},
|
||||
{cn: "origin-pull.cloudflare.net"},
|
||||
{cn: "Argo Tunnel Sample Hello Server Certificate"},
|
||||
}
|
||||
|
||||
assertFixturesMatchSubjects(t, expected, extraSubjects)
|
||||
}
|
||||
|
||||
func TestLoadOriginCertPoolWithExtraPEMs(t *testing.T) {
|
||||
certPoolWithoutPEMSubjects := loadCertPoolSubjects(t, nil)
|
||||
certPoolWithPEMSubjects := loadCertPoolSubjects(t, samplePEM)
|
||||
|
||||
difference := subjectSubtract(certPoolWithoutPEMSubjects, certPoolWithPEMSubjects)
|
||||
|
||||
assert.Equal(t, 2, len(difference))
|
||||
|
||||
expected := []*certificateFixture{
|
||||
{cn: "Test One"},
|
||||
{cn: "Test Two"},
|
||||
}
|
||||
|
||||
assertFixturesMatchSubjects(t, expected, difference)
|
||||
}
|
||||
|
||||
func loadCertPoolSubjects(t *testing.T, originCAPoolPEM []byte) []*pkix.Name {
|
||||
certPool, err := loadOriginCertPool(originCAPoolPEM)
|
||||
if isUnrecoverableError(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.NotEmpty(t, certPool.Subjects())
|
||||
certPoolSubjects, err := getCertPoolSubjects(certPool)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return certPoolSubjects
|
||||
}
|
||||
|
||||
func assertFixturesMatchSubjects(t *testing.T, fixtures []*certificateFixture, subjects []*pkix.Name) {
|
||||
assert.Equal(t, len(fixtures), len(subjects))
|
||||
|
||||
for _, fixture := range fixtures {
|
||||
found := false
|
||||
for _, subject := range subjects {
|
||||
found = found || fixtureMatchesSubjectPredicate(fixture, subject)
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fixtureMatchesSubjectPredicate(fixture *certificateFixture, subject *pkix.Name) bool {
|
||||
cnMatch := true
|
||||
if fixture.cn != "" {
|
||||
cnMatch = fixture.cn == subject.CommonName
|
||||
}
|
||||
|
||||
ouMatch := true
|
||||
if fixture.ou != "" {
|
||||
ouMatch = len(subject.OrganizationalUnit) > 0 && fixture.ou == subject.OrganizationalUnit[0]
|
||||
}
|
||||
|
||||
return cnMatch && ouMatch
|
||||
}
|
||||
|
||||
func subjectSubtract(left []*pkix.Name, right []*pkix.Name) []*pkix.Name {
|
||||
var difference []*pkix.Name
|
||||
|
||||
var found bool
|
||||
for _, r := range right {
|
||||
found = false
|
||||
for _, l := range left {
|
||||
if (*l).String() == (*r).String() {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
difference = append(difference, r)
|
||||
}
|
||||
}
|
||||
|
||||
return difference
|
||||
}
|
||||
|
||||
func getCertPoolSubjects(certPool *x509.CertPool) ([]*pkix.Name, error) {
|
||||
var subjects []*pkix.Name
|
||||
|
||||
for _, subject := range certPool.Subjects() {
|
||||
var sequence pkix.RDNSequence
|
||||
_, err := asn1.Unmarshal(subject, &sequence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
name := pkix.Name{}
|
||||
name.FillFromRDNSequence(&sequence)
|
||||
|
||||
subjects = append(subjects, &name)
|
||||
}
|
||||
|
||||
return subjects, nil
|
||||
}
|
||||
|
||||
func isUnrecoverableError(err error) bool {
|
||||
return err != nil && err.Error() != "crypto/x509: system root pool is not available on Windows"
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"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 "github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
baseLoginURL = "https://dash.cloudflare.com/argotunnel"
|
||||
callbackStoreURL = "https://login.argotunnel.com/"
|
||||
)
|
||||
|
||||
func login(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
path, ok, err := checkForExistingCert()
|
||||
if ok {
|
||||
fmt.Fprintf(os.Stdout, "You have an existing certificate at %s which login would overwrite.\nIf this is intentional, please move or delete that file then run this command again.\n", path)
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
loginURL, err := url.Parse(baseLoginURL)
|
||||
if err != nil {
|
||||
// shouldn't happen, URL is hardcoded
|
||||
return err
|
||||
}
|
||||
|
||||
_, 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
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stdout, "You have successfully logged in.\nIf you wish to copy your credentials to a server, they have been saved to:\n%s\n", path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkForExistingCert() (string, bool, error) {
|
||||
configPath, err := homedir.Expand(config.DefaultConfigSearchDirectories()[0])
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
ok, err := config.FileExists(configPath)
|
||||
if !ok && err == nil {
|
||||
// create config directory if doesn't already exist
|
||||
err = os.Mkdir(configPath, 0700)
|
||||
}
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
path := filepath.Join(configPath, config.DefaultCredentialFile)
|
||||
fileInfo, err := os.Stat(path)
|
||||
if err == nil && fileInfo.Size() > 0 {
|
||||
return path, true, nil
|
||||
}
|
||||
if err != nil && err.(*os.PathError).Err != syscall.ENOENT {
|
||||
return path, false, err
|
||||
}
|
||||
|
||||
return path, false, nil
|
||||
}
|
||||
@@ -1,30 +1,28 @@
|
||||
package main
|
||||
package tunnel
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
func runDNSProxyServer(c *cli.Context, dnsReadySignal, shutdownC chan struct{}) error {
|
||||
func runDNSProxyServer(c *cli.Context, dnsReadySignal, shutdownC chan struct{}, logger logger.Service) error {
|
||||
port := c.Int("proxy-dns-port")
|
||||
if port <= 0 || port > 65535 {
|
||||
logger.Errorf("The 'proxy-dns-port' must be a valid port number in <1, 65535> range.")
|
||||
return errors.New("The 'proxy-dns-port' must be a valid port number in <1, 65535> range.")
|
||||
}
|
||||
listener, err := tunneldns.CreateListener(c.String("proxy-dns-address"), uint16(port), c.StringSlice("proxy-dns-upstream"))
|
||||
listener, err := tunneldns.CreateListener(c.String("proxy-dns-address"), uint16(port), c.StringSlice("proxy-dns-upstream"), c.StringSlice("proxy-dns-bootstrap"), logger)
|
||||
if err != nil {
|
||||
close(dnsReadySignal)
|
||||
listener.Stop()
|
||||
logger.WithError(err).Error("Cannot create the DNS over HTTPS proxy server")
|
||||
return errors.Wrap(err, "Cannot create the DNS over HTTPS proxy server")
|
||||
}
|
||||
|
||||
err = listener.Start(dnsReadySignal)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Cannot start the DNS over HTTPS proxy server")
|
||||
return errors.Wrap(err, "Cannot start the DNS over HTTPS proxy server")
|
||||
}
|
||||
<-shutdownC
|
||||
@@ -1,25 +1,29 @@
|
||||
package main
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
// waitForSignal notifies all routines to shutdownC immediately by closing the
|
||||
// shutdownC when one of the routines in main exits, or when this process receives
|
||||
// SIGTERM/SIGINT
|
||||
func waitForSignal(errC chan error, shutdownC chan struct{}) error {
|
||||
func waitForSignal(errC chan error, shutdownC chan struct{}, logger logger.Service) error {
|
||||
signals := make(chan os.Signal, 10)
|
||||
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
|
||||
defer signal.Stop(signals)
|
||||
|
||||
select {
|
||||
case err := <-errC:
|
||||
logger.Infof("terminating due to error: %v", err)
|
||||
close(shutdownC)
|
||||
return err
|
||||
case <-signals:
|
||||
case s := <-signals:
|
||||
logger.Infof("terminating due to signal %s", s)
|
||||
close(shutdownC)
|
||||
case <-shutdownC:
|
||||
}
|
||||
@@ -37,6 +41,7 @@ func waitForSignal(errC chan error, shutdownC chan struct{}) error {
|
||||
func waitForSignalWithGraceShutdown(errC chan error,
|
||||
shutdownC, graceShutdownC chan struct{},
|
||||
gracePeriod time.Duration,
|
||||
logger logger.Service,
|
||||
) error {
|
||||
signals := make(chan os.Signal, 10)
|
||||
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
|
||||
@@ -44,14 +49,16 @@ func waitForSignalWithGraceShutdown(errC chan error,
|
||||
|
||||
select {
|
||||
case err := <-errC:
|
||||
logger.Infof("Initiating graceful shutdown due to %v ...", err)
|
||||
close(graceShutdownC)
|
||||
close(shutdownC)
|
||||
return err
|
||||
case <-signals:
|
||||
case s := <-signals:
|
||||
logger.Infof("Initiating graceful shutdown due to signal %s ...", s)
|
||||
close(graceShutdownC)
|
||||
waitForGracePeriod(signals, errC, shutdownC, gracePeriod)
|
||||
waitForGracePeriod(signals, errC, shutdownC, gracePeriod, logger)
|
||||
case <-graceShutdownC:
|
||||
waitForGracePeriod(signals, errC, shutdownC, gracePeriod)
|
||||
waitForGracePeriod(signals, errC, shutdownC, gracePeriod, logger)
|
||||
case <-shutdownC:
|
||||
close(graceShutdownC)
|
||||
}
|
||||
@@ -63,8 +70,8 @@ func waitForGracePeriod(signals chan os.Signal,
|
||||
errC chan error,
|
||||
shutdownC chan struct{},
|
||||
gracePeriod time.Duration,
|
||||
logger logger.Service,
|
||||
) {
|
||||
logger.Infof("Initiating graceful shutdown...")
|
||||
// Unregister signal handler early, so the client can send a second SIGTERM/SIGINT
|
||||
// to force shutdown cloudflared
|
||||
signal.Stop(signals)
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -27,6 +28,8 @@ func testChannelClosed(t *testing.T, c chan struct{}) {
|
||||
}
|
||||
|
||||
func TestWaitForSignal(t *testing.T) {
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
// Test handling server error
|
||||
errC := make(chan error)
|
||||
shutdownC := make(chan struct{})
|
||||
@@ -36,7 +39,7 @@ func TestWaitForSignal(t *testing.T) {
|
||||
}()
|
||||
|
||||
// received error, shutdownC should be closed
|
||||
err := waitForSignal(errC, shutdownC)
|
||||
err := waitForSignal(errC, shutdownC, logger)
|
||||
assert.Equal(t, serverErr, err)
|
||||
testChannelClosed(t, shutdownC)
|
||||
|
||||
@@ -56,7 +59,7 @@ func TestWaitForSignal(t *testing.T) {
|
||||
syscall.Kill(syscall.Getpid(), sig)
|
||||
}(sig)
|
||||
|
||||
err = waitForSignal(errC, shutdownC)
|
||||
err = waitForSignal(errC, shutdownC, logger)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, shutdownErr, <-errC)
|
||||
testChannelClosed(t, shutdownC)
|
||||
@@ -73,8 +76,10 @@ func TestWaitForSignalWithGraceShutdown(t *testing.T) {
|
||||
errC <- serverErr
|
||||
}()
|
||||
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
// received error, both shutdownC and graceshutdownC should be closed
|
||||
err := waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick)
|
||||
err := waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
|
||||
assert.Equal(t, serverErr, err)
|
||||
testChannelClosed(t, shutdownC)
|
||||
testChannelClosed(t, graceshutdownC)
|
||||
@@ -84,7 +89,7 @@ func TestWaitForSignalWithGraceShutdown(t *testing.T) {
|
||||
shutdownC = make(chan struct{})
|
||||
graceshutdownC = make(chan struct{})
|
||||
close(shutdownC)
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick)
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
|
||||
assert.NoError(t, err)
|
||||
testChannelClosed(t, shutdownC)
|
||||
testChannelClosed(t, graceshutdownC)
|
||||
@@ -94,7 +99,7 @@ func TestWaitForSignalWithGraceShutdown(t *testing.T) {
|
||||
shutdownC = make(chan struct{})
|
||||
graceshutdownC = make(chan struct{})
|
||||
close(graceshutdownC)
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick)
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
|
||||
assert.NoError(t, err)
|
||||
testChannelClosed(t, shutdownC)
|
||||
testChannelClosed(t, graceshutdownC)
|
||||
@@ -117,7 +122,7 @@ func TestWaitForSignalWithGraceShutdown(t *testing.T) {
|
||||
syscall.Kill(syscall.Getpid(), sig)
|
||||
}(sig)
|
||||
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick)
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, graceShutdownErr, <-errC)
|
||||
testChannelClosed(t, shutdownC)
|
||||
@@ -143,7 +148,7 @@ func TestWaitForSignalWithGraceShutdown(t *testing.T) {
|
||||
syscall.Kill(syscall.Getpid(), sig)
|
||||
}(sig)
|
||||
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick)
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, shutdownErr, <-errC)
|
||||
testChannelClosed(t, shutdownC)
|
||||
@@ -0,0 +1,353 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/certutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// subcommandContext carries structs shared between subcommands, to reduce number of arguments needed to
|
||||
// pass between subcommands, and make sure they are only initialized once
|
||||
type subcommandContext struct {
|
||||
c *cli.Context
|
||||
logger logger.Service
|
||||
|
||||
// These fields should be accessed using their respective Getter
|
||||
tunnelstoreClient tunnelstore.Client
|
||||
userCredential *userCredential
|
||||
|
||||
isUIEnabled bool
|
||||
}
|
||||
|
||||
func newSubcommandContext(c *cli.Context) (*subcommandContext, error) {
|
||||
isUIEnabled := c.IsSet(uiFlag) && c.String("name") != ""
|
||||
|
||||
// If UI is enabled, terminal log output should be disabled -- log should be written into a UI log window instead
|
||||
logger, err := createLogger(c, false, isUIEnabled)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
return &subcommandContext{
|
||||
c: c,
|
||||
logger: logger,
|
||||
isUIEnabled: isUIEnabled,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type userCredential struct {
|
||||
cert *certutil.OriginCert
|
||||
certPath string
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) client() (tunnelstore.Client, error) {
|
||||
if sc.tunnelstoreClient != nil {
|
||||
return sc.tunnelstoreClient, nil
|
||||
}
|
||||
credential, err := sc.credential()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userAgent := fmt.Sprintf("cloudflared/%s", version)
|
||||
client, err := tunnelstore.NewRESTClient(sc.c.String("api-url"), credential.cert.AccountID, credential.cert.ZoneID, credential.cert.ServiceKey, userAgent, sc.logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sc.tunnelstoreClient = client
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) credential() (*userCredential, error) {
|
||||
if sc.userCredential == nil {
|
||||
originCertPath, err := findOriginCert(sc.c, sc.logger)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error locating origin cert")
|
||||
}
|
||||
blocks, err := readOriginCert(originCertPath, sc.logger)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Can't read origin cert from %s", originCertPath)
|
||||
}
|
||||
|
||||
cert, err := certutil.DecodeOriginCert(blocks)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error decoding origin cert")
|
||||
}
|
||||
|
||||
if cert.AccountID == "" {
|
||||
return nil, errors.Errorf(`Origin certificate needs to be refreshed before creating new tunnels.\nDelete %s and run "cloudflared login" to obtain a new cert.`, originCertPath)
|
||||
}
|
||||
|
||||
sc.userCredential = &userCredential{
|
||||
cert: cert,
|
||||
certPath: originCertPath,
|
||||
}
|
||||
}
|
||||
return sc.userCredential, nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) readTunnelCredentials(tunnelID uuid.UUID) (*pogs.TunnelAuth, error) {
|
||||
filePath, err := sc.tunnelCredentialsPath(tunnelID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "couldn't read tunnel credentials from %v", filePath)
|
||||
}
|
||||
|
||||
var auth pogs.TunnelAuth
|
||||
if err = json.Unmarshal(body, &auth); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &auth, nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) tunnelCredentialsPath(tunnelID uuid.UUID) (string, error) {
|
||||
if filePath := sc.c.String("credentials-file"); filePath != "" {
|
||||
if validFilePath(filePath) {
|
||||
return filePath, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to look for tunnel credentials in the origin cert directory
|
||||
if originCertPath, err := findOriginCert(sc.c, sc.logger); err == nil {
|
||||
originCertDir := filepath.Dir(originCertPath)
|
||||
if filePath, err := tunnelFilePath(tunnelID, originCertDir); err == nil {
|
||||
if validFilePath(filePath) {
|
||||
return filePath, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort look under default config directories
|
||||
for _, configDir := range config.DefaultConfigSearchDirectories() {
|
||||
if filePath, err := tunnelFilePath(tunnelID, configDir); err == nil {
|
||||
if validFilePath(filePath) {
|
||||
return filePath, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("Tunnel credentials file not found")
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) create(name string) (*tunnelstore.Tunnel, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "couldn't create client to talk to Argo Tunnel backend")
|
||||
}
|
||||
|
||||
tunnelSecret, err := generateTunnelSecret()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "couldn't generate the secret for your new tunnel")
|
||||
}
|
||||
|
||||
tunnel, err := client.CreateTunnel(name, tunnelSecret)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Create Tunnel API call failed")
|
||||
}
|
||||
|
||||
credential, err := sc.credential()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if writeFileErr := writeTunnelCredentials(tunnel.ID, credential.cert.AccountID, credential.certPath, tunnelSecret, sc.logger); err != nil {
|
||||
var errorLines []string
|
||||
errorLines = append(errorLines, fmt.Sprintf("Your tunnel '%v' was created with ID %v. However, cloudflared couldn't write to the tunnel credentials file at %v.json.", tunnel.Name, tunnel.ID, tunnel.ID))
|
||||
errorLines = append(errorLines, fmt.Sprintf("The file-writing error is: %v", writeFileErr))
|
||||
if deleteErr := client.DeleteTunnel(tunnel.ID); deleteErr != nil {
|
||||
errorLines = append(errorLines, fmt.Sprintf("Cloudflared tried to delete the tunnel for you, but encountered an error. You should use `cloudflared tunnel delete %v` to delete the tunnel yourself, because the tunnel can't be run without the tunnelfile.", tunnel.ID))
|
||||
errorLines = append(errorLines, fmt.Sprintf("The delete tunnel error is: %v", deleteErr))
|
||||
} else {
|
||||
errorLines = append(errorLines, fmt.Sprintf("The tunnel was deleted, because the tunnel can't be run without the tunnelfile"))
|
||||
}
|
||||
errorMsg := strings.Join(errorLines, "\n")
|
||||
return nil, errors.New(errorMsg)
|
||||
}
|
||||
|
||||
if outputFormat := sc.c.String(outputFormatFlag.Name); outputFormat != "" {
|
||||
return nil, renderOutput(outputFormat, &tunnel)
|
||||
}
|
||||
|
||||
sc.logger.Infof("Created tunnel %s with id %s", tunnel.Name, tunnel.ID)
|
||||
return tunnel, nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) list(filter *tunnelstore.Filter) ([]*tunnelstore.Tunnel, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.ListTunnels(filter)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error {
|
||||
forceFlagSet := sc.c.Bool("force")
|
||||
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, id := range tunnelIDs {
|
||||
tunnel, err := client.GetTunnel(id)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Can't get tunnel information. Please check tunnel id: %s", tunnel.ID)
|
||||
}
|
||||
|
||||
// Check if tunnel DeletedAt field has already been set
|
||||
if !tunnel.DeletedAt.IsZero() {
|
||||
return fmt.Errorf("Tunnel %s has already been deleted", tunnel.ID)
|
||||
}
|
||||
// Check if tunnel has existing connections and if force flag is set, cleanup connections
|
||||
if len(tunnel.Connections) > 0 {
|
||||
if !forceFlagSet {
|
||||
return fmt.Errorf("You can not delete tunnel %s because it has active connections. To see connections run the 'list' command. If you believe the tunnel is not active, you can use a -f / --force flag with this command.", id)
|
||||
}
|
||||
|
||||
if err := client.CleanupConnections(tunnel.ID); err != nil {
|
||||
return errors.Wrapf(err, "Error cleaning up connections for tunnel %s", tunnel.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if err := client.DeleteTunnel(tunnel.ID); err != nil {
|
||||
return errors.Wrapf(err, "Error deleting tunnel %s", tunnel.ID)
|
||||
}
|
||||
|
||||
tunnelCredentialsPath, err := sc.tunnelCredentialsPath(tunnel.ID)
|
||||
if err != nil {
|
||||
sc.logger.Infof("Cannot locate tunnel credentials to delete, error: %v. Please delete the file manually", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = os.Remove(tunnelCredentialsPath); err != nil {
|
||||
sc.logger.Infof("Cannot delete tunnel credentials, error: %v. Please delete the file manually", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) run(tunnelID uuid.UUID) error {
|
||||
credentials, err := sc.readTunnelCredentials(tunnelID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return StartServer(
|
||||
sc.c,
|
||||
version,
|
||||
shutdownC,
|
||||
graceShutdownC,
|
||||
&origin.NamedTunnelConfig{Auth: *credentials, ID: tunnelID},
|
||||
sc.logger,
|
||||
sc.isUIEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) cleanupConnections(tunnelIDs []uuid.UUID) error {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, tunnelID := range tunnelIDs {
|
||||
sc.logger.Infof("Cleanup connection for tunnel %s", tunnelID)
|
||||
if err := client.CleanupConnections(tunnelID); err != nil {
|
||||
sc.logger.Errorf("Error cleaning up connections for tunnel %v, error :%v", tunnelID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) route(tunnelID uuid.UUID, r tunnelstore.Route) (tunnelstore.RouteResult, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.RouteTunnel(tunnelID, r)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) tunnelActive(name string) (*tunnelstore.Tunnel, bool, error) {
|
||||
filter := tunnelstore.NewFilter()
|
||||
filter.NoDeleted()
|
||||
filter.ByName(name)
|
||||
tunnels, err := sc.list(filter)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if len(tunnels) == 0 {
|
||||
return nil, false, nil
|
||||
}
|
||||
// There should only be 1 active tunnel for a given name
|
||||
return tunnels[0], true, nil
|
||||
}
|
||||
|
||||
// findID parses the input. If it's a UUID, return the UUID.
|
||||
// Otherwise, assume it's a name, and look up the ID of that tunnel.
|
||||
func (sc *subcommandContext) findID(input string) (uuid.UUID, error) {
|
||||
if u, err := uuid.Parse(input); err == nil {
|
||||
return u, nil
|
||||
}
|
||||
|
||||
if tunnel, found, err := sc.tunnelActive(input); err != nil {
|
||||
return uuid.Nil, err
|
||||
} else if found {
|
||||
return tunnel.ID, nil
|
||||
}
|
||||
|
||||
return uuid.Nil, fmt.Errorf("%s is neither the ID nor the name of any of your tunnels", input)
|
||||
}
|
||||
|
||||
// findIDs is just like mapping `findID` over a slice, but it only uses
|
||||
// one Tunnelstore API call.
|
||||
func (sc *subcommandContext) findIDs(inputs []string) ([]uuid.UUID, error) {
|
||||
|
||||
// First, look up all tunnels the user has
|
||||
filter := tunnelstore.NewFilter()
|
||||
filter.NoDeleted()
|
||||
tunnels, err := sc.list(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Do the pure list-processing in its own function, so that it can be
|
||||
// unit tested easily.
|
||||
return findIDs(tunnels, inputs)
|
||||
}
|
||||
|
||||
func findIDs(tunnels []*tunnelstore.Tunnel, inputs []string) ([]uuid.UUID, error) {
|
||||
// Put them into a dictionary for faster lookups
|
||||
nameToID := make(map[string]uuid.UUID, len(tunnels))
|
||||
for _, tunnel := range tunnels {
|
||||
nameToID[tunnel.Name] = tunnel.ID
|
||||
}
|
||||
|
||||
// For each input, try to find the tunnel ID.
|
||||
tunnelIDs := make([]uuid.UUID, len(inputs))
|
||||
var badInputs []string
|
||||
for i, input := range inputs {
|
||||
if id, err := uuid.Parse(input); err == nil {
|
||||
tunnelIDs[i] = id
|
||||
} else if id, ok := nameToID[input]; ok {
|
||||
tunnelIDs[i] = id
|
||||
} else {
|
||||
badInputs = append(badInputs, input)
|
||||
}
|
||||
}
|
||||
if len(badInputs) > 0 {
|
||||
msg := "Please specify either the ID or name of a tunnel. The following inputs were neither: %s"
|
||||
return nil, fmt.Errorf(msg, strings.Join(badInputs, ", "))
|
||||
}
|
||||
return tunnelIDs, nil
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func Test_findIDs(t *testing.T) {
|
||||
type args struct {
|
||||
tunnels []*tunnelstore.Tunnel
|
||||
inputs []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []uuid.UUID
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "input not found",
|
||||
args: args{
|
||||
inputs: []string{"asdf"},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "only UUID",
|
||||
args: args{
|
||||
inputs: []string{"a8398a0b-876d-48ed-b609-3fcfd67a4950"},
|
||||
},
|
||||
want: []uuid.UUID{uuid.MustParse("a8398a0b-876d-48ed-b609-3fcfd67a4950")},
|
||||
},
|
||||
{
|
||||
name: "only name",
|
||||
args: args{
|
||||
tunnels: []*tunnelstore.Tunnel{
|
||||
{
|
||||
ID: uuid.MustParse("a8398a0b-876d-48ed-b609-3fcfd67a4950"),
|
||||
Name: "tunnel1",
|
||||
},
|
||||
},
|
||||
inputs: []string{"tunnel1"},
|
||||
},
|
||||
want: []uuid.UUID{uuid.MustParse("a8398a0b-876d-48ed-b609-3fcfd67a4950")},
|
||||
},
|
||||
{
|
||||
name: "both UUID and name",
|
||||
args: args{
|
||||
tunnels: []*tunnelstore.Tunnel{
|
||||
{
|
||||
ID: uuid.MustParse("a8398a0b-876d-48ed-b609-3fcfd67a4950"),
|
||||
Name: "tunnel1",
|
||||
},
|
||||
{
|
||||
ID: uuid.MustParse("bf028b68-744f-466e-97f8-c46161d80aa5"),
|
||||
Name: "tunnel2",
|
||||
},
|
||||
},
|
||||
inputs: []string{"tunnel1", "bf028b68-744f-466e-97f8-c46161d80aa5"},
|
||||
},
|
||||
want: []uuid.UUID{
|
||||
uuid.MustParse("a8398a0b-876d-48ed-b609-3fcfd67a4950"),
|
||||
uuid.MustParse("bf028b68-744f-466e-97f8-c46161d80aa5"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := findIDs(tt.args.tunnels, tt.args.inputs)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("findIDs() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("findIDs() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
"golang.org/x/net/idna"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
)
|
||||
|
||||
const (
|
||||
credFileFlagAlias = "cred-file"
|
||||
)
|
||||
|
||||
var (
|
||||
showDeletedFlag = &cli.BoolFlag{
|
||||
Name: "show-deleted",
|
||||
Aliases: []string{"d"},
|
||||
Usage: "Include deleted tunnels in the list",
|
||||
}
|
||||
listNameFlag = &cli.StringFlag{
|
||||
Name: "name",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "List tunnels with the given `NAME`",
|
||||
}
|
||||
listExistedAtFlag = &cli.TimestampFlag{
|
||||
Name: "when",
|
||||
Aliases: []string{"w"},
|
||||
Usage: "List tunnels that are active at the given `TIME` in RFC3339 format",
|
||||
Layout: tunnelstore.TimeLayout,
|
||||
DefaultText: fmt.Sprintf("current time, %s", time.Now().Format(tunnelstore.TimeLayout)),
|
||||
}
|
||||
listIDFlag = &cli.StringFlag{
|
||||
Name: "id",
|
||||
Aliases: []string{"i"},
|
||||
Usage: "List tunnel by `ID`",
|
||||
}
|
||||
showRecentlyDisconnected = &cli.BoolFlag{
|
||||
Name: "show-recently-disconnected",
|
||||
Aliases: []string{"rd"},
|
||||
Usage: "Include connections that have recently disconnected in the list",
|
||||
}
|
||||
outputFormatFlag = &cli.StringFlag{
|
||||
Name: "output",
|
||||
Aliases: []string{"o"},
|
||||
Usage: "Render output using given `FORMAT`. Valid options are 'json' or 'yaml'",
|
||||
}
|
||||
forceFlag = &cli.BoolFlag{
|
||||
Name: "force",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "By default, if a tunnel is currently being run from a cloudflared, you can't " +
|
||||
"simultaneously rerun it again from a second cloudflared. The --force flag lets you " +
|
||||
"overwrite the previous tunnel. If you want to use a single hostname with multiple " +
|
||||
"tunnels, you can do so with Cloudflare's Load Balancer product.",
|
||||
}
|
||||
credentialsFileFlag = &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.",
|
||||
}
|
||||
)
|
||||
|
||||
func buildCreateCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "create",
|
||||
Action: cliutil.ErrorHandler(createCommand),
|
||||
Usage: "Create a new tunnel with given name",
|
||||
ArgsUsage: "TUNNEL-NAME",
|
||||
Flags: []cli.Flag{outputFormatFlag},
|
||||
}
|
||||
}
|
||||
|
||||
// generateTunnelSecret as an array of 32 bytes using secure random number generator
|
||||
func generateTunnelSecret() ([]byte, error) {
|
||||
randomBytes := make([]byte, 32)
|
||||
_, err := rand.Read(randomBytes)
|
||||
return randomBytes, err
|
||||
}
|
||||
|
||||
func 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()
|
||||
|
||||
_, err = sc.create(name)
|
||||
return errors.Wrap(err, "failed to create tunnel")
|
||||
}
|
||||
|
||||
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 uuid.UUID, accountID, originCertPath string, tunnelSecret []byte, logger logger.Service) error {
|
||||
originCertDir := filepath.Dir(originCertPath)
|
||||
filePath, err := tunnelFilePath(tunnelID, originCertDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body, err := json.Marshal(pogs.TunnelAuth{
|
||||
AccountTag: accountID,
|
||||
TunnelSecret: tunnelSecret,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to marshal tunnel credentials to JSON")
|
||||
}
|
||||
logger.Infof("Writing tunnel credentials to %v. cloudflared chose this file based on where your origin certificate was found.", filePath)
|
||||
logger.Infof("Keep this file secret. To revoke these credentials, delete the tunnel.")
|
||||
return ioutil.WriteFile(filePath, body, 400)
|
||||
}
|
||||
|
||||
func validFilePath(path string) bool {
|
||||
fileStat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !fileStat.IsDir()
|
||||
}
|
||||
|
||||
func buildListCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "list",
|
||||
Action: cliutil.ErrorHandler(listCommand),
|
||||
Usage: "List existing tunnels",
|
||||
ArgsUsage: " ",
|
||||
Flags: []cli.Flag{outputFormatFlag, showDeletedFlag, listNameFlag, listExistedAtFlag, listIDFlag, showRecentlyDisconnected},
|
||||
}
|
||||
}
|
||||
|
||||
func listCommand(c *cli.Context) error {
|
||||
sc, err := newSubcommandContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
tunnels, err := sc.list(filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" {
|
||||
return renderOutput(outputFormat, tunnels)
|
||||
}
|
||||
|
||||
if len(tunnels) > 0 {
|
||||
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, showRecentlyDisconnected bool) {
|
||||
const (
|
||||
minWidth = 0
|
||||
tabWidth = 8
|
||||
padding = 1
|
||||
padChar = ' '
|
||||
flags = 0
|
||||
)
|
||||
|
||||
writer := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, padChar, flags)
|
||||
defer writer.Flush()
|
||||
|
||||
// Print column headers with tabbed columns
|
||||
fmt.Fprintln(writer, "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, showRecentlyDisconnected),
|
||||
)
|
||||
fmt.Fprintln(writer, formattedStr)
|
||||
}
|
||||
}
|
||||
|
||||
func fmtConnections(connections []tunnelstore.Connection, showRecentlyDisconnected bool) string {
|
||||
|
||||
// Count connections per colo
|
||||
numConnsPerColo := make(map[string]uint, len(connections))
|
||||
for _, connection := range connections {
|
||||
if !connection.IsPendingReconnect || showRecentlyDisconnected {
|
||||
numConnsPerColo[connection.ColoName]++
|
||||
}
|
||||
}
|
||||
|
||||
// Get sorted list of colos
|
||||
sortedColos := []string{}
|
||||
for coloName := range numConnsPerColo {
|
||||
sortedColos = append(sortedColos, coloName)
|
||||
}
|
||||
sort.Strings(sortedColos)
|
||||
|
||||
// Map each colo to its frequency, combine into output string.
|
||||
var output []string
|
||||
for _, coloName := range sortedColos {
|
||||
output = append(output, fmt.Sprintf("%dx%s", numConnsPerColo[coloName], coloName))
|
||||
}
|
||||
return strings.Join(output, ", ")
|
||||
}
|
||||
|
||||
func buildDeleteCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "delete",
|
||||
Action: cliutil.ErrorHandler(deleteCommand),
|
||||
Usage: "Delete existing tunnel by UUID or name",
|
||||
ArgsUsage: "TUNNEL",
|
||||
Flags: []cli.Flag{credentialsFileFlag, forceDeleteFlag},
|
||||
}
|
||||
}
|
||||
|
||||
func deleteCommand(c *cli.Context) error {
|
||||
sc, err := newSubcommandContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.NArg() < 1 {
|
||||
return cliutil.UsageError(`"cloudflared tunnel delete" requires at least 1 argument, the ID or name of the tunnel to delete.`)
|
||||
}
|
||||
|
||||
tunnelIDs, err := sc.findIDs(c.Args().Slice())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sc.delete(tunnelIDs)
|
||||
}
|
||||
|
||||
func renderOutput(format string, v interface{}) error {
|
||||
switch format {
|
||||
case "json":
|
||||
encoder := json.NewEncoder(os.Stdout)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(v)
|
||||
case "yaml":
|
||||
return yaml.NewEncoder(os.Stdout).Encode(v)
|
||||
default:
|
||||
return errors.Errorf("Unknown output format '%s'", format)
|
||||
}
|
||||
}
|
||||
|
||||
func buildRunCommand() *cli.Command {
|
||||
flags := []cli.Flag{
|
||||
forceFlag,
|
||||
credentialsFileFlag,
|
||||
urlFlag(false),
|
||||
helloWorldFlag(false),
|
||||
createSocks5Flag(false),
|
||||
}
|
||||
flags = append(flags, sshFlags(false)...)
|
||||
var runCommandHelpTemplate = `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
{{.UsageText}}
|
||||
|
||||
DESCRIPTION:
|
||||
{{.Description}}
|
||||
|
||||
TUNNEL COMMAND OPTIONS:
|
||||
See cloudflared tunnel -h
|
||||
|
||||
RUN COMMAND OPTIONS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}
|
||||
`
|
||||
return &cli.Command{
|
||||
Name: "run",
|
||||
Action: cliutil.ErrorHandler(runCommand),
|
||||
Usage: "Proxy a local web server by running the given tunnel",
|
||||
UsageText: "cloudflared tunnel [tunnel command options] run [run command options]",
|
||||
ArgsUsage: "TUNNEL",
|
||||
Description: `Runs the tunnel identified by name or UUUD, creating a highly available connection
|
||||
between your server and the Cloudflare edge.
|
||||
|
||||
This command requires the tunnel credentials file created when "cloudflared tunnel create" was run,
|
||||
however it does not need access to cert.pem from "cloudflared login". If you experience problems running
|
||||
the tunnel, "cloudflared tunnel cleanup" may help by removing any old connection records.
|
||||
|
||||
All the flags from the tunnel command are available, note that they have to be specified before the run command. There are flags defined both in tunnel and run command. The one in run command will take precedence.
|
||||
For example cloudflared tunnel --url localhost:3000 run --url localhost:5000 <TUNNEL ID> will proxy requests to localhost:5000.
|
||||
`,
|
||||
Flags: flags,
|
||||
CustomHelpTemplate: runCommandHelpTemplate,
|
||||
}
|
||||
}
|
||||
|
||||
func runCommand(c *cli.Context) error {
|
||||
sc, err := newSubcommandContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.NArg() != 1 {
|
||||
return cliutil.UsageError(`"cloudflared tunnel run" requires exactly 1 argument, the ID or name of the tunnel to run.`)
|
||||
}
|
||||
tunnelID, err := sc.findID(c.Args().First())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error parsing tunnel ID")
|
||||
}
|
||||
|
||||
return sc.run(tunnelID)
|
||||
}
|
||||
|
||||
func buildCleanupCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "cleanup",
|
||||
Action: cliutil.ErrorHandler(cleanupCommand),
|
||||
Usage: "Cleanup tunnel connections",
|
||||
ArgsUsage: "TUNNEL",
|
||||
}
|
||||
}
|
||||
|
||||
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.`)
|
||||
}
|
||||
|
||||
sc, err := newSubcommandContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tunnelIDs, err := sc.findIDs(c.Args().Slice())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sc.cleanupConnections(tunnelIDs)
|
||||
}
|
||||
|
||||
func buildRouteCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "route",
|
||||
Action: cliutil.ErrorHandler(routeCommand),
|
||||
Usage: "Define what hostname or load balancer can route to this tunnel",
|
||||
Description: `The route defines what hostname or load balancer will proxy requests to this tunnel.
|
||||
|
||||
To route a hostname by creating a CNAME to tunnel's address:
|
||||
cloudflared tunnel route dns <tunnel ID> <hostname>
|
||||
To use this tunnel as a load balancer origin, creating pool and load balancer if necessary:
|
||||
cloudflared tunnel route lb <tunnel ID> <load balancer name> <load balancer pool>`,
|
||||
ArgsUsage: "dns|lb TUNNEL HOSTNAME [LB-POOL]",
|
||||
}
|
||||
}
|
||||
|
||||
func dnsRouteFromArg(c *cli.Context) (tunnelstore.Route, error) {
|
||||
const (
|
||||
userHostnameIndex = 2
|
||||
expectedNArgs = 3
|
||||
)
|
||||
if c.NArg() != expectedNArgs {
|
||||
return nil, cliutil.UsageError("Expected %d arguments, got %d", expectedNArgs, c.NArg())
|
||||
}
|
||||
userHostname := c.Args().Get(userHostnameIndex)
|
||||
if userHostname == "" {
|
||||
return nil, cliutil.UsageError("The third argument should be the hostname")
|
||||
} else if !validateHostname(userHostname) {
|
||||
return nil, errors.Errorf("%s is not a valid hostname", userHostname)
|
||||
}
|
||||
return tunnelstore.NewDNSRoute(userHostname), nil
|
||||
}
|
||||
|
||||
func lbRouteFromArg(c *cli.Context) (tunnelstore.Route, error) {
|
||||
const (
|
||||
lbNameIndex = 2
|
||||
lbPoolIndex = 3
|
||||
expectedNArgs = 4
|
||||
)
|
||||
if c.NArg() != expectedNArgs {
|
||||
return nil, cliutil.UsageError("Expected %d arguments, got %d", expectedNArgs, c.NArg())
|
||||
}
|
||||
lbName := c.Args().Get(lbNameIndex)
|
||||
if lbName == "" {
|
||||
return nil, cliutil.UsageError("The third argument should be the load balancer name")
|
||||
} else if !validateHostname(lbName) {
|
||||
return nil, errors.Errorf("%s is not a valid load balancer name", lbName)
|
||||
}
|
||||
|
||||
lbPool := c.Args().Get(lbPoolIndex)
|
||||
if lbPool == "" {
|
||||
return nil, cliutil.UsageError("The fourth argument should be the pool name")
|
||||
} else if !validateName(lbPool) {
|
||||
return nil, errors.Errorf("%s is not a valid pool name", lbPool)
|
||||
}
|
||||
|
||||
return tunnelstore.NewLBRoute(lbName, lbPool), nil
|
||||
}
|
||||
|
||||
var nameRegex = regexp.MustCompile("^[_a-zA-Z0-9][-_.a-zA-Z0-9]*$")
|
||||
|
||||
func validateName(s string) bool {
|
||||
return nameRegex.MatchString(s)
|
||||
}
|
||||
|
||||
func validateHostname(s string) bool {
|
||||
// Slightly stricter than PunyCodeProfile
|
||||
idnaProfile := idna.New(
|
||||
idna.ValidateLabels(true),
|
||||
idna.VerifyDNSLength(true))
|
||||
|
||||
puny, err := idnaProfile.ToASCII(s)
|
||||
return err == nil && validateName(puny)
|
||||
}
|
||||
|
||||
func routeCommand(c *cli.Context) error {
|
||||
if c.NArg() < 2 {
|
||||
return cliutil.UsageError(`"cloudflared tunnel route" requires the first argument to be the route type(dns or lb), followed by the ID or name of the tunnel`)
|
||||
}
|
||||
sc, err := newSubcommandContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
const tunnelIDIndex = 1
|
||||
|
||||
routeType := c.Args().First()
|
||||
var route tunnelstore.Route
|
||||
var tunnelID uuid.UUID
|
||||
switch routeType {
|
||||
case "dns":
|
||||
tunnelID, err = sc.findID(c.Args().Get(tunnelIDIndex))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
route, err = dnsRouteFromArg(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "lb":
|
||||
tunnelID, err = sc.findID(c.Args().Get(tunnelIDIndex))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
route, err = lbRouteFromArg(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return cliutil.UsageError("%s is not a recognized route type. Supported route types are dns and lb", routeType)
|
||||
}
|
||||
|
||||
res, err := sc.route(tunnelID, route)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sc.logger.Infof(res.SuccessSummary())
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_fmtConnections(t *testing.T) {
|
||||
type args struct {
|
||||
connections []tunnelstore.Connection
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
args: args{
|
||||
connections: []tunnelstore.Connection{},
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "trivial",
|
||||
args: args{
|
||||
connections: []tunnelstore.Connection{
|
||||
{
|
||||
ColoName: "DFW",
|
||||
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "1xDFW",
|
||||
},
|
||||
{
|
||||
name: "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{
|
||||
connections: []tunnelstore.Connection{
|
||||
{
|
||||
ColoName: "YRV",
|
||||
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
|
||||
},
|
||||
{
|
||||
ColoName: "DFW",
|
||||
ID: uuid.MustParse("c13c0b3b-0fbf-453c-8169-a1990fced6d0"),
|
||||
},
|
||||
{
|
||||
ColoName: "ATL",
|
||||
ID: uuid.MustParse("70c90639-e386-4e8d-9a4e-7f046d70e63f"),
|
||||
},
|
||||
{
|
||||
ColoName: "DFW",
|
||||
ID: uuid.MustParse("30ad6251-0305-4635-a670-d3994f474981"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "1xATL, 2xDFW, 1xYRV",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := fmtConnections(tt.args.connections, false); got != tt.want {
|
||||
t.Errorf("fmtConnections() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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(tunnelID, originCertDir)
|
||||
assert.NoError(t, err)
|
||||
homeDir, err := homedir.Dir()
|
||||
assert.NoError(t, err)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -0,0 +1,236 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
type connState struct {
|
||||
location string
|
||||
state status
|
||||
}
|
||||
|
||||
type status int
|
||||
|
||||
const (
|
||||
Disconnected status = iota
|
||||
Connected
|
||||
Reconnecting
|
||||
SetUrl
|
||||
RegisteringTunnel
|
||||
)
|
||||
|
||||
type TunnelEvent struct {
|
||||
Index uint8
|
||||
EventType status
|
||||
Location string
|
||||
Url string
|
||||
}
|
||||
|
||||
type uiModel struct {
|
||||
version string
|
||||
edgeURL string
|
||||
metricsURL string
|
||||
proxyURL string
|
||||
connections []connState
|
||||
}
|
||||
|
||||
type palette struct {
|
||||
url string
|
||||
connected string
|
||||
defaultText string
|
||||
disconnected string
|
||||
reconnecting string
|
||||
}
|
||||
|
||||
func NewUIModel(version, hostname, metricsURL, proxyURL string, haConnections int) *uiModel {
|
||||
return &uiModel{
|
||||
version: version,
|
||||
edgeURL: hostname,
|
||||
metricsURL: metricsURL,
|
||||
proxyURL: proxyURL,
|
||||
connections: make([]connState, haConnections),
|
||||
}
|
||||
}
|
||||
|
||||
func (data *uiModel) LaunchUI(
|
||||
ctx context.Context,
|
||||
log logger.Service,
|
||||
logLevels []logger.Level,
|
||||
tunnelEventChan <-chan TunnelEvent,
|
||||
) {
|
||||
// Configure the logger to stream logs into the textview
|
||||
|
||||
// Add TextView as a group to write output to
|
||||
logTextView := NewDynamicColorTextView()
|
||||
log.Add(logTextView, logger.NewUIFormatter(time.RFC3339), logLevels...)
|
||||
|
||||
// Construct the UI
|
||||
palette := palette{
|
||||
url: "lightblue",
|
||||
connected: "lime",
|
||||
defaultText: "white",
|
||||
disconnected: "red",
|
||||
reconnecting: "orange",
|
||||
}
|
||||
|
||||
app := tview.NewApplication()
|
||||
|
||||
grid := tview.NewGrid().SetGap(1, 0)
|
||||
frame := tview.NewFrame(grid)
|
||||
header := fmt.Sprintf("cloudflared [::b]%s", data.version)
|
||||
|
||||
frame.AddText(header, true, tview.AlignLeft, tcell.ColorWhite)
|
||||
|
||||
// Create table to store connection info and status
|
||||
connTable := tview.NewTable()
|
||||
// SetColumns takes a value for each column, representing the size of the column
|
||||
// Numbers <= 0 represent proportional widths and positive numbers represent absolute widths
|
||||
grid.SetColumns(20, 0)
|
||||
|
||||
// SetRows takes a value for each row, representing the size of the row
|
||||
grid.SetRows(1, 1, len(data.connections), 1, 0)
|
||||
|
||||
// AddItem takes a primitive tview type, row, column, rowSpan, columnSpan, minGridHeight, minGridWidth, and focus
|
||||
grid.AddItem(tview.NewTextView().SetText("Tunnel:"), 0, 0, 1, 1, 0, 0, false)
|
||||
grid.AddItem(tview.NewTextView().SetText("Status:"), 1, 0, 1, 1, 0, 0, false)
|
||||
grid.AddItem(tview.NewTextView().SetText("Connections:"), 2, 0, 1, 1, 0, 0, false)
|
||||
|
||||
grid.AddItem(tview.NewTextView().SetText("Metrics:"), 3, 0, 1, 1, 0, 0, false)
|
||||
|
||||
tunnelHostText := tview.NewTextView().SetText(data.edgeURL)
|
||||
|
||||
grid.AddItem(tunnelHostText, 0, 1, 1, 1, 0, 0, false)
|
||||
grid.AddItem(NewDynamicColorTextView().SetText(fmt.Sprintf("[%s]\u2022[%s] Proxying to [%s::b]%s", palette.connected, palette.defaultText, palette.url, data.proxyURL)), 1, 1, 1, 1, 0, 0, false)
|
||||
|
||||
grid.AddItem(connTable, 2, 1, 1, 1, 0, 0, false)
|
||||
|
||||
grid.AddItem(NewDynamicColorTextView().SetText(fmt.Sprintf("Metrics at [%s::b]http://%s/metrics", palette.url, data.metricsURL)), 3, 1, 1, 1, 0, 0, false)
|
||||
|
||||
// Add TextView to stream logs
|
||||
// Logs are displayed in a new grid so a border can be set around them
|
||||
logGrid := tview.NewGrid().SetBorders(true).AddItem(logTextView.SetChangedFunc(handleNewText(app, logTextView)), 0, 0, 5, 2, 0, 0, false)
|
||||
// LogFrame holds the Logs header as well as the grid with the textView for streamed logs
|
||||
logFrame := tview.NewFrame(logGrid).AddText("[::b]Logs:[::-]", true, tview.AlignLeft, tcell.ColorWhite).SetBorders(0, 0, 0, 0, 0, 0)
|
||||
// Footer for log frame
|
||||
logFrame.AddText("[::d]Use Ctrl+C to exit[::-]", false, tview.AlignRight, tcell.ColorWhite)
|
||||
grid.AddItem(logFrame, 4, 0, 5, 2, 0, 0, false)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
app.Stop()
|
||||
return
|
||||
case event := <-tunnelEventChan:
|
||||
switch event.EventType {
|
||||
case Connected:
|
||||
data.setConnTableCell(event, connTable, palette)
|
||||
case Disconnected, Reconnecting:
|
||||
data.changeConnStatus(event, connTable, log, palette)
|
||||
case SetUrl:
|
||||
tunnelHostText.SetText(event.Url)
|
||||
data.edgeURL = event.Url
|
||||
case RegisteringTunnel:
|
||||
if data.edgeURL == "" {
|
||||
tunnelHostText.SetText("Registering tunnel...")
|
||||
}
|
||||
}
|
||||
}
|
||||
app.Draw()
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
if err := app.SetRoot(frame, true).Run(); err != nil {
|
||||
log.Errorf("Error launching UI: %s", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func NewDynamicColorTextView() *tview.TextView {
|
||||
return tview.NewTextView().SetDynamicColors(true)
|
||||
}
|
||||
|
||||
// Re-draws application when new logs are streamed to UI
|
||||
func handleNewText(app *tview.Application, logTextView *tview.TextView) func() {
|
||||
return func() {
|
||||
app.Draw()
|
||||
// SetFocus to enable scrolling in textview
|
||||
app.SetFocus(logTextView)
|
||||
}
|
||||
}
|
||||
|
||||
func (data *uiModel) changeConnStatus(event TunnelEvent, table *tview.Table, logger logger.Service, palette palette) {
|
||||
index := int(event.Index)
|
||||
// Get connection location and state
|
||||
connState := data.getConnState(index)
|
||||
// Check if connection is already displayed in UI
|
||||
if connState == nil {
|
||||
logger.Info("Connection is not in the UI table")
|
||||
return
|
||||
}
|
||||
|
||||
locationState := event.Location
|
||||
|
||||
if event.EventType == Disconnected {
|
||||
connState.state = Disconnected
|
||||
} else if event.EventType == Reconnecting {
|
||||
connState.state = Reconnecting
|
||||
locationState = "Reconnecting..."
|
||||
}
|
||||
|
||||
connectionNum := index + 1
|
||||
// Get table cell
|
||||
cell := table.GetCell(index, 0)
|
||||
// Change dot color in front of text as well as location state
|
||||
text := newCellText(palette, connectionNum, locationState, event.EventType)
|
||||
cell.SetText(text)
|
||||
}
|
||||
|
||||
// Return connection location and row in UI table
|
||||
func (data *uiModel) getConnState(connID int) *connState {
|
||||
if connID < len(data.connections) {
|
||||
return &data.connections[connID]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (data *uiModel) setConnTableCell(event TunnelEvent, table *tview.Table, palette palette) {
|
||||
index := int(event.Index)
|
||||
connectionNum := index + 1
|
||||
|
||||
// Update slice to keep track of connection location and state in UI table
|
||||
data.connections[index].state = Connected
|
||||
data.connections[index].location = event.Location
|
||||
|
||||
// Update text in table cell to show disconnected state
|
||||
text := newCellText(palette, connectionNum, event.Location, event.EventType)
|
||||
cell := tview.NewTableCell(text)
|
||||
table.SetCell(index, 0, cell)
|
||||
}
|
||||
|
||||
func newCellText(palette palette, connectionNum int, location string, connectedStatus status) string {
|
||||
// HA connection indicator formatted as: "• #<CONNECTION_INDEX>: <COLO>",
|
||||
// where the left middle dot's color depends on the status of the connection
|
||||
const connFmtString = "[%s]\u2022[%s] #%d: %s"
|
||||
|
||||
var dotColor string
|
||||
switch connectedStatus {
|
||||
case Connected:
|
||||
dotColor = palette.connected
|
||||
case Disconnected:
|
||||
dotColor = palette.disconnected
|
||||
case Reconnecting:
|
||||
dotColor = palette.reconnecting
|
||||
}
|
||||
|
||||
return fmt.Sprintf(connFmtString, dotColor, palette.defaultText, connectionNum, location)
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/equinox-io/equinox"
|
||||
"github.com/facebookgo/grace/gracenet"
|
||||
)
|
||||
|
||||
const (
|
||||
appID = "app_idCzgxYerVD"
|
||||
noUpdateInShellMessage = "cloudflared will not automatically update when run from the shell. To enable auto-updates, run cloudflared as a service: https://developers.cloudflare.com/argo-tunnel/reference/service/"
|
||||
noUpdateOnWindowsMessage = "cloudflared will not automatically update on Windows systems."
|
||||
)
|
||||
|
||||
var publicKey = []byte(`
|
||||
-----BEGIN ECDSA PUBLIC KEY-----
|
||||
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4OWZocTVZ8Do/L6ScLdkV+9A0IYMHoOf
|
||||
dsCmJ/QZ6aw0w9qkkwEpne1Lmo6+0pGexZzFZOH6w5amShn+RXt7qkSid9iWlzGq
|
||||
EKx0BZogHSor9Wy5VztdFaAaVbsJiCbO
|
||||
-----END ECDSA PUBLIC KEY-----
|
||||
`)
|
||||
|
||||
type ReleaseInfo struct {
|
||||
Updated bool
|
||||
Version string
|
||||
Error error
|
||||
}
|
||||
|
||||
func checkForUpdates() ReleaseInfo {
|
||||
var opts equinox.Options
|
||||
if err := opts.SetPublicKeyPEM(publicKey); err != nil {
|
||||
return ReleaseInfo{Error: err}
|
||||
}
|
||||
|
||||
resp, err := equinox.Check(appID, opts)
|
||||
switch {
|
||||
case err == equinox.NotAvailableErr:
|
||||
return ReleaseInfo{}
|
||||
case err != nil:
|
||||
return ReleaseInfo{Error: err}
|
||||
}
|
||||
|
||||
err = resp.Apply()
|
||||
if err != nil {
|
||||
return ReleaseInfo{Error: err}
|
||||
}
|
||||
|
||||
return ReleaseInfo{Updated: true, Version: resp.ReleaseVersion}
|
||||
}
|
||||
|
||||
func update(_ *cli.Context) error {
|
||||
if updateApplied() {
|
||||
os.Exit(64)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func autoupdate(freq time.Duration, listeners *gracenet.Net, shutdownC chan struct{}) error {
|
||||
tickC := time.Tick(freq)
|
||||
for {
|
||||
if updateApplied() {
|
||||
os.Args = append(os.Args, "--is-autoupdated=true")
|
||||
pid, err := listeners.StartProcess()
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Unable to restart server automatically")
|
||||
return err
|
||||
}
|
||||
// stop old process after autoupdate. Otherwise we create a new process
|
||||
// after each update
|
||||
logger.Infof("PID of the new process is %d", pid)
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-tickC:
|
||||
case <-shutdownC:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateApplied() bool {
|
||||
releaseInfo := checkForUpdates()
|
||||
if releaseInfo.Updated {
|
||||
logger.Infof("Updated to version %s", releaseInfo.Version)
|
||||
return true
|
||||
}
|
||||
if releaseInfo.Error != nil {
|
||||
logger.WithError(releaseInfo.Error).Error("Update check failed")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isAutoupdateEnabled(c *cli.Context) bool {
|
||||
if runtime.GOOS == "windows" {
|
||||
logger.Info(noUpdateOnWindowsMessage)
|
||||
return false
|
||||
}
|
||||
|
||||
if isRunningFromTerminal() {
|
||||
logger.Info(noUpdateInShellMessage)
|
||||
return false
|
||||
}
|
||||
|
||||
return !c.Bool("no-autoupdate") && c.Duration("autoupdate-freq") != 0
|
||||
}
|
||||
|
||||
func isRunningFromTerminal() bool {
|
||||
return terminal.IsTerminal(int(os.Stdout.Fd()))
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/equinox-io/equinox"
|
||||
"github.com/facebookgo/grace/gracenet"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultCheckUpdateFreq = time.Hour * 24
|
||||
appID = "app_idCzgxYerVD"
|
||||
noUpdateInShellMessage = "cloudflared will not automatically update when run from the shell. To enable auto-updates, run cloudflared as a service: https://developers.cloudflare.com/argo-tunnel/reference/service/"
|
||||
noUpdateOnWindowsMessage = "cloudflared will not automatically update on Windows systems."
|
||||
noUpdateManagedPackageMessage = "cloudflared will not automatically update if installed by a package manager."
|
||||
isManagedInstallFile = ".installedFromPackageManager"
|
||||
)
|
||||
|
||||
var (
|
||||
publicKey = []byte(`
|
||||
-----BEGIN ECDSA PUBLIC KEY-----
|
||||
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4OWZocTVZ8Do/L6ScLdkV+9A0IYMHoOf
|
||||
dsCmJ/QZ6aw0w9qkkwEpne1Lmo6+0pGexZzFZOH6w5amShn+RXt7qkSid9iWlzGq
|
||||
EKx0BZogHSor9Wy5VztdFaAaVbsJiCbO
|
||||
-----END ECDSA PUBLIC KEY-----
|
||||
`)
|
||||
)
|
||||
|
||||
// BinaryUpdated implements ExitCoder interface, the app will exit with status code 11
|
||||
// https://pkg.go.dev/github.com/urfave/cli/v2?tab=doc#ExitCoder
|
||||
type statusSuccess struct {
|
||||
newVersion string
|
||||
}
|
||||
|
||||
func (u *statusSuccess) Error() string {
|
||||
return fmt.Sprintf("cloudflared has been updated to version %s", u.newVersion)
|
||||
}
|
||||
|
||||
func (u *statusSuccess) ExitCode() int {
|
||||
return 11
|
||||
}
|
||||
|
||||
// UpdateErr implements ExitCoder interface, the app will exit with status code 10
|
||||
type statusErr struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *statusErr) Error() string {
|
||||
return fmt.Sprintf("failed to update cloudflared: %v", e.err)
|
||||
}
|
||||
|
||||
func (e *statusErr) ExitCode() int {
|
||||
return 10
|
||||
}
|
||||
|
||||
type UpdateOutcome struct {
|
||||
Updated bool
|
||||
Version string
|
||||
Error error
|
||||
}
|
||||
|
||||
func (uo *UpdateOutcome) noUpdate() bool {
|
||||
return uo.Error == nil && uo.Updated == false
|
||||
}
|
||||
|
||||
func checkForUpdateAndApply() UpdateOutcome {
|
||||
var opts equinox.Options
|
||||
if err := opts.SetPublicKeyPEM(publicKey); err != nil {
|
||||
return UpdateOutcome{Error: err}
|
||||
}
|
||||
|
||||
resp, err := equinox.Check(appID, opts)
|
||||
switch {
|
||||
case err == equinox.NotAvailableErr:
|
||||
return UpdateOutcome{}
|
||||
case err != nil:
|
||||
return UpdateOutcome{Error: err}
|
||||
}
|
||||
|
||||
err = resp.Apply()
|
||||
if err != nil {
|
||||
return UpdateOutcome{Error: err}
|
||||
}
|
||||
|
||||
return UpdateOutcome{Updated: true, Version: resp.ReleaseVersion}
|
||||
}
|
||||
|
||||
func Update(_ *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
if wasInstalledFromPackageManager() {
|
||||
logger.Error("cloudflared was installed by a package manager. Please update using the same method.")
|
||||
return nil
|
||||
}
|
||||
|
||||
updateOutcome := loggedUpdate(logger)
|
||||
if updateOutcome.Error != nil {
|
||||
return &statusErr{updateOutcome.Error}
|
||||
}
|
||||
|
||||
if updateOutcome.noUpdate() {
|
||||
logger.Infof("cloudflared is up to date (%s)", updateOutcome.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
return &statusSuccess{newVersion: updateOutcome.Version}
|
||||
}
|
||||
|
||||
// Checks for an update and applies it if one is available
|
||||
func loggedUpdate(logger logger.Service) UpdateOutcome {
|
||||
updateOutcome := checkForUpdateAndApply()
|
||||
if updateOutcome.Updated {
|
||||
logger.Infof("cloudflared has been updated to version %s", updateOutcome.Version)
|
||||
}
|
||||
if updateOutcome.Error != nil {
|
||||
logger.Errorf("update check failed: %s", updateOutcome.Error)
|
||||
}
|
||||
|
||||
return updateOutcome
|
||||
}
|
||||
|
||||
// AutoUpdater periodically checks for new version of cloudflared.
|
||||
type AutoUpdater struct {
|
||||
configurable *configurable
|
||||
listeners *gracenet.Net
|
||||
updateConfigChan chan *configurable
|
||||
logger logger.Service
|
||||
}
|
||||
|
||||
// AutoUpdaterConfigurable is the attributes of AutoUpdater that can be reconfigured during runtime
|
||||
type configurable struct {
|
||||
enabled bool
|
||||
freq time.Duration
|
||||
}
|
||||
|
||||
func NewAutoUpdater(freq time.Duration, listeners *gracenet.Net, logger logger.Service) *AutoUpdater {
|
||||
updaterConfigurable := &configurable{
|
||||
enabled: true,
|
||||
freq: freq,
|
||||
}
|
||||
if freq == 0 {
|
||||
updaterConfigurable.enabled = false
|
||||
updaterConfigurable.freq = DefaultCheckUpdateFreq
|
||||
}
|
||||
return &AutoUpdater{
|
||||
configurable: updaterConfigurable,
|
||||
listeners: listeners,
|
||||
updateConfigChan: make(chan *configurable),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AutoUpdater) Run(ctx context.Context) error {
|
||||
ticker := time.NewTicker(a.configurable.freq)
|
||||
for {
|
||||
if a.configurable.enabled {
|
||||
updateOutcome := loggedUpdate(a.logger)
|
||||
if updateOutcome.Updated {
|
||||
os.Args = append(os.Args, "--is-autoupdated=true")
|
||||
if IsSysV() {
|
||||
// SysV doesn't have a mechanism to keep service alive, we have to restart the process
|
||||
a.logger.Info("Restarting service managed by SysV...")
|
||||
pid, err := a.listeners.StartProcess()
|
||||
if err != nil {
|
||||
a.logger.Errorf("Unable to restart server automatically: %s", err)
|
||||
return &statusErr{err: err}
|
||||
}
|
||||
// stop old process after autoupdate. Otherwise we create a new process
|
||||
// after each update
|
||||
a.logger.Infof("PID of the new process is %d", pid)
|
||||
}
|
||||
return &statusSuccess{newVersion: updateOutcome.Version}
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case newConfigurable := <-a.updateConfigChan:
|
||||
ticker.Stop()
|
||||
a.configurable = newConfigurable
|
||||
ticker = time.NewTicker(a.configurable.freq)
|
||||
// Check if there is new version of cloudflared after receiving new AutoUpdaterConfigurable
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update is the method to pass new AutoUpdaterConfigurable to a running AutoUpdater. It is safe to be called concurrently
|
||||
func (a *AutoUpdater) Update(newFreq time.Duration) {
|
||||
newConfigurable := &configurable{
|
||||
enabled: true,
|
||||
freq: newFreq,
|
||||
}
|
||||
// A ero duration means autoupdate is disabled
|
||||
if newFreq == 0 {
|
||||
newConfigurable.enabled = false
|
||||
newConfigurable.freq = DefaultCheckUpdateFreq
|
||||
}
|
||||
a.updateConfigChan <- newConfigurable
|
||||
}
|
||||
|
||||
func IsAutoupdateEnabled(c *cli.Context, l logger.Service) bool {
|
||||
if !SupportAutoUpdate(l) {
|
||||
return false
|
||||
}
|
||||
return !c.Bool("no-autoupdate") && c.Duration("autoupdate-freq") != 0
|
||||
}
|
||||
|
||||
func SupportAutoUpdate(logger logger.Service) bool {
|
||||
if runtime.GOOS == "windows" {
|
||||
logger.Info(noUpdateOnWindowsMessage)
|
||||
return false
|
||||
}
|
||||
|
||||
if wasInstalledFromPackageManager() {
|
||||
logger.Info(noUpdateManagedPackageMessage)
|
||||
return false
|
||||
}
|
||||
|
||||
if isRunningFromTerminal() {
|
||||
logger.Info(noUpdateInShellMessage)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func wasInstalledFromPackageManager() bool {
|
||||
ok, _ := config.FileExists(filepath.Join(config.DefaultUnixConfigLocation, isManagedInstallFile))
|
||||
return ok
|
||||
}
|
||||
|
||||
func isRunningFromTerminal() bool {
|
||||
return terminal.IsTerminal(int(os.Stdout.Fd()))
|
||||
}
|
||||
|
||||
func IsSysV() bool {
|
||||
if runtime.GOOS != "linux" {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, err := os.Stat("/run/systemd/system"); err == nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/facebookgo/grace/gracenet"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDisabledAutoUpdater(t *testing.T) {
|
||||
listeners := &gracenet.Net{}
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
autoupdater := NewAutoUpdater(0, listeners, logger)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
errC := make(chan error)
|
||||
go func() {
|
||||
errC <- autoupdater.Run(ctx)
|
||||
}()
|
||||
|
||||
assert.False(t, autoupdater.configurable.enabled)
|
||||
assert.Equal(t, DefaultCheckUpdateFreq, autoupdater.configurable.freq)
|
||||
|
||||
cancel()
|
||||
// Make sure that autoupdater terminates after canceling the context
|
||||
assert.Equal(t, context.Canceled, <-errC)
|
||||
}
|
||||
@@ -8,10 +8,13 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/pkg/errors"
|
||||
cli "github.com/urfave/cli/v2"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/svc"
|
||||
@@ -22,6 +25,7 @@ import (
|
||||
const (
|
||||
windowsServiceName = "Cloudflared"
|
||||
windowsServiceDescription = "Argo Tunnel agent"
|
||||
windowsServiceUrl = "https://developers.cloudflare.com/argo-tunnel/reference/service/"
|
||||
|
||||
recoverActionDelay = time.Second * 20
|
||||
failureCountResetPeriod = time.Hour * 24
|
||||
@@ -29,6 +33,10 @@ const (
|
||||
// not defined in golang.org/x/sys/windows package
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms681988(v=vs.85).aspx
|
||||
serviceConfigFailureActionsFlag = 4
|
||||
|
||||
// ERROR_FAILED_SERVICE_CONTROLLER_CONNECT
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/debug/system-error-codes--1000-1299-
|
||||
serviceControllerConnectionFailure = 1063
|
||||
)
|
||||
|
||||
func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
@@ -49,44 +57,85 @@ func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
},
|
||||
})
|
||||
|
||||
// `IsAnInteractiveSession()` isn't exactly equivalent to "should the
|
||||
// process run as a normal EXE?" There are legitimate non-service cases,
|
||||
// like running cloudflared in a GCP startup script, for which
|
||||
// `IsAnInteractiveSession()` returns false. For more context, see:
|
||||
// https://github.com/judwhite/go-svc/issues/6
|
||||
// It seems that the "correct way" to check "is this a normal EXE?" is:
|
||||
// 1. attempt to connect to the Service Control Manager
|
||||
// 2. get ERROR_FAILED_SERVICE_CONTROLLER_CONNECT
|
||||
// This involves actually trying to start the service.
|
||||
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
isIntSess, err := svc.IsAnInteractiveSession()
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to determine if we are running in an interactive session: %v", err)
|
||||
}
|
||||
|
||||
if isIntSess {
|
||||
app.Run(os.Args)
|
||||
return
|
||||
}
|
||||
|
||||
elog, err := eventlog.Open(windowsServiceName)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Cannot open event log for %s", windowsServiceName)
|
||||
return
|
||||
}
|
||||
defer elog.Close()
|
||||
|
||||
elog.Info(1, fmt.Sprintf("%s service starting", windowsServiceName))
|
||||
// Run executes service name by calling windowsService which is a Handler
|
||||
// interface that implements Execute method.
|
||||
// It will set service status to stop after Execute returns
|
||||
err = svc.Run(windowsServiceName, &windowsService{app: app, elog: elog, shutdownC: shutdownC, graceShutdownC: graceShutdownC})
|
||||
err = svc.Run(windowsServiceName, &windowsService{app: app, shutdownC: shutdownC, graceShutdownC: graceShutdownC})
|
||||
if err != nil {
|
||||
elog.Error(1, fmt.Sprintf("%s service failed: %v", windowsServiceName, err))
|
||||
return
|
||||
if errno, ok := err.(syscall.Errno); ok && int(errno) == serviceControllerConnectionFailure {
|
||||
// Hack: assume this is a false negative from the IsAnInteractiveSession() check above.
|
||||
// Run the app in "interactive" mode anyway.
|
||||
app.Run(os.Args)
|
||||
return
|
||||
}
|
||||
logger.Fatalf("%s service failed: %v", windowsServiceName, err)
|
||||
}
|
||||
elog.Info(1, fmt.Sprintf("%s service stopped", windowsServiceName))
|
||||
}
|
||||
|
||||
type windowsService struct {
|
||||
app *cli.App
|
||||
elog *eventlog.Log
|
||||
shutdownC chan struct{}
|
||||
graceShutdownC chan struct{}
|
||||
}
|
||||
|
||||
// called by the package code at the start of the service
|
||||
func (s *windowsService) Execute(args []string, r <-chan svc.ChangeRequest, statusChan chan<- svc.Status) (ssec bool, errno uint32) {
|
||||
func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeRequest, statusChan chan<- svc.Status) (ssec bool, errno uint32) {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
elog, err := eventlog.Open(windowsServiceName)
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot open event log for %s with error: %s", windowsServiceName, err)
|
||||
return
|
||||
}
|
||||
defer elog.Close()
|
||||
|
||||
elog.Info(1, fmt.Sprintf("%s service starting", windowsServiceName))
|
||||
defer func() {
|
||||
elog.Info(1, fmt.Sprintf("%s service stopped", windowsServiceName))
|
||||
}()
|
||||
|
||||
// the arguments passed here are only meaningful if they were manually
|
||||
// specified by the user, e.g. using the Services console or `sc start`.
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/services/service-entry-point
|
||||
// https://stackoverflow.com/a/6235139
|
||||
var args []string
|
||||
if len(serviceArgs) > 1 {
|
||||
args = serviceArgs
|
||||
} else {
|
||||
// fall back to the arguments from ImagePath (or, as sc calls it, binPath)
|
||||
args = os.Args
|
||||
}
|
||||
elog.Info(1, fmt.Sprintf("%s service arguments: %v", windowsServiceName, args))
|
||||
|
||||
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
|
||||
statusChan <- svc.Status{State: svc.StartPending}
|
||||
errC := make(chan error)
|
||||
@@ -100,26 +149,22 @@ func (s *windowsService) Execute(args []string, r <-chan svc.ChangeRequest, stat
|
||||
case c := <-r:
|
||||
switch c.Cmd {
|
||||
case svc.Interrogate:
|
||||
s.elog.Info(1, fmt.Sprintf("control request 1 #%d", c))
|
||||
statusChan <- c.CurrentStatus
|
||||
case svc.Stop:
|
||||
s.elog.Info(1, "received stop control request")
|
||||
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:
|
||||
s.elog.Info(1, "received shutdown control request")
|
||||
close(s.shutdownC)
|
||||
statusChan <- svc.Status{State: svc.StopPending}
|
||||
return
|
||||
default:
|
||||
s.elog.Error(1, fmt.Sprintf("unexpected control request #%d", c))
|
||||
elog.Error(1, fmt.Sprintf("unexpected control request #%d", c))
|
||||
}
|
||||
case err := <-errC:
|
||||
ssec = true
|
||||
if err != nil {
|
||||
s.elog.Error(1, fmt.Sprintf("cloudflared terminated with error %v", err))
|
||||
elog.Error(1, fmt.Sprintf("cloudflared terminated with error %v", err))
|
||||
errno = 1
|
||||
} else {
|
||||
s.elog.Info(1, "cloudflared terminated without error")
|
||||
elog.Info(1, "cloudflared terminated without error")
|
||||
errno = 0
|
||||
}
|
||||
return
|
||||
@@ -128,6 +173,11 @@ func (s *windowsService) Execute(args []string, r <-chan svc.ChangeRequest, stat
|
||||
}
|
||||
|
||||
func installWindowsService(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
logger.Infof("Installing Argo Tunnel Windows service")
|
||||
exepath, err := os.Executable()
|
||||
if err != nil {
|
||||
@@ -136,7 +186,7 @@ func installWindowsService(c *cli.Context) error {
|
||||
}
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Cannot establish a connection to the service control manager")
|
||||
logger.Errorf("Cannot establish a connection to the service control manager: %s", err)
|
||||
return err
|
||||
}
|
||||
defer m.Disconnect()
|
||||
@@ -157,18 +207,23 @@ func installWindowsService(c *cli.Context) error {
|
||||
err = eventlog.InstallAsEventCreate(windowsServiceName, eventlog.Error|eventlog.Warning|eventlog.Info)
|
||||
if err != nil {
|
||||
s.Delete()
|
||||
logger.WithError(err).Errorf("Cannot install event logger")
|
||||
logger.Errorf("Cannot install event logger: %s", err)
|
||||
return fmt.Errorf("SetupEventLogSource() failed: %s", err)
|
||||
}
|
||||
err = configRecoveryOption(s.Handle)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Cannot set service recovery actions")
|
||||
logger.Infof("See %s to manually configure service recovery actions", serviceUrl)
|
||||
logger.Errorf("Cannot set service recovery actions: %s", err)
|
||||
logger.Infof("See %s to manually configure service recovery actions", windowsServiceUrl)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func uninstallWindowsService(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
logger.Infof("Uninstalling Argo Tunnel Windows Service")
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
package sqlgateway
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/elgs/gosqljson"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Connection Connection `json:"connection"`
|
||||
Command string `json:"command"`
|
||||
Params []interface{} `json:"params"`
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
SSLMode string `json:"sslmode"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Columns []string `json:"columns"`
|
||||
Rows [][]string `json:"rows"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type Proxy struct {
|
||||
Context *cli.Context
|
||||
Router *mux.Router
|
||||
Token string
|
||||
User string
|
||||
Password string
|
||||
Driver string
|
||||
Database string
|
||||
Logger *logrus.Logger
|
||||
}
|
||||
|
||||
func StartProxy(c *cli.Context, logger *logrus.Logger, password string) error {
|
||||
proxy := NewProxy(c, logger, password)
|
||||
|
||||
logger.Infof("Starting SQL Gateway Proxy on port %s", strings.Split(c.String("url"), ":")[1])
|
||||
|
||||
err := http.ListenAndServe(":"+strings.Split(c.String("url"), ":")[1], proxy.Router)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func randID(n int, c *cli.Context) string {
|
||||
charBytes := []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = charBytes[rand.Intn(len(charBytes))]
|
||||
}
|
||||
return fmt.Sprintf("%s&%s", c.String("hostname"), b)
|
||||
}
|
||||
|
||||
// db://user@dbname
|
||||
func parseInfo(input string) (string, string, string) {
|
||||
p1 := strings.Split(input, "://")
|
||||
p2 := strings.Split(p1[1], "@")
|
||||
return p1[0], p2[0], p2[1]
|
||||
}
|
||||
|
||||
func NewProxy(c *cli.Context, logger *logrus.Logger, pass string) *Proxy {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
driver, user, dbname := parseInfo(c.String("address"))
|
||||
proxy := Proxy{
|
||||
Context: c,
|
||||
Router: mux.NewRouter(),
|
||||
Token: randID(64, c),
|
||||
Logger: logger,
|
||||
User: user,
|
||||
Password: pass,
|
||||
Database: dbname,
|
||||
Driver: driver,
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf(`
|
||||
|
||||
--------------------
|
||||
SQL Gateway Proxy
|
||||
Token: %s
|
||||
--------------------
|
||||
|
||||
`, proxy.Token))
|
||||
|
||||
proxy.Router.HandleFunc("/", proxy.proxyRequest).Methods("POST")
|
||||
return &proxy
|
||||
}
|
||||
|
||||
func (proxy *Proxy) proxyRequest(rw http.ResponseWriter, req *http.Request) {
|
||||
var message Message
|
||||
response := Response{}
|
||||
|
||||
err := json.NewDecoder(req.Body).Decode(&message)
|
||||
if err != nil {
|
||||
proxy.Logger.Error(err)
|
||||
http.Error(rw, fmt.Sprintf("400 - %s", err.Error()), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if message.Connection.Token != proxy.Token {
|
||||
proxy.Logger.Error("Invalid token")
|
||||
http.Error(rw, "400 - Invalid token", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
connStr := fmt.Sprintf("user=%s password=%s dbname=%s sslmode=%s", proxy.User, proxy.Password, proxy.Database, message.Connection.SSLMode)
|
||||
|
||||
db, err := sql.Open(proxy.Driver, connStr)
|
||||
defer db.Close()
|
||||
|
||||
if err != nil {
|
||||
proxy.Logger.Error(err)
|
||||
http.Error(rw, fmt.Sprintf("400 - %s", err.Error()), http.StatusBadRequest)
|
||||
return
|
||||
|
||||
} else {
|
||||
proxy.Logger.Info("Forwarding SQL: ", message.Command)
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
|
||||
headers, data, err := gosqljson.QueryDbToArray(db, "lower", message.Command, message.Params...)
|
||||
|
||||
if err != nil {
|
||||
proxy.Logger.Error(err)
|
||||
http.Error(rw, fmt.Sprintf("400 - %s", err.Error()), http.StatusBadRequest)
|
||||
return
|
||||
|
||||
} else {
|
||||
response = Response{headers, data, ""}
|
||||
}
|
||||
}
|
||||
json.NewEncoder(rw).Encode(response)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// DialEdge makes a TLS connection to a Cloudflare edge node
|
||||
func DialEdge(
|
||||
ctx context.Context,
|
||||
timeout time.Duration,
|
||||
tlsConfig *tls.Config,
|
||||
edgeTCPAddr *net.TCPAddr,
|
||||
) (net.Conn, error) {
|
||||
// Inherit from parent context so we can cancel (Ctrl-C) while dialing
|
||||
dialCtx, dialCancel := context.WithTimeout(ctx, timeout)
|
||||
defer dialCancel()
|
||||
|
||||
dialer := net.Dialer{}
|
||||
edgeConn, err := dialer.DialContext(dialCtx, "tcp", edgeTCPAddr.String())
|
||||
if err != nil {
|
||||
return nil, newDialError(err, "DialContext error")
|
||||
}
|
||||
tlsEdgeConn := tls.Client(edgeConn, tlsConfig)
|
||||
tlsEdgeConn.SetDeadline(time.Now().Add(timeout))
|
||||
|
||||
if err = tlsEdgeConn.Handshake(); err != nil {
|
||||
return nil, newDialError(err, "TLS handshake with edge error")
|
||||
}
|
||||
// clear the deadline on the conn; h2mux has its own timeouts
|
||||
tlsEdgeConn.SetDeadline(time.Time{})
|
||||
return tlsEdgeConn, nil
|
||||
}
|
||||
|
||||
// DialError is an error returned from DialEdge
|
||||
type DialError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func newDialError(err error, message string) error {
|
||||
return DialError{cause: errors.Wrap(err, message)}
|
||||
}
|
||||
|
||||
func (e DialError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (e DialError) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing/quick"
|
||||
)
|
||||
|
||||
type mockAddrs struct {
|
||||
// a set of synthetic SRV records
|
||||
addrMap map[net.SRV][]*net.TCPAddr
|
||||
// the total number of addresses, aggregated across addrMap.
|
||||
// For the convenience of test code that would otherwise have to compute
|
||||
// this by hand every time.
|
||||
numAddrs int
|
||||
}
|
||||
|
||||
func newMockAddrs(port uint16, numRegions uint8, numAddrsPerRegion uint8) mockAddrs {
|
||||
addrMap := make(map[net.SRV][]*net.TCPAddr)
|
||||
numAddrs := 0
|
||||
|
||||
for r := uint8(0); r < numRegions; r++ {
|
||||
var (
|
||||
srv = net.SRV{Target: fmt.Sprintf("test-region-%v.example.com", r), Port: port}
|
||||
addrs []*net.TCPAddr
|
||||
)
|
||||
for a := uint8(0); a < numAddrsPerRegion; a++ {
|
||||
addrs = append(addrs, &net.TCPAddr{
|
||||
IP: net.ParseIP(fmt.Sprintf("10.0.%v.%v", r, a)),
|
||||
Port: int(port),
|
||||
})
|
||||
}
|
||||
addrMap[srv] = addrs
|
||||
numAddrs += len(addrs)
|
||||
}
|
||||
return mockAddrs{addrMap: addrMap, numAddrs: numAddrs}
|
||||
}
|
||||
|
||||
var _ quick.Generator = mockAddrs{}
|
||||
|
||||
func (mockAddrs) Generate(rand *rand.Rand, size int) reflect.Value {
|
||||
port := uint16(rand.Intn(math.MaxUint16))
|
||||
numRegions := uint8(1 + rand.Intn(10))
|
||||
numAddrsPerRegion := uint8(1 + rand.Intn(32))
|
||||
result := newMockAddrs(port, numRegions, numAddrsPerRegion)
|
||||
return reflect.ValueOf(result)
|
||||
}
|
||||
|
||||
// Returns a function compatible with net.LookupSRV that will return the SRV
|
||||
// records from mockAddrs.
|
||||
func mockNetLookupSRV(
|
||||
m mockAddrs,
|
||||
) func(service, proto, name string) (cname string, addrs []*net.SRV, err error) {
|
||||
var addrs []*net.SRV
|
||||
for k := range m.addrMap {
|
||||
addr := k
|
||||
addrs = append(addrs, &addr)
|
||||
// We can't just do
|
||||
// addrs = append(addrs, &k)
|
||||
// `k` will be reused by subsequent loop iterations,
|
||||
// so all the copies of `&k` would point to the same location.
|
||||
}
|
||||
return func(_, _, _ string) (string, []*net.SRV, error) {
|
||||
return "", addrs, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a function compatible with net.LookupIP that translates the SRV records
|
||||
// from mockAddrs into IP addresses, based on the TCP addresses in mockAddrs.
|
||||
func mockNetLookupIP(
|
||||
m mockAddrs,
|
||||
) func(host string) ([]net.IP, error) {
|
||||
return func(host string) ([]net.IP, error) {
|
||||
for srv, tcpAddrs := range m.addrMap {
|
||||
if srv.Target != host {
|
||||
continue
|
||||
}
|
||||
result := make([]net.IP, len(tcpAddrs))
|
||||
for i, tcpAddr := range tcpAddrs {
|
||||
result[i] = tcpAddr.IP
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
return nil, fmt.Errorf("No IPs for %v", host)
|
||||
}
|
||||
}
|
||||
|
||||
type mockEdgeServiceDiscoverer struct {
|
||||
}
|
||||
|
||||
func (mr *mockEdgeServiceDiscoverer) Addr() (*net.TCPAddr, error) {
|
||||
return &net.TCPAddr{
|
||||
IP: net.ParseIP("127.0.0.1"),
|
||||
Port: 63102,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (mr *mockEdgeServiceDiscoverer) AnyAddr() (*net.TCPAddr, error) {
|
||||
return &net.TCPAddr{
|
||||
IP: net.ParseIP("127.0.0.1"),
|
||||
Port: 63102,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (mr *mockEdgeServiceDiscoverer) ReplaceAddr(addr *net.TCPAddr) {}
|
||||
|
||||
func (mr *mockEdgeServiceDiscoverer) MarkAddrBad(addr *net.TCPAddr) {}
|
||||
|
||||
func (mr *mockEdgeServiceDiscoverer) AvailableAddrs() int {
|
||||
return 1
|
||||
}
|
||||
|
||||
func (mr *mockEdgeServiceDiscoverer) Refresh() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
rpc "zombiezen.com/go/capnproto2/rpc"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
// NewRPCClient creates and returns a new RPC client, which will communicate
|
||||
// using a stream on the given muxer
|
||||
func NewRPCClient(
|
||||
ctx context.Context,
|
||||
muxer *h2mux.Muxer,
|
||||
logger logger.Service,
|
||||
openStreamTimeout time.Duration,
|
||||
) (client tunnelpogs.TunnelServer_PogsClient, err error) {
|
||||
openStreamCtx, openStreamCancel := context.WithTimeout(ctx, openStreamTimeout)
|
||||
defer openStreamCancel()
|
||||
stream, err := muxer.OpenRPCStream(openStreamCtx)
|
||||
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}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func isRPCStreamResponse(headers []h2mux.Header) bool {
|
||||
return len(headers) == 1 &&
|
||||
headers[0].Name == ":status" &&
|
||||
headers[0].Value == "200"
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Client is an interface to talk to any database.
|
||||
//
|
||||
// Currently, the only implementation is SQLClient, but its structure
|
||||
// should be designed to handle a MongoClient or RedisClient in the future.
|
||||
type Client interface {
|
||||
Ping(context.Context) error
|
||||
Submit(context.Context, *Command) (interface{}, error)
|
||||
}
|
||||
|
||||
// NewClient creates a database client based on its URL scheme.
|
||||
func NewClient(ctx context.Context, originURL *url.URL) (Client, error) {
|
||||
return NewSQLClient(ctx, originURL)
|
||||
}
|
||||
|
||||
// Command is a standard, non-vendor format for submitting database commands.
|
||||
//
|
||||
// When determining the scope of this struct, refer to the following litmus test:
|
||||
// Could this (roughly) conform to SQL, Document-based, and Key-value command formats?
|
||||
type Command struct {
|
||||
Statement string `json:"statement"`
|
||||
Arguments Arguments `json:"arguments,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Isolation string `json:"isolation,omitempty"`
|
||||
Timeout time.Duration `json:"timeout,omitempty"`
|
||||
}
|
||||
|
||||
// Validate enforces the contract of Command: non empty statement (both in length and logic),
|
||||
// lowercase mode and isolation, non-zero timeout, and valid Arguments.
|
||||
func (cmd *Command) Validate() error {
|
||||
if cmd.Statement == "" {
|
||||
return fmt.Errorf("cannot provide an empty statement")
|
||||
}
|
||||
|
||||
if strings.Map(func(char rune) rune {
|
||||
if char == ';' || unicode.IsSpace(char) {
|
||||
return -1
|
||||
}
|
||||
return char
|
||||
}, cmd.Statement) == "" {
|
||||
return fmt.Errorf("cannot provide a statement with no logic: '%s'", cmd.Statement)
|
||||
}
|
||||
|
||||
cmd.Mode = strings.ToLower(cmd.Mode)
|
||||
cmd.Isolation = strings.ToLower(cmd.Isolation)
|
||||
|
||||
if cmd.Timeout.Nanoseconds() <= 0 {
|
||||
cmd.Timeout = 24 * time.Hour
|
||||
}
|
||||
|
||||
return cmd.Arguments.Validate()
|
||||
}
|
||||
|
||||
// UnmarshalJSON converts a byte representation of JSON into a Command, which is also validated.
|
||||
func (cmd *Command) UnmarshalJSON(data []byte) error {
|
||||
// Alias is required to avoid infinite recursion from the default UnmarshalJSON.
|
||||
type Alias Command
|
||||
alias := &struct {
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(cmd),
|
||||
}
|
||||
|
||||
err := json.Unmarshal(data, &alias)
|
||||
if err == nil {
|
||||
err = cmd.Validate()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Arguments is a wrapper for either map-based or array-based Command arguments.
|
||||
//
|
||||
// Each field is mutually-exclusive and some Client implementations may not
|
||||
// support both fields (eg. MySQL does not accept named arguments).
|
||||
type Arguments struct {
|
||||
Named map[string]interface{}
|
||||
Positional []interface{}
|
||||
}
|
||||
|
||||
// Validate enforces the contract of Arguments: non nil, mutually exclusive, and no empty or reserved keys.
|
||||
func (args *Arguments) Validate() error {
|
||||
if args.Named == nil {
|
||||
args.Named = map[string]interface{}{}
|
||||
}
|
||||
if args.Positional == nil {
|
||||
args.Positional = []interface{}{}
|
||||
}
|
||||
|
||||
if len(args.Named) > 0 && len(args.Positional) > 0 {
|
||||
return fmt.Errorf("both named and positional arguments cannot be specified: %+v and %+v", args.Named, args.Positional)
|
||||
}
|
||||
|
||||
for key := range args.Named {
|
||||
if key == "" {
|
||||
return fmt.Errorf("named arguments cannot contain an empty key: %+v", args.Named)
|
||||
}
|
||||
if !utf8.ValidString(key) {
|
||||
return fmt.Errorf("named argument does not conform to UTF-8 encoding: %s", key)
|
||||
}
|
||||
if strings.HasPrefix(key, "_") {
|
||||
return fmt.Errorf("named argument cannot start with a reserved keyword '_': %s", key)
|
||||
}
|
||||
if unicode.IsNumber([]rune(key)[0]) {
|
||||
return fmt.Errorf("named argument cannot start with a number: %s", key)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON converts a byte representation of JSON into Arguments, which is also validated.
|
||||
func (args *Arguments) UnmarshalJSON(data []byte) error {
|
||||
var obj interface{}
|
||||
err := json.Unmarshal(data, &obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
named, ok := obj.(map[string]interface{})
|
||||
if ok {
|
||||
args.Named = named
|
||||
} else {
|
||||
positional, ok := obj.([]interface{})
|
||||
if ok {
|
||||
args.Positional = positional
|
||||
} else {
|
||||
return fmt.Errorf("arguments must either be an object {\"0\":\"val\"} or an array [\"val\"]: %s", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
return args.Validate()
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCommandValidateEmpty(t *testing.T) {
|
||||
stmts := []string{
|
||||
"",
|
||||
";",
|
||||
" \n\t",
|
||||
";\n;\t;",
|
||||
}
|
||||
|
||||
for _, stmt := range stmts {
|
||||
cmd := Command{Statement: stmt}
|
||||
|
||||
assert.Error(t, cmd.Validate(), stmt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandValidateMode(t *testing.T) {
|
||||
modes := []string{
|
||||
"",
|
||||
"query",
|
||||
"ExEc",
|
||||
"PREPARE",
|
||||
}
|
||||
|
||||
for _, mode := range modes {
|
||||
cmd := Command{Statement: "Ok", Mode: mode}
|
||||
|
||||
assert.NoError(t, cmd.Validate(), mode)
|
||||
assert.Equal(t, strings.ToLower(mode), cmd.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandValidateIsolation(t *testing.T) {
|
||||
isos := []string{
|
||||
"",
|
||||
"default",
|
||||
"read_committed",
|
||||
"SNAPshot",
|
||||
}
|
||||
|
||||
for _, iso := range isos {
|
||||
cmd := Command{Statement: "Ok", Isolation: iso}
|
||||
|
||||
assert.NoError(t, cmd.Validate(), iso)
|
||||
assert.Equal(t, strings.ToLower(iso), cmd.Isolation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandValidateTimeout(t *testing.T) {
|
||||
cmd := Command{Statement: "Ok", Timeout: 0}
|
||||
|
||||
assert.NoError(t, cmd.Validate())
|
||||
assert.NotZero(t, cmd.Timeout)
|
||||
|
||||
cmd = Command{Statement: "Ok", Timeout: 1 * time.Second}
|
||||
|
||||
assert.NoError(t, cmd.Validate())
|
||||
assert.Equal(t, 1*time.Second, cmd.Timeout)
|
||||
}
|
||||
|
||||
func TestCommandValidateArguments(t *testing.T) {
|
||||
cmd := Command{Statement: "Ok", Arguments: Arguments{
|
||||
Named: map[string]interface{}{"key": "val"},
|
||||
Positional: []interface{}{"val"},
|
||||
}}
|
||||
|
||||
assert.Error(t, cmd.Validate())
|
||||
}
|
||||
|
||||
func TestCommandUnmarshalJSON(t *testing.T) {
|
||||
strs := []string{
|
||||
"{\"statement\":\"Ok\"}",
|
||||
"{\"statement\":\"Ok\",\"arguments\":[0, 3.14, \"apple\"],\"mode\":\"query\"}",
|
||||
"{\"statement\":\"Ok\",\"isolation\":\"read_uncommitted\",\"timeout\":1000}",
|
||||
}
|
||||
|
||||
for _, str := range strs {
|
||||
var cmd Command
|
||||
assert.NoError(t, json.Unmarshal([]byte(str), &cmd), str)
|
||||
}
|
||||
|
||||
strs = []string{
|
||||
"",
|
||||
"\"",
|
||||
"{}",
|
||||
"{\"argument\":{\"key\":\"val\"}}",
|
||||
"{\"statement\":[\"Ok\"]}",
|
||||
}
|
||||
|
||||
for _, str := range strs {
|
||||
var cmd Command
|
||||
assert.Error(t, json.Unmarshal([]byte(str), &cmd), str)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgumentsValidateNotNil(t *testing.T) {
|
||||
args := Arguments{}
|
||||
|
||||
assert.NoError(t, args.Validate())
|
||||
assert.NotNil(t, args.Named)
|
||||
assert.NotNil(t, args.Positional)
|
||||
}
|
||||
|
||||
func TestArgumentsValidateMutuallyExclusive(t *testing.T) {
|
||||
args := []Arguments{
|
||||
Arguments{},
|
||||
Arguments{Named: map[string]interface{}{"key": "val"}},
|
||||
Arguments{Positional: []interface{}{"val"}},
|
||||
}
|
||||
|
||||
for _, arg := range args {
|
||||
assert.NoError(t, arg.Validate())
|
||||
assert.False(t, len(arg.Named) > 0 && len(arg.Positional) > 0)
|
||||
}
|
||||
|
||||
args = []Arguments{
|
||||
Arguments{
|
||||
Named: map[string]interface{}{"key": "val"},
|
||||
Positional: []interface{}{"val"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, arg := range args {
|
||||
assert.Error(t, arg.Validate())
|
||||
assert.True(t, len(arg.Named) > 0 && len(arg.Positional) > 0)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgumentsValidateKeys(t *testing.T) {
|
||||
keys := []string{
|
||||
"",
|
||||
"_",
|
||||
"_key",
|
||||
"1",
|
||||
"1key",
|
||||
"\xf0\x28\x8c\xbc", // non-utf8
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
args := Arguments{Named: map[string]interface{}{key: "val"}}
|
||||
|
||||
assert.Error(t, args.Validate(), key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgumentsUnmarshalJSON(t *testing.T) {
|
||||
strs := []string{
|
||||
"{}",
|
||||
"{\"key\":\"val\"}",
|
||||
"{\"key\":[1, 3.14, {\"key\":\"val\"}]}",
|
||||
"[]",
|
||||
"[\"key\",\"val\"]",
|
||||
"[{}]",
|
||||
}
|
||||
|
||||
for _, str := range strs {
|
||||
var args Arguments
|
||||
assert.NoError(t, json.Unmarshal([]byte(str), &args), str)
|
||||
}
|
||||
|
||||
strs = []string{
|
||||
"",
|
||||
"\"",
|
||||
"1",
|
||||
"\"key\"",
|
||||
"{\"key\",\"val\"}",
|
||||
}
|
||||
|
||||
for _, str := range strs {
|
||||
var args Arguments
|
||||
assert.Error(t, json.Unmarshal([]byte(str), &args), str)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
)
|
||||
|
||||
// Cmd is the entrypoint command for dbconnect.
|
||||
//
|
||||
// The tunnel package is responsible for appending this to tunnel.Commands().
|
||||
func Cmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Category: "Database Connect (ALPHA) - Deprecated",
|
||||
Name: "db-connect",
|
||||
Usage: "deprecated: Access your SQL database from Cloudflare Workers or the browser",
|
||||
ArgsUsage: " ",
|
||||
Description: `
|
||||
This feature has been deprecated.
|
||||
Please see:
|
||||
|
||||
cloudflared access tcp --help
|
||||
|
||||
for setting up database connections to the cloudflare edge.
|
||||
|
||||
|
||||
Creates a connection between your database and the Cloudflare edge.
|
||||
Now you can execute SQL commands anywhere you can send HTTPS requests.
|
||||
|
||||
Connect your database with any of the following commands, you can also try the "playground" without a database:
|
||||
|
||||
cloudflared db-connect --hostname sql.mysite.com --url postgres://user:pass@localhost?sslmode=disable \
|
||||
--auth-domain mysite.cloudflareaccess.com --application-aud my-access-policy-tag
|
||||
cloudflared db-connect --hostname sql-dev.mysite.com --url mysql://localhost --insecure
|
||||
cloudflared db-connect --playground
|
||||
|
||||
Requests should be authenticated using Cloudflare Access, learn more about how to enable it here:
|
||||
|
||||
https://developers.cloudflare.com/access/service-auth/service-token/
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "url",
|
||||
Usage: "URL to the database (eg. postgres://user:pass@localhost?sslmode=disable)",
|
||||
EnvVars: []string{"TUNNEL_URL"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "hostname",
|
||||
Usage: "Hostname to accept commands over HTTPS (eg. sql.mysite.com)",
|
||||
EnvVars: []string{"TUNNEL_HOSTNAME"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "auth-domain",
|
||||
Usage: "Cloudflare Access authentication domain for your account (eg. mysite.cloudflareaccess.com)",
|
||||
EnvVars: []string{"TUNNEL_ACCESS_AUTH_DOMAIN"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "application-aud",
|
||||
Usage: "Cloudflare Access application \"AUD\" to verify JWTs from requests",
|
||||
EnvVars: []string{"TUNNEL_ACCESS_APPLICATION_AUD"},
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "insecure",
|
||||
Usage: "Disable authentication, the database will be open to the Internet",
|
||||
Value: false,
|
||||
EnvVars: []string{"TUNNEL_ACCESS_INSECURE"},
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "playground",
|
||||
Usage: "Run a temporary, in-memory SQLite3 database for testing",
|
||||
Value: false,
|
||||
EnvVars: []string{"TUNNEL_HELLO_WORLD"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "loglevel",
|
||||
Value: "debug", // Make it more verbose than the tunnel default 'info'.
|
||||
EnvVars: []string{"TUNNEL_LOGLEVEL"},
|
||||
Hidden: true,
|
||||
}),
|
||||
},
|
||||
Before: CmdBefore,
|
||||
Action: CmdAction,
|
||||
Hidden: true,
|
||||
}
|
||||
}
|
||||
|
||||
// CmdBefore runs some validation checks before running the command.
|
||||
func CmdBefore(c *cli.Context) error {
|
||||
// Show the help text is no flags are specified.
|
||||
if c.NumFlags() == 0 {
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
|
||||
// Hello-world and playground are synonymous with each other,
|
||||
// unset hello-world to prevent tunnel from initializing the hello package.
|
||||
if c.IsSet("hello-world") {
|
||||
c.Set("playground", "true")
|
||||
c.Set("hello-world", "false")
|
||||
}
|
||||
|
||||
// Unix-socket database urls are supported, but the logic is the same as url.
|
||||
if c.IsSet("unix-socket") {
|
||||
c.Set("url", c.String("unix-socket"))
|
||||
c.Set("unix-socket", "")
|
||||
}
|
||||
|
||||
// When playground mode is enabled, run with an in-memory database.
|
||||
if c.IsSet("playground") {
|
||||
c.Set("url", "sqlite3::memory:?cache=shared")
|
||||
c.Set("insecure", strconv.FormatBool(!c.IsSet("auth-domain") && !c.IsSet("application-aud")))
|
||||
}
|
||||
|
||||
// At this point, insecure configurations are valid.
|
||||
if c.Bool("insecure") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure that secure configurations specify a hostname, domain, and tag for JWT validation.
|
||||
if !c.IsSet("hostname") || !c.IsSet("auth-domain") || !c.IsSet("application-aud") {
|
||||
log.Fatal("must specify --hostname, --auth-domain, and --application-aud unless you want to run in --insecure mode")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CmdAction starts the Proxy and sets the url in cli.Context to point to the Proxy address.
|
||||
func CmdAction(c *cli.Context) error {
|
||||
// STOR-612: sync with context in tunnel daemon.
|
||||
ctx := context.Background()
|
||||
|
||||
var proxy *Proxy
|
||||
var err error
|
||||
if c.Bool("insecure") {
|
||||
proxy, err = NewInsecureProxy(ctx, c.String("url"))
|
||||
} else {
|
||||
proxy, err = NewSecureProxy(ctx, c.String("url"), c.String("auth-domain"), c.String("application-aud"))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
|
||||
listenerC := make(chan net.Listener)
|
||||
defer close(listenerC)
|
||||
|
||||
// Since the Proxy should only talk to the tunnel daemon, find the next available
|
||||
// localhost port and start to listen to requests.
|
||||
go func() {
|
||||
err := proxy.Start(ctx, "127.0.0.1:", listenerC)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Block until the the Proxy is online, retreive its address, and change the url to point to it.
|
||||
// This is effectively "handing over" control to the tunnel package so it can run the tunnel daemon.
|
||||
c.Set("url", "https://"+(<-listenerC).Addr().String())
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func TestCmd(t *testing.T) {
|
||||
tests := [][]string{
|
||||
{"cloudflared", "db-connect", "--playground"},
|
||||
{"cloudflared", "db-connect", "--playground", "--hostname", "sql.mysite.com"},
|
||||
{"cloudflared", "db-connect", "--url", "sqlite3::memory:?cache=shared", "--insecure"},
|
||||
{"cloudflared", "db-connect", "--url", "sqlite3::memory:?cache=shared", "--hostname", "sql.mysite.com", "--auth-domain", "mysite.cloudflareaccess.com", "--application-aud", "aud"},
|
||||
}
|
||||
|
||||
app := &cli.App{
|
||||
Name: "cloudflared",
|
||||
Commands: []*cli.Command{Cmd()},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
assert.NoError(t, app.Run(test))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/hello"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Proxy is an HTTP server that proxies requests to a Client.
|
||||
type Proxy struct {
|
||||
client Client
|
||||
accessValidator *validation.Access
|
||||
logger logger.Service
|
||||
}
|
||||
|
||||
// NewInsecureProxy creates a Proxy that talks to a Client at an origin.
|
||||
//
|
||||
// In insecure mode, the Proxy will allow all Command requests.
|
||||
func NewInsecureProxy(ctx context.Context, origin string) (*Proxy, error) {
|
||||
originURL, err := url.Parse(origin)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "must provide a valid database url")
|
||||
}
|
||||
|
||||
client, err := NewClient(ctx, originURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = client.Ping(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not connect to the database")
|
||||
}
|
||||
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
return &Proxy{client, nil, logger}, nil
|
||||
}
|
||||
|
||||
// NewSecureProxy creates a Proxy that talks to a Client at an origin.
|
||||
//
|
||||
// In secure mode, the Proxy will reject any Command requests that are
|
||||
// not authenticated by Cloudflare Access with a valid JWT.
|
||||
func NewSecureProxy(ctx context.Context, origin, authDomain, applicationAUD string) (*Proxy, error) {
|
||||
proxy, err := NewInsecureProxy(ctx, origin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
validator, err := validation.NewAccessValidator(ctx, authDomain, authDomain, applicationAUD)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy.accessValidator = validator
|
||||
|
||||
return proxy, err
|
||||
}
|
||||
|
||||
// IsInsecure gets whether the Proxy will accept a Command from any source.
|
||||
func (proxy *Proxy) IsInsecure() bool {
|
||||
return proxy.accessValidator == nil
|
||||
}
|
||||
|
||||
// IsAllowed checks whether a http.Request is allowed to receive data.
|
||||
//
|
||||
// By default, requests must pass through Cloudflare Access for authentication.
|
||||
// If the proxy is explcitly set to insecure mode, all requests will be allowed.
|
||||
func (proxy *Proxy) IsAllowed(r *http.Request, verbose ...bool) bool {
|
||||
if proxy.IsInsecure() {
|
||||
return true
|
||||
}
|
||||
|
||||
// Access and Tunnel should prevent bad JWTs from even reaching the origin,
|
||||
// but validate tokens anyway as an abundance of caution.
|
||||
err := proxy.accessValidator.ValidateRequest(r.Context(), r)
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// Warn administrators that invalid JWTs are being rejected. This is indicative
|
||||
// of either a misconfiguration of the CLI or a massive failure of upstream systems.
|
||||
if len(verbose) > 0 {
|
||||
cfRay := proxy.getRayHeader(r)
|
||||
proxy.logger.Infof("dbproxy: Failed JWT authentication: cf-ray: %s %s", cfRay, err)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Start the Proxy at a given address and notify the listener channel when the server is online.
|
||||
func (proxy *Proxy) Start(ctx context.Context, addr string, listenerC chan<- net.Listener) error {
|
||||
// STOR-611: use a seperate listener and consider web socket support.
|
||||
httpListener, err := hello.CreateTLSListener(addr)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not create listener at %s", addr)
|
||||
}
|
||||
|
||||
errC := make(chan error)
|
||||
defer close(errC)
|
||||
|
||||
// Starts the HTTP server and begins to serve requests.
|
||||
go func() {
|
||||
errC <- proxy.httpListen(ctx, httpListener)
|
||||
}()
|
||||
|
||||
// Continually ping the server until it comes online or 10 attempts fail.
|
||||
go func() {
|
||||
var err error
|
||||
for i := 0; i < 10; i++ {
|
||||
_, err = http.Get("http://" + httpListener.Addr().String())
|
||||
|
||||
// Once no error was detected, notify the listener channel and return.
|
||||
if err == nil {
|
||||
listenerC <- httpListener
|
||||
return
|
||||
}
|
||||
|
||||
// Backoff between requests to ping the server.
|
||||
<-time.After(1 * time.Second)
|
||||
}
|
||||
errC <- errors.Wrap(err, "took too long for the http server to start")
|
||||
}()
|
||||
|
||||
return <-errC
|
||||
}
|
||||
|
||||
// httpListen starts the httpServer and blocks until the context closes.
|
||||
func (proxy *Proxy) httpListen(ctx context.Context, listener net.Listener) error {
|
||||
httpServer := &http.Server{
|
||||
Addr: listener.Addr().String(),
|
||||
Handler: proxy.httpRouter(),
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 60 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
httpServer.Close()
|
||||
listener.Close()
|
||||
}()
|
||||
|
||||
return httpServer.Serve(listener)
|
||||
}
|
||||
|
||||
// httpRouter creates a mux.Router for the Proxy.
|
||||
func (proxy *Proxy) httpRouter() *mux.Router {
|
||||
router := mux.NewRouter()
|
||||
|
||||
router.HandleFunc("/ping", proxy.httpPing()).Methods("GET", "HEAD")
|
||||
router.HandleFunc("/submit", proxy.httpSubmit()).Methods("POST")
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
// httpPing tests the connection to the database.
|
||||
//
|
||||
// By default, this endpoint is unauthenticated to allow for health checks.
|
||||
// To enable authentication, Cloudflare Access must be enabled on this route.
|
||||
func (proxy *Proxy) httpPing() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
err := proxy.client.Ping(ctx)
|
||||
|
||||
if err == nil {
|
||||
proxy.httpRespond(w, r, http.StatusOK, "")
|
||||
} else {
|
||||
proxy.httpRespondErr(w, r, http.StatusInternalServerError, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// httpSubmit sends a command to the database and returns its response.
|
||||
//
|
||||
// By default, this endpoint will reject requests that do not pass through Cloudflare Access.
|
||||
// To disable authentication, the --insecure flag must be specified in the command line.
|
||||
func (proxy *Proxy) httpSubmit() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !proxy.IsAllowed(r, true) {
|
||||
proxy.httpRespondErr(w, r, http.StatusForbidden, fmt.Errorf(""))
|
||||
return
|
||||
}
|
||||
|
||||
var cmd Command
|
||||
err := json.NewDecoder(r.Body).Decode(&cmd)
|
||||
if err != nil {
|
||||
proxy.httpRespondErr(w, r, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
data, err := proxy.client.Submit(ctx, &cmd)
|
||||
|
||||
if err != nil {
|
||||
proxy.httpRespondErr(w, r, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-type", "application/json")
|
||||
err = json.NewEncoder(w).Encode(data)
|
||||
if err != nil {
|
||||
proxy.httpRespondErr(w, r, http.StatusInternalServerError, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// httpRespond writes a status code and string response to the response writer.
|
||||
func (proxy *Proxy) httpRespond(w http.ResponseWriter, r *http.Request, status int, message string) {
|
||||
w.WriteHeader(status)
|
||||
|
||||
// Only expose the message detail of the reponse if the request is not HEAD
|
||||
// and the user is authenticated. For example, this prevents an unauthenticated
|
||||
// failed health check from accidentally leaking sensitive information about the Client.
|
||||
if r.Method != http.MethodHead && proxy.IsAllowed(r) {
|
||||
if message == "" {
|
||||
message = http.StatusText(status)
|
||||
}
|
||||
fmt.Fprint(w, message)
|
||||
}
|
||||
}
|
||||
|
||||
// httpRespondErr is similar to httpRespond, except it formats errors to be more friendly.
|
||||
func (proxy *Proxy) httpRespondErr(w http.ResponseWriter, r *http.Request, defaultStatus int, err error) {
|
||||
status, err := httpError(defaultStatus, err)
|
||||
|
||||
proxy.httpRespond(w, r, status, err.Error())
|
||||
if len(err.Error()) > 0 {
|
||||
cfRay := proxy.getRayHeader(r)
|
||||
proxy.logger.Infof("dbproxy: Database proxy error: cf-ray: %s %s", cfRay, err)
|
||||
}
|
||||
}
|
||||
|
||||
// getRayHeader returns the request's Cf-ray header.
|
||||
func (proxy *Proxy) getRayHeader(r *http.Request) string {
|
||||
return r.Header.Get("Cf-ray")
|
||||
}
|
||||
|
||||
// httpError extracts common errors and returns an status code and friendly error.
|
||||
func httpError(defaultStatus int, err error) (int, error) {
|
||||
if err == nil {
|
||||
return http.StatusNotImplemented, fmt.Errorf("error expected but found none")
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
return http.StatusBadRequest, fmt.Errorf("request body cannot be empty")
|
||||
}
|
||||
|
||||
if err == context.DeadlineExceeded {
|
||||
return http.StatusRequestTimeout, err
|
||||
}
|
||||
|
||||
_, ok := err.(net.Error)
|
||||
if ok {
|
||||
return http.StatusRequestTimeout, err
|
||||
}
|
||||
|
||||
if err == context.Canceled {
|
||||
// Does not exist in Golang, but would be: http.StatusClientClosedWithoutResponse
|
||||
return 444, err
|
||||
}
|
||||
|
||||
return defaultStatus, err
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewInsecureProxy(t *testing.T) {
|
||||
origins := []string{
|
||||
"",
|
||||
":/",
|
||||
"http://localhost",
|
||||
"tcp://localhost:9000?debug=true",
|
||||
"mongodb://127.0.0.1",
|
||||
}
|
||||
|
||||
for _, origin := range origins {
|
||||
proxy, err := NewInsecureProxy(context.Background(), origin)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, proxy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyIsAllowed(t *testing.T) {
|
||||
proxy := helperNewProxy(t)
|
||||
req := httptest.NewRequest("GET", "https://1.1.1.1/ping", nil)
|
||||
assert.True(t, proxy.IsAllowed(req))
|
||||
|
||||
proxy = helperNewProxy(t, true)
|
||||
req.Header.Set("Cf-access-jwt-assertion", "xxx")
|
||||
assert.False(t, proxy.IsAllowed(req))
|
||||
}
|
||||
|
||||
func TestProxyStart(t *testing.T) {
|
||||
proxy := helperNewProxy(t)
|
||||
ctx := context.Background()
|
||||
listenerC := make(chan net.Listener)
|
||||
|
||||
err := proxy.Start(ctx, "1.1.1.1:", listenerC)
|
||||
assert.Error(t, err)
|
||||
|
||||
err = proxy.Start(ctx, "127.0.0.1:-1", listenerC)
|
||||
assert.Error(t, err)
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 0)
|
||||
defer cancel()
|
||||
|
||||
err = proxy.Start(ctx, "127.0.0.1:", listenerC)
|
||||
assert.IsType(t, http.ErrServerClosed, err)
|
||||
}
|
||||
|
||||
func TestProxyHTTPRouter(t *testing.T) {
|
||||
proxy := helperNewProxy(t)
|
||||
router := proxy.httpRouter()
|
||||
|
||||
tests := []struct {
|
||||
path string
|
||||
method string
|
||||
valid bool
|
||||
}{
|
||||
{"", "GET", false},
|
||||
{"/", "GET", false},
|
||||
{"/ping", "GET", true},
|
||||
{"/ping", "HEAD", true},
|
||||
{"/ping", "POST", false},
|
||||
{"/submit", "POST", true},
|
||||
{"/submit", "GET", false},
|
||||
{"/submit/extra", "POST", false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
match := &mux.RouteMatch{}
|
||||
ok := router.Match(httptest.NewRequest(test.method, "https://1.1.1.1"+test.path, nil), match)
|
||||
|
||||
assert.True(t, ok == test.valid, test.path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyHTTPPing(t *testing.T) {
|
||||
proxy := helperNewProxy(t)
|
||||
|
||||
server := httptest.NewServer(proxy.httpPing())
|
||||
defer server.Close()
|
||||
client := server.Client()
|
||||
|
||||
res, err := client.Get(server.URL)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
assert.Equal(t, int64(2), res.ContentLength)
|
||||
|
||||
res, err = client.Head(server.URL)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
assert.Equal(t, int64(-1), res.ContentLength)
|
||||
}
|
||||
|
||||
func TestProxyHTTPSubmit(t *testing.T) {
|
||||
proxy := helperNewProxy(t)
|
||||
|
||||
server := httptest.NewServer(proxy.httpSubmit())
|
||||
defer server.Close()
|
||||
client := server.Client()
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
status int
|
||||
output string
|
||||
}{
|
||||
{"", http.StatusBadRequest, "request body cannot be empty"},
|
||||
{"{}", http.StatusBadRequest, "cannot provide an empty statement"},
|
||||
{"{\"statement\":\"Ok\"}", http.StatusUnprocessableEntity, "cannot provide invalid sql mode: ''"},
|
||||
{"{\"statement\":\"Ok\",\"mode\":\"query\"}", http.StatusUnprocessableEntity, "near \"Ok\": syntax error"},
|
||||
{"{\"statement\":\"CREATE TABLE t (a INT);\",\"mode\":\"exec\"}", http.StatusOK, "{\"last_insert_id\":0,\"rows_affected\":0}\n"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
res, err := client.Post(server.URL, "application/json", strings.NewReader(test.input))
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.status, res.StatusCode)
|
||||
if res.StatusCode > http.StatusOK {
|
||||
assert.Equal(t, "text/plain; charset=utf-8", res.Header.Get("Content-type"))
|
||||
} else {
|
||||
assert.Equal(t, "application/json", res.Header.Get("Content-type"))
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
defer res.Body.Close()
|
||||
str := string(data)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.output, str)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyHTTPSubmitForbidden(t *testing.T) {
|
||||
proxy := helperNewProxy(t, true)
|
||||
|
||||
server := httptest.NewServer(proxy.httpSubmit())
|
||||
defer server.Close()
|
||||
client := server.Client()
|
||||
|
||||
res, err := client.Get(server.URL)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusForbidden, res.StatusCode)
|
||||
assert.Zero(t, res.ContentLength)
|
||||
}
|
||||
|
||||
func TestProxyHTTPRespond(t *testing.T) {
|
||||
proxy := helperNewProxy(t)
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
proxy.httpRespond(w, r, http.StatusAccepted, "Hello")
|
||||
}))
|
||||
defer server.Close()
|
||||
client := server.Client()
|
||||
|
||||
res, err := client.Get(server.URL)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusAccepted, res.StatusCode)
|
||||
assert.Equal(t, int64(5), res.ContentLength)
|
||||
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
defer res.Body.Close()
|
||||
assert.Equal(t, []byte("Hello"), data)
|
||||
}
|
||||
|
||||
func TestProxyHTTPRespondForbidden(t *testing.T) {
|
||||
proxy := helperNewProxy(t, true)
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
proxy.httpRespond(w, r, http.StatusAccepted, "Hello")
|
||||
}))
|
||||
defer server.Close()
|
||||
client := server.Client()
|
||||
|
||||
res, err := client.Get(server.URL)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusAccepted, res.StatusCode)
|
||||
assert.Equal(t, int64(0), res.ContentLength)
|
||||
}
|
||||
|
||||
func TestHTTPError(t *testing.T) {
|
||||
_, errTimeout := net.DialTimeout("tcp", "127.0.0.1", 0)
|
||||
assert.Error(t, errTimeout)
|
||||
|
||||
tests := []struct {
|
||||
input error
|
||||
status int
|
||||
output error
|
||||
}{
|
||||
{nil, http.StatusNotImplemented, fmt.Errorf("error expected but found none")},
|
||||
{io.EOF, http.StatusBadRequest, fmt.Errorf("request body cannot be empty")},
|
||||
{context.DeadlineExceeded, http.StatusRequestTimeout, nil},
|
||||
{context.Canceled, 444, nil},
|
||||
{errTimeout, http.StatusRequestTimeout, nil},
|
||||
{fmt.Errorf(""), http.StatusInternalServerError, nil},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
status, err := httpError(http.StatusInternalServerError, test.input)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, test.status, status)
|
||||
if test.output == nil {
|
||||
test.output = test.input
|
||||
}
|
||||
assert.Equal(t, test.output, err)
|
||||
}
|
||||
}
|
||||
|
||||
func helperNewProxy(t *testing.T, secure ...bool) *Proxy {
|
||||
t.Helper()
|
||||
|
||||
proxy, err := NewSecureProxy(context.Background(), "file::memory:?cache=shared", "test.cloudflareaccess.com", "")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, proxy)
|
||||
|
||||
if len(secure) == 0 {
|
||||
proxy.accessValidator = nil // Mark as insecure
|
||||
}
|
||||
|
||||
return proxy
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/xo/dburl"
|
||||
|
||||
// SQL drivers self-register with the database/sql package.
|
||||
// https://github.com/golang/go/wiki/SQLDrivers
|
||||
_ "github.com/denisenkom/go-mssqldb"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"github.com/kshvakov/clickhouse"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
// SQLClient is a Client that talks to a SQL database.
|
||||
type SQLClient struct {
|
||||
Dialect string
|
||||
driver *sqlx.DB
|
||||
}
|
||||
|
||||
// NewSQLClient creates a SQL client based on its URL scheme.
|
||||
func NewSQLClient(ctx context.Context, originURL *url.URL) (Client, error) {
|
||||
res, err := dburl.Parse(originURL.String())
|
||||
if err != nil {
|
||||
helpText := fmt.Sprintf("supported drivers: %+q, see documentation for more details: %s", sql.Drivers(), "https://godoc.org/github.com/xo/dburl")
|
||||
return nil, fmt.Errorf("could not parse sql database url '%s': %s\n%s", originURL, err.Error(), helpText)
|
||||
}
|
||||
|
||||
// Establishes the driver, but does not test the connection.
|
||||
driver, err := sqlx.Open(res.Driver, res.DSN)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open sql driver %s: %s\n%s", res.Driver, err.Error(), res.DSN)
|
||||
}
|
||||
|
||||
// Closes the driver, will occur when the context finishes.
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
driver.Close()
|
||||
}()
|
||||
|
||||
return &SQLClient{driver.DriverName(), driver}, nil
|
||||
}
|
||||
|
||||
// Ping verifies a connection to the database is still alive.
|
||||
func (client *SQLClient) Ping(ctx context.Context) error {
|
||||
return client.driver.PingContext(ctx)
|
||||
}
|
||||
|
||||
// Submit queries or executes a command to the SQL database.
|
||||
func (client *SQLClient) Submit(ctx context.Context, cmd *Command) (interface{}, error) {
|
||||
txx, err := cmd.ValidateSQL(client.Dialect)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, cmd.Timeout)
|
||||
defer cancel()
|
||||
|
||||
var res interface{}
|
||||
|
||||
// Get the next available sql.Conn and submit the Command.
|
||||
err = sqlConn(ctx, client.driver, txx, func(conn *sql.Conn) error {
|
||||
stmt := cmd.Statement
|
||||
args := cmd.Arguments.Positional
|
||||
|
||||
if cmd.Mode == "query" {
|
||||
res, err = sqlQuery(ctx, conn, stmt, args)
|
||||
} else {
|
||||
res, err = sqlExec(ctx, conn, stmt, args)
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// ValidateSQL extends the contract of Command for SQL dialects:
|
||||
// mode is conformed, arguments are []sql.NamedArg, and isolation is a sql.IsolationLevel.
|
||||
//
|
||||
// When the command should not be wrapped in a transaction, *sql.TxOptions and error will both be nil.
|
||||
func (cmd *Command) ValidateSQL(dialect string) (*sql.TxOptions, error) {
|
||||
err := cmd.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mode, err := sqlMode(cmd.Mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Mutates Arguments to only use positional arguments with the type sql.NamedArg.
|
||||
// This is a required by the sql.Driver before submitting arguments.
|
||||
cmd.Arguments.sql(dialect)
|
||||
|
||||
iso, err := sqlIsolation(cmd.Isolation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// When isolation is out-of-range, this is indicative that no
|
||||
// transaction should be executed and sql.TxOptions should be nil.
|
||||
if iso < sql.LevelDefault {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// In query mode, execute the transaction in read-only, unless it's Microsoft SQL
|
||||
// which does not support that type of transaction.
|
||||
readOnly := mode == "query" && dialect != "mssql"
|
||||
|
||||
return &sql.TxOptions{Isolation: iso, ReadOnly: readOnly}, nil
|
||||
}
|
||||
|
||||
// sqlConn gets the next available sql.Conn in the connection pool and runs a function to use it.
|
||||
//
|
||||
// If the transaction options are nil, run the useIt function outside a transaction.
|
||||
// This is potentially an unsafe operation if the command does not clean up its state.
|
||||
func sqlConn(ctx context.Context, driver *sqlx.DB, txx *sql.TxOptions, useIt func(*sql.Conn) error) error {
|
||||
conn, err := driver.Conn(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// If transaction options are specified, begin and defer a rollback to catch errors.
|
||||
var tx *sql.Tx
|
||||
if txx != nil {
|
||||
tx, err = conn.BeginTx(ctx, txx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
}
|
||||
|
||||
err = useIt(conn)
|
||||
|
||||
// Check if useIt was successful and a transaction exists before committing.
|
||||
if err == nil && tx != nil {
|
||||
err = tx.Commit()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// sqlQuery queries rows on a sql.Conn and returns an array of result objects.
|
||||
func sqlQuery(ctx context.Context, conn *sql.Conn, stmt string, args []interface{}) ([]map[string]interface{}, error) {
|
||||
rows, err := conn.QueryContext(ctx, stmt, args...)
|
||||
if err == nil {
|
||||
return sqlRows(rows)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// sqlExec executes a command on a sql.Conn and returns the result of the operation.
|
||||
func sqlExec(ctx context.Context, conn *sql.Conn, stmt string, args []interface{}) (sqlResult, error) {
|
||||
exec, err := conn.ExecContext(ctx, stmt, args...)
|
||||
if err == nil {
|
||||
return sqlResultFrom(exec), nil
|
||||
}
|
||||
return sqlResult{}, err
|
||||
}
|
||||
|
||||
// sql mutates Arguments to contain a positional []sql.NamedArg.
|
||||
//
|
||||
// The actual return type is []interface{} due to the native Golang
|
||||
// function signatures for sql.Exec and sql.Query being generic.
|
||||
func (args *Arguments) sql(dialect string) {
|
||||
result := args.Positional
|
||||
|
||||
for i, val := range result {
|
||||
result[i] = sqlArg("", val, dialect)
|
||||
}
|
||||
|
||||
for key, val := range args.Named {
|
||||
result = append(result, sqlArg(key, val, dialect))
|
||||
}
|
||||
|
||||
args.Positional = result
|
||||
args.Named = map[string]interface{}{}
|
||||
}
|
||||
|
||||
// sqlArg creates a sql.NamedArg from a key-value pair and an optional dialect.
|
||||
//
|
||||
// Certain dialects will need to wrap objects, such as arrays, to conform its driver requirements.
|
||||
func sqlArg(key, val interface{}, dialect string) sql.NamedArg {
|
||||
switch reflect.ValueOf(val).Kind() {
|
||||
|
||||
// PostgreSQL and Clickhouse require arrays to be wrapped before
|
||||
// being inserted into the driver interface.
|
||||
case reflect.Slice, reflect.Array:
|
||||
switch dialect {
|
||||
case "postgres":
|
||||
val = pq.Array(val)
|
||||
case "clickhouse":
|
||||
val = clickhouse.Array(val)
|
||||
}
|
||||
}
|
||||
|
||||
return sql.Named(fmt.Sprint(key), val)
|
||||
}
|
||||
|
||||
// sqlIsolation tries to match a string to a sql.IsolationLevel.
|
||||
func sqlIsolation(str string) (sql.IsolationLevel, error) {
|
||||
if str == "none" {
|
||||
return sql.IsolationLevel(-1), nil
|
||||
}
|
||||
|
||||
for iso := sql.LevelDefault; ; iso++ {
|
||||
if iso > sql.LevelLinearizable {
|
||||
return -1, fmt.Errorf("cannot provide an invalid sql isolation level: '%s'", str)
|
||||
}
|
||||
|
||||
if str == "" || strings.EqualFold(iso.String(), strings.ReplaceAll(str, "_", " ")) {
|
||||
return iso, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sqlMode tries to match a string to a command mode: 'query' or 'exec' for now.
|
||||
func sqlMode(str string) (string, error) {
|
||||
switch str {
|
||||
case "query", "exec":
|
||||
return str, nil
|
||||
default:
|
||||
return "", fmt.Errorf("cannot provide invalid sql mode: '%s'", str)
|
||||
}
|
||||
}
|
||||
|
||||
// sqlRows scans through a SQL result set and returns an array of objects.
|
||||
func sqlRows(rows *sql.Rows) ([]map[string]interface{}, error) {
|
||||
columns, err := rows.Columns()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not extract columns from result")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
types, err := rows.ColumnTypes()
|
||||
if err != nil {
|
||||
// Some drivers do not support type extraction, so fail silently and continue.
|
||||
types = make([]*sql.ColumnType, len(columns))
|
||||
}
|
||||
|
||||
values := make([]interface{}, len(columns))
|
||||
pointers := make([]interface{}, len(columns))
|
||||
|
||||
var results []map[string]interface{}
|
||||
for rows.Next() {
|
||||
for i := range columns {
|
||||
pointers[i] = &values[i]
|
||||
}
|
||||
rows.Scan(pointers...)
|
||||
|
||||
// Convert a row, an array of values, into an object where
|
||||
// each key is the name of its respective column.
|
||||
entry := make(map[string]interface{})
|
||||
for i, col := range columns {
|
||||
entry[col] = sqlValue(values[i], types[i])
|
||||
}
|
||||
results = append(results, entry)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// sqlValue handles special cases where sql.Rows does not return a "human-readable" object.
|
||||
func sqlValue(val interface{}, col *sql.ColumnType) interface{} {
|
||||
bytes, ok := val.([]byte)
|
||||
if ok {
|
||||
// Opportunistically check for embeded JSON and convert it to a first-class object.
|
||||
var embeded interface{}
|
||||
if json.Unmarshal(bytes, &embeded) == nil {
|
||||
return embeded
|
||||
}
|
||||
|
||||
// STOR-604: investigate a way to coerce PostgreSQL arrays '{a, b, ...}' into JSON.
|
||||
// Although easy with strings, it becomes more difficult with special types like INET[].
|
||||
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
// sqlResult is a thin wrapper around sql.Result.
|
||||
type sqlResult struct {
|
||||
LastInsertId int64 `json:"last_insert_id"`
|
||||
RowsAffected int64 `json:"rows_affected"`
|
||||
}
|
||||
|
||||
// sqlResultFrom converts sql.Result into a JSON-marshable sqlResult.
|
||||
func sqlResultFrom(res sql.Result) sqlResult {
|
||||
insertID, errID := res.LastInsertId()
|
||||
rowsAffected, errRows := res.RowsAffected()
|
||||
|
||||
// If an error occurs when extracting the result, it is because the
|
||||
// driver does not support that specific field. Instead of passing this
|
||||
// to the user, omit the field in the response.
|
||||
if errID != nil {
|
||||
insertID = -1
|
||||
}
|
||||
if errRows != nil {
|
||||
rowsAffected = -1
|
||||
}
|
||||
|
||||
return sqlResult{insertID, rowsAffected}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kshvakov/clickhouse"
|
||||
"github.com/lib/pq"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewSQLClient(t *testing.T) {
|
||||
originURLs := []string{
|
||||
"postgres://localhost",
|
||||
"cockroachdb://localhost:1337",
|
||||
"postgresql://user:pass@127.0.0.1",
|
||||
"mysql://localhost",
|
||||
"clickhouse://127.0.0.1:9000/?debug",
|
||||
"sqlite3::memory:",
|
||||
"file:test.db?cache=shared",
|
||||
}
|
||||
|
||||
for _, originURL := range originURLs {
|
||||
origin, _ := url.Parse(originURL)
|
||||
_, err := NewSQLClient(context.Background(), origin)
|
||||
|
||||
assert.NoError(t, err, originURL)
|
||||
}
|
||||
|
||||
originURLs = []string{
|
||||
"",
|
||||
"/",
|
||||
"http://localhost",
|
||||
"coolthing://user:pass@127.0.0.1",
|
||||
}
|
||||
|
||||
for _, originURL := range originURLs {
|
||||
origin, _ := url.Parse(originURL)
|
||||
_, err := NewSQLClient(context.Background(), origin)
|
||||
|
||||
assert.Error(t, err, originURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgumentsSQL(t *testing.T) {
|
||||
args := []Arguments{
|
||||
Arguments{
|
||||
Positional: []interface{}{
|
||||
"val", 10, 3.14,
|
||||
},
|
||||
},
|
||||
Arguments{
|
||||
Named: map[string]interface{}{
|
||||
"key": time.Unix(0, 0),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var nameType sql.NamedArg
|
||||
for _, arg := range args {
|
||||
arg.sql("")
|
||||
for _, named := range arg.Positional {
|
||||
assert.IsType(t, nameType, named)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLArg(t *testing.T) {
|
||||
tests := []struct {
|
||||
key interface{}
|
||||
val interface{}
|
||||
dialect string
|
||||
arg sql.NamedArg
|
||||
}{
|
||||
{"key", "val", "mssql", sql.Named("key", "val")},
|
||||
{0, 1, "sqlite3", sql.Named("0", 1)},
|
||||
{1, []string{"a", "b", "c"}, "postgres", sql.Named("1", pq.Array([]string{"a", "b", "c"}))},
|
||||
{"in", []uint{0, 1}, "clickhouse", sql.Named("in", clickhouse.Array([]uint{0, 1}))},
|
||||
{"", time.Unix(0, 0), "", sql.Named("", time.Unix(0, 0))},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
arg := sqlArg(test.key, test.val, test.dialect)
|
||||
assert.Equal(t, test.arg, arg, test.key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLIsolation(t *testing.T) {
|
||||
tests := []struct {
|
||||
str string
|
||||
iso sql.IsolationLevel
|
||||
}{
|
||||
{"", sql.LevelDefault},
|
||||
{"DEFAULT", sql.LevelDefault},
|
||||
{"read_UNcommitted", sql.LevelReadUncommitted},
|
||||
{"serializable", sql.LevelSerializable},
|
||||
{"none", sql.IsolationLevel(-1)},
|
||||
{"SNAP shot", -2},
|
||||
{"blah", -2},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
iso, err := sqlIsolation(test.str)
|
||||
|
||||
if test.iso < -1 {
|
||||
assert.Error(t, err, test.str)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.iso, iso, test.str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLMode(t *testing.T) {
|
||||
modes := []string{
|
||||
"query",
|
||||
"exec",
|
||||
}
|
||||
|
||||
for _, mode := range modes {
|
||||
actual, err := sqlMode(mode)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.ToLower(mode), actual, mode)
|
||||
}
|
||||
|
||||
modes = []string{
|
||||
"",
|
||||
"blah",
|
||||
}
|
||||
|
||||
for _, mode := range modes {
|
||||
_, err := sqlMode(mode)
|
||||
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func helperRows(mockRows *sqlmock.Rows) *sql.Rows {
|
||||
db, mock, _ := sqlmock.New()
|
||||
mock.ExpectQuery("SELECT").WillReturnRows(mockRows)
|
||||
rows, _ := db.Query("SELECT")
|
||||
return rows
|
||||
}
|
||||
|
||||
func TestSQLRows(t *testing.T) {
|
||||
actual, err := sqlRows(helperRows(sqlmock.NewRows(
|
||||
[]string{"name", "age", "dept"}).
|
||||
AddRow("alice", 19, "prod")))
|
||||
expected := []map[string]interface{}{map[string]interface{}{
|
||||
"name": "alice",
|
||||
"age": int64(19),
|
||||
"dept": "prod"}}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestSQLValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
input interface{}
|
||||
output interface{}
|
||||
}{
|
||||
{"hello", "hello"},
|
||||
{1, 1},
|
||||
{false, false},
|
||||
{[]byte("random"), "random"},
|
||||
{[]byte("{\"json\":true}"), map[string]interface{}{"json": true}},
|
||||
{[]byte("[]"), []interface{}{}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
assert.Equal(t, test.output, sqlValue(test.input, nil), test.input)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLResultFrom(t *testing.T) {
|
||||
res := sqlResultFrom(sqlmock.NewResult(1, 2))
|
||||
assert.Equal(t, sqlResult{1, 2}, res)
|
||||
|
||||
res = sqlResultFrom(sqlmock.NewErrorResult(fmt.Errorf("")))
|
||||
assert.Equal(t, sqlResult{-1, -1}, res)
|
||||
}
|
||||
|
||||
func helperSQLite3(t *testing.T) (context.Context, Client) {
|
||||
t.Helper()
|
||||
|
||||
ctx := context.Background()
|
||||
url, _ := url.Parse("file::memory:?cache=shared")
|
||||
|
||||
sqlite3, err := NewSQLClient(ctx, url)
|
||||
assert.NoError(t, err)
|
||||
|
||||
return ctx, sqlite3
|
||||
}
|
||||
|
||||
func TestPing(t *testing.T) {
|
||||
ctx, sqlite3 := helperSQLite3(t)
|
||||
err := sqlite3.Ping(ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSubmit(t *testing.T) {
|
||||
ctx, sqlite3 := helperSQLite3(t)
|
||||
|
||||
res, err := sqlite3.Submit(ctx, &Command{
|
||||
Statement: "CREATE TABLE t (a INTEGER, b FLOAT, c TEXT, d BLOB);",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sqlResult{0, 0}, res)
|
||||
|
||||
res, err = sqlite3.Submit(ctx, &Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "query",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, res)
|
||||
|
||||
res, err = sqlite3.Submit(ctx, &Command{
|
||||
Statement: "INSERT INTO t VALUES (?, ?, ?, ?);",
|
||||
Mode: "exec",
|
||||
Arguments: Arguments{
|
||||
Positional: []interface{}{
|
||||
1,
|
||||
3.14,
|
||||
"text",
|
||||
"blob",
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sqlResult{1, 1}, res)
|
||||
|
||||
res, err = sqlite3.Submit(ctx, &Command{
|
||||
Statement: "UPDATE t SET c = NULL;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sqlResult{1, 1}, res)
|
||||
|
||||
res, err = sqlite3.Submit(ctx, &Command{
|
||||
Statement: "SELECT * FROM t WHERE a = ?;",
|
||||
Mode: "query",
|
||||
Arguments: Arguments{
|
||||
Positional: []interface{}{1},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, res, 1)
|
||||
|
||||
resf, ok := res.([]map[string]interface{})
|
||||
assert.True(t, ok)
|
||||
assert.EqualValues(t, map[string]interface{}{
|
||||
"a": int64(1),
|
||||
"b": 3.14,
|
||||
"c": nil,
|
||||
"d": "blob",
|
||||
}, resf[0])
|
||||
|
||||
res, err = sqlite3.Submit(ctx, &Command{
|
||||
Statement: "DROP TABLE t;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sqlResult{1, 1}, res)
|
||||
}
|
||||
|
||||
func TestSubmitTransaction(t *testing.T) {
|
||||
ctx, sqlite3 := helperSQLite3(t)
|
||||
|
||||
res, err := sqlite3.Submit(ctx, &Command{
|
||||
Statement: "BEGIN;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, res)
|
||||
|
||||
res, err = sqlite3.Submit(ctx, &Command{
|
||||
Statement: "BEGIN; CREATE TABLE tt (a INT); COMMIT;",
|
||||
Mode: "exec",
|
||||
Isolation: "none",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sqlResult{0, 0}, res)
|
||||
|
||||
rows, err := sqlite3.Submit(ctx, &Command{
|
||||
Statement: "SELECT * FROM tt;",
|
||||
Mode: "query",
|
||||
Isolation: "repeatable_read",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, rows)
|
||||
}
|
||||
|
||||
func TestSubmitTimeout(t *testing.T) {
|
||||
ctx, sqlite3 := helperSQLite3(t)
|
||||
|
||||
res, err := sqlite3.Submit(ctx, &Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "query",
|
||||
Timeout: 1 * time.Nanosecond,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, res)
|
||||
}
|
||||
|
||||
func TestSubmitMode(t *testing.T) {
|
||||
ctx, sqlite3 := helperSQLite3(t)
|
||||
|
||||
res, err := sqlite3.Submit(ctx, &Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "notanoption",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, res)
|
||||
}
|
||||
|
||||
func TestSubmitEmpty(t *testing.T) {
|
||||
ctx, sqlite3 := helperSQLite3(t)
|
||||
|
||||
res, err := sqlite3.Submit(ctx, &Command{
|
||||
Statement: "; ; ; ;",
|
||||
Mode: "query",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, res)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
# docker-compose -f ./dbconnect_tests/dbconnect.yaml up --build --force-recreate --renew-anon-volumes --exit-code-from cloudflared
|
||||
|
||||
version: "2.3"
|
||||
networks:
|
||||
test-dbconnect-network:
|
||||
driver: bridge
|
||||
services:
|
||||
cloudflared:
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: dev.Dockerfile
|
||||
command: go test github.com/cloudflare/cloudflared/dbconnect_tests -v
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
mssql:
|
||||
condition: service_healthy
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DBCONNECT_INTEGRATION_TEST: "true"
|
||||
POSTGRESQL_URL: postgres://postgres:secret@postgres/db?sslmode=disable
|
||||
MYSQL_URL: mysql://root:secret@mysql/db?tls=false
|
||||
MSSQL_URL: mssql://sa:secret12345!@mssql
|
||||
CLICKHOUSE_URL: clickhouse://clickhouse:9000/db
|
||||
networks:
|
||||
- test-dbconnect-network
|
||||
postgres:
|
||||
image: postgres:11.4-alpine
|
||||
environment:
|
||||
POSTGRES_DB: db
|
||||
POSTGRES_PASSWORD: secret
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "-U", "postgres"]
|
||||
start_period: 3s
|
||||
interval: 1s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
networks:
|
||||
- test-dbconnect-network
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
environment:
|
||||
MYSQL_DATABASE: db
|
||||
MYSQL_ROOT_PASSWORD: secret
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping"]
|
||||
start_period: 3s
|
||||
interval: 1s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
networks:
|
||||
- test-dbconnect-network
|
||||
mssql:
|
||||
image: mcr.microsoft.com/mssql/server:2017-CU8-ubuntu
|
||||
environment:
|
||||
ACCEPT_EULA: "Y"
|
||||
SA_PASSWORD: secret12345!
|
||||
healthcheck:
|
||||
test: ["CMD", "/opt/mssql-tools/bin/sqlcmd", "-S", "localhost", "-U", "sa", "-P", "secret12345!", "-Q", "SELECT 1"]
|
||||
start_period: 3s
|
||||
interval: 1s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
networks:
|
||||
- test-dbconnect-network
|
||||
clickhouse:
|
||||
image: yandex/clickhouse-server:19.11
|
||||
healthcheck:
|
||||
test: ["CMD", "clickhouse-client", "--query", "SELECT 1"]
|
||||
start_period: 3s
|
||||
interval: 1s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
networks:
|
||||
- test-dbconnect-network
|
||||
@@ -0,0 +1,265 @@
|
||||
package dbconnect_tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/dbconnect"
|
||||
)
|
||||
|
||||
func TestIntegrationPostgreSQL(t *testing.T) {
|
||||
ctx, pq := helperNewSQLClient(t, "POSTGRESQL_URL")
|
||||
|
||||
err := pq.Ping(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = pq.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "CREATE TABLE t (a TEXT, b UUID, c JSON, d INET[], e SERIAL);",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = pq.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "INSERT INTO t VALUES ($1, $2, $3, $4);",
|
||||
Mode: "exec",
|
||||
Arguments: dbconnect.Arguments{
|
||||
Positional: []interface{}{
|
||||
"text",
|
||||
"6b8d686d-bd8e-43bc-b09a-cfcbbe702c10",
|
||||
"{\"bool\":true,\"array\":[\"a\", 1, 3.14],\"embed\":{\"num\":21}}",
|
||||
[]string{"1.1.1.1", "1.0.0.1"},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = pq.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "UPDATE t SET b = NULL;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err := pq.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "query",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.IsType(t, make([]map[string]interface{}, 0), res)
|
||||
|
||||
actual := res.([]map[string]interface{})[0]
|
||||
expected := map[string]interface{}{
|
||||
"a": "text",
|
||||
"b": nil,
|
||||
"c": map[string]interface{}{
|
||||
"bool": true,
|
||||
"array": []interface{}{"a", float64(1), 3.14},
|
||||
"embed": map[string]interface{}{"num": float64(21)},
|
||||
},
|
||||
"d": "{1.1.1.1,1.0.0.1}",
|
||||
"e": int64(1),
|
||||
}
|
||||
assert.EqualValues(t, expected, actual)
|
||||
|
||||
_, err = pq.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "DROP TABLE t;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestIntegrationMySQL(t *testing.T) {
|
||||
ctx, my := helperNewSQLClient(t, "MYSQL_URL")
|
||||
|
||||
err := my.Ping(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = my.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "CREATE TABLE t (a CHAR, b TINYINT, c FLOAT, d JSON, e YEAR);",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = my.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "INSERT INTO t VALUES (?, ?, ?, ?, ?);",
|
||||
Mode: "exec",
|
||||
Arguments: dbconnect.Arguments{
|
||||
Positional: []interface{}{
|
||||
"a",
|
||||
10,
|
||||
3.14,
|
||||
"{\"bool\":true}",
|
||||
2000,
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = my.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "ALTER TABLE t ADD COLUMN f GEOMETRY;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err := my.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "query",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.IsType(t, make([]map[string]interface{}, 0), res)
|
||||
|
||||
actual := res.([]map[string]interface{})[0]
|
||||
expected := map[string]interface{}{
|
||||
"a": "a",
|
||||
"b": float64(10),
|
||||
"c": 3.14,
|
||||
"d": map[string]interface{}{"bool": true},
|
||||
"e": float64(2000),
|
||||
"f": nil,
|
||||
}
|
||||
assert.EqualValues(t, expected, actual)
|
||||
|
||||
_, err = my.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "DROP TABLE t;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestIntegrationMSSQL(t *testing.T) {
|
||||
ctx, ms := helperNewSQLClient(t, "MSSQL_URL")
|
||||
|
||||
err := ms.Ping(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = ms.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "CREATE TABLE t (a BIT, b DECIMAL, c MONEY, d TEXT);",
|
||||
Mode: "exec"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = ms.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "INSERT INTO t VALUES (?, ?, ?, ?);",
|
||||
Mode: "exec",
|
||||
Arguments: dbconnect.Arguments{
|
||||
Positional: []interface{}{
|
||||
0,
|
||||
3,
|
||||
"$0.99",
|
||||
"text",
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = ms.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "UPDATE t SET d = NULL;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err := ms.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "query",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.IsType(t, make([]map[string]interface{}, 0), res)
|
||||
|
||||
actual := res.([]map[string]interface{})[0]
|
||||
expected := map[string]interface{}{
|
||||
"a": false,
|
||||
"b": float64(3),
|
||||
"c": float64(0.99),
|
||||
"d": nil,
|
||||
}
|
||||
assert.EqualValues(t, expected, actual)
|
||||
|
||||
_, err = ms.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "DROP TABLE t;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestIntegrationClickhouse(t *testing.T) {
|
||||
ctx, ch := helperNewSQLClient(t, "CLICKHOUSE_URL")
|
||||
|
||||
err := ch.Ping(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = ch.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "CREATE TABLE t (a UUID, b String, c Float64, d UInt32, e Int16, f Array(Enum8('a'=1, 'b'=2, 'c'=3))) engine=Memory;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = ch.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "INSERT INTO t VALUES (?, ?, ?, ?, ?, ?);",
|
||||
Mode: "exec",
|
||||
Arguments: dbconnect.Arguments{
|
||||
Positional: []interface{}{
|
||||
"ec65f626-6f50-4c86-9628-6314ef1edacd",
|
||||
"",
|
||||
3.14,
|
||||
314,
|
||||
-144,
|
||||
[]string{"a", "b", "c"},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err := ch.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "query",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.IsType(t, make([]map[string]interface{}, 0), res)
|
||||
|
||||
actual := res.([]map[string]interface{})[0]
|
||||
expected := map[string]interface{}{
|
||||
"a": "ec65f626-6f50-4c86-9628-6314ef1edacd",
|
||||
"b": "",
|
||||
"c": float64(3.14),
|
||||
"d": uint32(314),
|
||||
"e": int16(-144),
|
||||
"f": []string{"a", "b", "c"},
|
||||
}
|
||||
assert.EqualValues(t, expected, actual)
|
||||
|
||||
_, err = ch.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "DROP TABLE t;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func helperNewSQLClient(t *testing.T, env string) (context.Context, dbconnect.Client) {
|
||||
_, ok := os.LookupEnv("DBCONNECT_INTEGRATION_TEST")
|
||||
if ok {
|
||||
t.Helper()
|
||||
} else {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
val, ok := os.LookupEnv(env)
|
||||
if !ok {
|
||||
log.Fatalf("must provide database url as environment variable: %s", env)
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(val)
|
||||
if err != nil {
|
||||
log.Fatalf("cannot provide invalid database url: %s=%s", env, val)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
client, err := dbconnect.NewSQLClient(ctx, parsed)
|
||||
if err != nil {
|
||||
log.Fatalf("could not start test client: %s", err)
|
||||
}
|
||||
|
||||
return ctx, client
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
FROM golang:1.12 as builder
|
||||
WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
||||
RUN apt-get update
|
||||
COPY . .
|
||||
@@ -0,0 +1,135 @@
|
||||
package allregions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// Used to discover HA origintunneld servers
|
||||
srvService = "origintunneld"
|
||||
srvProto = "tcp"
|
||||
srvName = "argotunnel.com"
|
||||
|
||||
// Used to fallback to DoT when we can't use the default resolver to
|
||||
// discover HA origintunneld servers (GitHub issue #75).
|
||||
dotServerName = "cloudflare-dns.com"
|
||||
dotServerAddr = "1.1.1.1:853"
|
||||
dotTimeout = time.Duration(15 * time.Second)
|
||||
|
||||
// SRV record resolution TTL
|
||||
resolveEdgeAddrTTL = 1 * time.Hour
|
||||
|
||||
subsystemEdgeAddrResolver = "edgeAddrResolver"
|
||||
)
|
||||
|
||||
// Redeclare network functions so they can be overridden in tests.
|
||||
var (
|
||||
netLookupSRV = net.LookupSRV
|
||||
netLookupIP = net.LookupIP
|
||||
)
|
||||
|
||||
// If the call to net.LookupSRV fails, try to fall back to DoT from Cloudflare directly.
|
||||
//
|
||||
// Note: Instead of DoT, we could also have used DoH. Either of these:
|
||||
// - directly via the JSON API (https://1.1.1.1/dns-query?ct=application/dns-json&name=_origintunneld._tcp.argotunnel.com&type=srv)
|
||||
// - indirectly via `tunneldns.NewUpstreamHTTPS()`
|
||||
// But both of these cases miss out on a key feature from the stdlib:
|
||||
// "The returned records are sorted by priority and randomized by weight within a priority."
|
||||
// (https://golang.org/pkg/net/#Resolver.LookupSRV)
|
||||
// Does this matter? I don't know. It may someday. Let's use DoT so we don't need to worry about it.
|
||||
// See also: Go feature request for stdlib-supported DoH: https://github.com/golang/go/issues/27552
|
||||
var fallbackLookupSRV = lookupSRVWithDOT
|
||||
|
||||
var friendlyDNSErrorLines = []string{
|
||||
`Please try the following things to diagnose this issue:`,
|
||||
` 1. ensure that argotunnel.com is returning "origintunneld" service records.`,
|
||||
` Run your system's equivalent of: dig srv _origintunneld._tcp.argotunnel.com`,
|
||||
` 2. ensure that your DNS resolver is not returning compressed SRV records.`,
|
||||
` See GitHub issue https://github.com/golang/go/issues/27546`,
|
||||
` For example, you could use Cloudflare's 1.1.1.1 as your resolver:`,
|
||||
` https://developers.cloudflare.com/1.1.1.1/setting-up-1.1.1.1/`,
|
||||
}
|
||||
|
||||
// EdgeDiscovery implements HA service discovery lookup.
|
||||
func edgeDiscovery(logger logger.Service) ([][]*net.TCPAddr, error) {
|
||||
_, addrs, err := netLookupSRV(srvService, srvProto, srvName)
|
||||
if err != nil {
|
||||
_, fallbackAddrs, fallbackErr := fallbackLookupSRV(srvService, srvProto, srvName)
|
||||
if fallbackErr != nil || len(fallbackAddrs) == 0 {
|
||||
// use the original DNS error `err` in messages, not `fallbackErr`
|
||||
logger.Errorf("Error looking up Cloudflare edge IPs: the DNS query failed: %s", err)
|
||||
for _, s := range friendlyDNSErrorLines {
|
||||
logger.Error(s)
|
||||
}
|
||||
return nil, errors.Wrapf(err, "Could not lookup srv records on _%v._%v.%v", srvService, srvProto, srvName)
|
||||
}
|
||||
// Accept the fallback results and keep going
|
||||
addrs = fallbackAddrs
|
||||
}
|
||||
|
||||
var resolvedIPsPerCNAME [][]*net.TCPAddr
|
||||
for _, addr := range addrs {
|
||||
ips, err := resolveSRVToTCP(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resolvedIPsPerCNAME = append(resolvedIPsPerCNAME, ips)
|
||||
}
|
||||
|
||||
return resolvedIPsPerCNAME, nil
|
||||
}
|
||||
|
||||
func lookupSRVWithDOT(service, proto, name string) (cname string, addrs []*net.SRV, err error) {
|
||||
// Inspiration: https://github.com/artyom/dot/blob/master/dot.go
|
||||
r := &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, _ string, _ string) (net.Conn, error) {
|
||||
var dialer net.Dialer
|
||||
conn, err := dialer.DialContext(ctx, "tcp", dotServerAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig := &tls.Config{ServerName: dotServerName}
|
||||
return tls.Client(conn, tlsConfig), nil
|
||||
},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dotTimeout)
|
||||
defer cancel()
|
||||
return r.LookupSRV(ctx, srvService, srvProto, srvName)
|
||||
}
|
||||
|
||||
func resolveSRVToTCP(srv *net.SRV) ([]*net.TCPAddr, error) {
|
||||
ips, err := netLookupIP(srv.Target)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Couldn't resolve SRV record %v", srv)
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return nil, fmt.Errorf("SRV record %v had no IPs", srv)
|
||||
}
|
||||
addrs := make([]*net.TCPAddr, len(ips))
|
||||
for i, ip := range ips {
|
||||
addrs[i] = &net.TCPAddr{IP: ip, Port: int(srv.Port)}
|
||||
}
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
for _, addr := range addrs {
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tcpAddrs = append(tcpAddrs, tcpAddr)
|
||||
}
|
||||
return tcpAddrs, nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package allregions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEdgeDiscovery(t *testing.T) {
|
||||
mockAddrs := newMockAddrs(19, 2, 5)
|
||||
netLookupSRV = mockNetLookupSRV(mockAddrs)
|
||||
netLookupIP = mockNetLookupIP(mockAddrs)
|
||||
|
||||
expectedAddrSet := map[string]bool{}
|
||||
for _, addrs := range mockAddrs.addrMap {
|
||||
for _, addr := range addrs {
|
||||
expectedAddrSet[addr.String()] = true
|
||||
}
|
||||
}
|
||||
|
||||
l := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
addrLists, err := edgeDiscovery(l)
|
||||
assert.NoError(t, err)
|
||||
actualAddrSet := map[string]bool{}
|
||||
for _, addrs := range addrLists {
|
||||
for _, addr := range addrs {
|
||||
actualAddrSet[addr.String()] = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedAddrSet, actualAddrSet)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package allregions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing/quick"
|
||||
)
|
||||
|
||||
type mockAddrs struct {
|
||||
// a set of synthetic SRV records
|
||||
addrMap map[net.SRV][]*net.TCPAddr
|
||||
// the total number of addresses, aggregated across addrMap.
|
||||
// For the convenience of test code that would otherwise have to compute
|
||||
// this by hand every time.
|
||||
numAddrs int
|
||||
}
|
||||
|
||||
func newMockAddrs(port uint16, numRegions uint8, numAddrsPerRegion uint8) mockAddrs {
|
||||
addrMap := make(map[net.SRV][]*net.TCPAddr)
|
||||
numAddrs := 0
|
||||
|
||||
for r := uint8(0); r < numRegions; r++ {
|
||||
var (
|
||||
srv = net.SRV{Target: fmt.Sprintf("test-region-%v.example.com", r), Port: port}
|
||||
addrs []*net.TCPAddr
|
||||
)
|
||||
for a := uint8(0); a < numAddrsPerRegion; a++ {
|
||||
addrs = append(addrs, &net.TCPAddr{
|
||||
IP: net.ParseIP(fmt.Sprintf("10.0.%v.%v", r, a)),
|
||||
Port: int(port),
|
||||
})
|
||||
}
|
||||
addrMap[srv] = addrs
|
||||
numAddrs += len(addrs)
|
||||
}
|
||||
return mockAddrs{addrMap: addrMap, numAddrs: numAddrs}
|
||||
}
|
||||
|
||||
var _ quick.Generator = mockAddrs{}
|
||||
|
||||
func (mockAddrs) Generate(rand *rand.Rand, size int) reflect.Value {
|
||||
port := uint16(rand.Intn(math.MaxUint16))
|
||||
numRegions := uint8(1 + rand.Intn(10))
|
||||
numAddrsPerRegion := uint8(1 + rand.Intn(32))
|
||||
result := newMockAddrs(port, numRegions, numAddrsPerRegion)
|
||||
return reflect.ValueOf(result)
|
||||
}
|
||||
|
||||
// Returns a function compatible with net.LookupSRV that will return the SRV
|
||||
// records from mockAddrs.
|
||||
func mockNetLookupSRV(
|
||||
m mockAddrs,
|
||||
) func(service, proto, name string) (cname string, addrs []*net.SRV, err error) {
|
||||
var addrs []*net.SRV
|
||||
for k := range m.addrMap {
|
||||
addr := k
|
||||
addrs = append(addrs, &addr)
|
||||
// We can't just do
|
||||
// addrs = append(addrs, &k)
|
||||
// `k` will be reused by subsequent loop iterations,
|
||||
// so all the copies of `&k` would point to the same location.
|
||||
}
|
||||
return func(_, _, _ string) (string, []*net.SRV, error) {
|
||||
return "", addrs, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a function compatible with net.LookupIP that translates the SRV records
|
||||
// from mockAddrs into IP addresses, based on the TCP addresses in mockAddrs.
|
||||
func mockNetLookupIP(
|
||||
m mockAddrs,
|
||||
) func(host string) ([]net.IP, error) {
|
||||
return func(host string) ([]net.IP, error) {
|
||||
for srv, tcpAddrs := range m.addrMap {
|
||||
if srv.Target != host {
|
||||
continue
|
||||
}
|
||||
result := make([]net.IP, len(tcpAddrs))
|
||||
for i, tcpAddr := range tcpAddrs {
|
||||
result[i] = tcpAddr.IP
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
return nil, fmt.Errorf("No IPs for %v", host)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package allregions
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
// Region contains cloudflared edge addresses. The edge is partitioned into several regions for
|
||||
// redundancy purposes.
|
||||
type Region struct {
|
||||
connFor map[*net.TCPAddr]UsedBy
|
||||
}
|
||||
|
||||
// NewRegion creates a region with the given addresses, which are all unused.
|
||||
func NewRegion(addrs []*net.TCPAddr) Region {
|
||||
// The zero value of UsedBy is Unused(), so we can just initialize the map's values with their
|
||||
// zero values.
|
||||
m := make(map[*net.TCPAddr]UsedBy)
|
||||
for _, addr := range addrs {
|
||||
m[addr] = Unused()
|
||||
}
|
||||
return Region{connFor: m}
|
||||
}
|
||||
|
||||
// AddrUsedBy finds the address used by the given connection in this region.
|
||||
// Returns nil if the connection isn't using any IP.
|
||||
func (r *Region) AddrUsedBy(connID int) *net.TCPAddr {
|
||||
for addr, used := range r.connFor {
|
||||
if used.Used && used.ConnID == connID {
|
||||
return addr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AvailableAddrs counts how many unused addresses this region contains.
|
||||
func (r Region) AvailableAddrs() int {
|
||||
n := 0
|
||||
for _, usedby := range r.connFor {
|
||||
if !usedby.Used {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// GetUnusedIP returns a random unused address in this region.
|
||||
// Returns nil if all addresses are in use.
|
||||
func (r Region) GetUnusedIP(excluding *net.TCPAddr) *net.TCPAddr {
|
||||
for addr, usedby := range r.connFor {
|
||||
if !usedby.Used && addr != excluding {
|
||||
return addr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use the address, assigning it to a proxy connection.
|
||||
func (r Region) Use(addr *net.TCPAddr, connID int) {
|
||||
if addr == nil {
|
||||
//logrus.Errorf("Attempted to use nil address for connection %d", connID)
|
||||
return
|
||||
}
|
||||
r.connFor[addr] = InUse(connID)
|
||||
}
|
||||
|
||||
// GetAnyAddress returns an arbitrary address from the region.
|
||||
func (r Region) GetAnyAddress() *net.TCPAddr {
|
||||
for addr := range r.connFor {
|
||||
return addr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GiveBack the address, ensuring it is no longer assigned to an IP.
|
||||
// Returns true if the address is in this region.
|
||||
func (r Region) GiveBack(addr *net.TCPAddr) (ok bool) {
|
||||
if _, ok := r.connFor[addr]; !ok {
|
||||
return false
|
||||
}
|
||||
r.connFor[addr] = Unused()
|
||||
return true
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user