mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-08-07 15:24:46 +00:00
Compare commits
566 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b12e74099 | |||
| b1edf5b96d | |||
| d07d24e5a2 | |||
| 0ab6867ae5 | |||
| ed2bac026d | |||
| e09dcf6d60 | |||
| 76fb329a65 | |||
| 7bac4b15b0 | |||
| 8a5343d0a5 | |||
| a84cbcde7e | |||
| 1a92f1acfe | |||
| c196679bc7 | |||
| 10fc450ae5 | |||
| 74556bcd7d | |||
| 97309d81ab | |||
| 0292727a95 | |||
| f33897615d | |||
| 6fa58aadba | |||
| ef3152f334 | |||
| d6036d96f0 | |||
| a6faa0c376 | |||
| 1086d5ede5 | |||
| c314d58b69 | |||
| 628545d229 | |||
| ead93e9f26 | |||
| 5f380f3a54 | |||
| 7814e870a7 | |||
| 7c7cf688e6 | |||
| a39d95d5f7 | |||
| 01ad2785ee | |||
| 6822e4f8ab | |||
| 834c0eaeed | |||
| 74a3026963 | |||
| 8445b88d3c | |||
| 7a55208c61 | |||
| 581cfb8480 | |||
| 201c462902 | |||
| ebae7a7024 | |||
| 70e675f42c | |||
| 8f46065ab5 | |||
| 41b9c22216 | |||
| 88ce63e785 | |||
| 2dc5f6ec8c | |||
| 8d41f99f2f | |||
| 173190aa79 | |||
| 9251b3aa1f | |||
| b0e27d1eac | |||
| 73a265f2fc | |||
| 9bc59bc78c | |||
| b73c588254 | |||
| 7e47667b08 | |||
| eea3d11e40 | |||
| dd32dc1364 | |||
| fc2333c934 | |||
| 571380b3f5 | |||
| eec6b87eea | |||
| 6cc7d99e32 | |||
| 59bbd51065 | |||
| e35f744b36 | |||
| a96d4243ba | |||
| d4733efb25 | |||
| d1be558ca7 | |||
| b88e0bc8f8 | |||
| 197a70c9c4 | |||
| e71b88fcaa | |||
| 157f5d1412 | |||
| 7024d193c9 | |||
| 794635fb54 | |||
| 1ee540a166 | |||
| 6bcc9a76e9 | |||
| 43f1c6ba82 | |||
| 0146a8d8ed | |||
| 36479ef11f | |||
| 58538619ea | |||
| 573d410606 | |||
| f6f10305a6 | |||
| 588f1a03c4 | |||
| f8fbbcd806 | |||
| 2ca4633f89 | |||
| 2ce11a20c4 | |||
| ff7c48568c | |||
| 958650be1f | |||
| eb51ff0a6d | |||
| 3f4407ce27 | |||
| d9636c73b4 | |||
| 6cbf90883d | |||
| 76ade67f95 | |||
| 997f2cf612 | |||
| ceb509ee98 | |||
| 5a3c0fdffa | |||
| 2822fbe3db | |||
| 5148d00516 | |||
| bb10e1dee5 | |||
| e445fd92f7 | |||
| bccf4a63dc | |||
| 7059ef8e13 | |||
| 1239006e96 | |||
| cbdf88ea28 | |||
| 79ebfa8304 | |||
| 5a5e49179a | |||
| 470a85e65d | |||
| d7da74cb9e | |||
| 27e1277a3b | |||
| 6238fd9022 | |||
| f985ed567f | |||
| d54c8cc745 | |||
| 548e85829a | |||
| fd14bf440b | |||
| e2b18364f4 | |||
| 52c4f875fa | |||
| dac077fef1 | |||
| 0e68b1d24e | |||
| 6968b714d0 | |||
| 9310241e1a | |||
| 1cb22817db | |||
| fe4b9b1535 | |||
| d04f48d872 | |||
| b5f6559179 | |||
| 89d408e3bd | |||
| 533d005159 | |||
| 98c3957d30 | |||
| 671754fd19 | |||
| d1801776b0 | |||
| 11c06b5a1f | |||
| 27cd83c2d3 | |||
| 75bdc96763 | |||
| c51879b17f | |||
| 738b4f8d25 | |||
| c7a44009d3 | |||
| 754ad59512 | |||
| 414cb12f02 | |||
| d0a1daac3b | |||
| 2afa307765 | |||
| a4a9f45b0a | |||
| 1da4fbbe0b | |||
| 836149a5b0 | |||
| 4ff215ee8c | |||
| 17e3073a17 | |||
| 071d595371 | |||
| b6c85401a5 | |||
| 3ef3e7a99a | |||
| 12ad264eb3 | |||
| 1082ac1c36 | |||
| a233f975c1 | |||
| 5f6e867685 | |||
| 78a9454023 | |||
| ca85df10ff | |||
| b8333b44a2 | |||
| e49a7a4389 | |||
| 5749425671 | |||
| fac9dfb6e5 | |||
| d9ec18314d | |||
| ed024d0741 | |||
| fd4000184c | |||
| bcc847963e | |||
| 1e8dea9112 | |||
| cd4af5696d | |||
| 8527d03a29 | |||
| fa8aa02270 | |||
| aa24338225 | |||
| 0924549efd | |||
| 67a3be5b7a | |||
| c33a516663 | |||
| ed1389ef08 | |||
| 8fb6508ae6 | |||
| ee8c8bd4c6 | |||
| 0269062b24 | |||
| bccf650165 | |||
| cecc74d3fb | |||
| 18992efa0c | |||
| bd8af7d80d | |||
| dff694b218 | |||
| 38af26e232 | |||
| 8f3526289a | |||
| 81dff44bb9 | |||
| 6e45e0d53b | |||
| a7d2de1e12 | |||
| 59cae0f622 | |||
| d678584d89 | |||
| f1b57526b3 | |||
| 3eb9efd9f0 | |||
| 8d99e92852 | |||
| 31ff7caeeb | |||
| 5d84874d53 | |||
| b06fe0fc5f | |||
| bf38e5aa12 | |||
| bf068e728f | |||
| f88732277a | |||
| a11c64b091 | |||
| e404c29edb | |||
| 98a0844f56 | |||
| d6d65d183b | |||
| 951d13d76c | |||
| f99ae90ca1 | |||
| e99d0b5bc4 | |||
| 48c5721bc6 | |||
| 235897ba21 | |||
| bf737a0efc | |||
| 4c5ebccacc | |||
| a3153c6add | |||
| a2bbe1bdc2 | |||
| 6526211a69 | |||
| 103b2eca00 | |||
| 14ea6298bc | |||
| b297e8bb90 | |||
| 6cc8c39fa8 | |||
| 4d4eedd1c0 | |||
| a00eda9538 | |||
| 9ff65c1e90 | |||
| 2dcf3bd011 | |||
| ae460b340b | |||
| bc54a7f87b | |||
| 75c3ca2f4a | |||
| 4bd17766a9 | |||
| b87cb9aee8 | |||
| 07af2a33b7 | |||
| 209091da39 | |||
| 4d43a70a38 | |||
| 5b35e968f3 | |||
| f3b0b33dc5 | |||
| aca0c93461 | |||
| eed7d7bbc9 | |||
| 1073f8db40 | |||
| e45695d4a3 | |||
| 7ce0aefea4 | |||
| 3ad99b241c | |||
| b25d38dd72 | |||
| 1720ac0fc6 | |||
| 0929e5f0ff | |||
| 36787d9cf7 | |||
| 5afa3251dd | |||
| 8ca0d86c85 | |||
| ebf5292bf9 | |||
| f9062ab473 | |||
| d14f3b39a7 | |||
| fd0529748a | |||
| 9d3a7bd08e | |||
| 1cf6ae37eb | |||
| 66da530ba3 | |||
| e9167f7f58 | |||
| 4494a27fab | |||
| 63833b07dd | |||
| da4d0b2bae | |||
| 027168c23a | |||
| 50435546c5 | |||
| 9018ee5d5e | |||
| 12447677cf | |||
| 6106737281 | |||
| 8250b67a9f | |||
| db5c6f2556 | |||
| 9dd7898792 | |||
| 92b3e8311f | |||
| 4a7763e497 | |||
| 9767ba1853 | |||
| 2c75326021 | |||
| 9023daba24 | |||
| 89d0e45d62 | |||
| a34099724e | |||
| 8c5498fad1 | |||
| 2c746b3361 | |||
| 954cd6adca | |||
| 8432735867 | |||
| d67fbbf94f | |||
| 39901e1d60 | |||
| 9df60276a9 | |||
| 6a9ba61242 | |||
| 848c44bd0b | |||
| 9f84706eae | |||
| 841344f1e7 | |||
| 25cfbec072 | |||
| f23e33c082 | |||
| d22b374208 | |||
| d6e867d4d1 | |||
| a7344435a5 | |||
| 206523344f | |||
| b0e69c4b8a | |||
| aa5ebb817a | |||
| 8e340d9598 | |||
| 4296b23087 | |||
| 452f8cef79 | |||
| 39065377b5 | |||
| d83d6d54ed | |||
| a2b41ea3e6 | |||
| 4481b9e46c | |||
| e5d6f969be | |||
| ded9dec4f0 | |||
| 89b738f8fa | |||
| 4f88982584 | |||
| bcd71b56e9 | |||
| 5c7b451e17 | |||
| b73c039070 | |||
| 53a69a228a | |||
| eda3a7a305 | |||
| f1ca2de515 | |||
| 27507ab192 | |||
| 6db934853d | |||
| 792520d313 | |||
| 8b9cfcde78 | |||
| 5ba3b3b309 | |||
| 63a29f421a | |||
| e20c4f8752 | |||
| ab4dda5427 | |||
| 5943808746 | |||
| ed57ee64e8 | |||
| 9c298e4851 | |||
| 8b794390e5 | |||
| a6c2348127 | |||
| 6681d179dc | |||
| 2146f71b45 | |||
| 3b93914612 | |||
| b4700a52e3 | |||
| 368066a966 | |||
| e2262085e5 | |||
| 5e2b43adb5 | |||
| c7dca16300 | |||
| 9d5bd256be | |||
| e9c2afec56 | |||
| 4e33281337 | |||
| ac559f86c9 | |||
| 117766562b | |||
| 5d76e940c7 | |||
| 1670ee87fb | |||
| a8ae6de213 | |||
| d7c4a89106 | |||
| e7354f4768 | |||
| 18ec338d4c | |||
| 7f97e2f030 | |||
| a278753bbf | |||
| 3004703074 | |||
| 67680f5536 | |||
| d8bee0b4d9 | |||
| a4f185fd28 | |||
| cf562ef8c8 | |||
| 820e0dfe51 | |||
| 0b16a473da | |||
| dbd90f270e | |||
| 7420439ed2 | |||
| 352207e933 | |||
| 5eb2874a13 | |||
| dca77ee13e | |||
| 0d22106416 | |||
| 88b53eb886 | |||
| b72ee69eef | |||
| 8a020d12e1 | |||
| a945518404 | |||
| 6cdd20e820 | |||
| d87bfcbe55 | |||
| 0df4f7dd24 | |||
| 7df3a1ab67 | |||
| c4fbb05c1b | |||
| d503aeaf77 | |||
| db0562c7b8 | |||
| ce22dd681a | |||
| a129572749 | |||
| 2d0b86f2e4 | |||
| ffac598fab | |||
| 4a76ed12e7 | |||
| 04b1e4f859 | |||
| 8c9d725eeb | |||
| de27361ffa | |||
| 8da61274b8 | |||
| 146c2d944a | |||
| d90a111c1d | |||
| d26df1c248 | |||
| 42cdb557a0 | |||
| 7c3ceeeaef | |||
| 391facbedf | |||
| 1c9f3ac7d4 | |||
| 6852047ef1 | |||
| a2109e4a78 | |||
| 01f0d67875 | |||
| 33d9b4027b | |||
| 9ed536c990 | |||
| 119d96cdc1 | |||
| 94ca4f98dd | |||
| b601b24f52 | |||
| b40d8557cf | |||
| a34604cfc7 | |||
| 1c1a8f21b2 | |||
| 78ffb1b846 | |||
| 92fd039440 | |||
| 55bf904689 | |||
| abab78730d | |||
| b855e33327 | |||
| 32336859f8 | |||
| 94c639d225 | |||
| 2ea491b1d0 | |||
| 9bc1c0c70b | |||
| 870f5fa907 | |||
| 06404bf3e8 | |||
| 3b4ed70cf3 | |||
| d45ca67498 | |||
| 1c0dac77d7 | |||
| 65e70650dc | |||
| 38fb0b28b6 | |||
| bda8fe2fbe | |||
| 6a8c7c0727 | |||
| 69fd502db3 | |||
| fcc393e2f0 | |||
| cad58b9b57 | |||
| 78cb60b85f | |||
| a08a7030d1 | |||
| 87203bbe25 | |||
| 11acb50cf7 | |||
| 23f2a04ed7 | |||
| 1805261263 | |||
| 53de779a0a | |||
| b7e91466f5 | |||
| 4c1b89576c | |||
| a1a554a29d | |||
| fdb1f961b3 | |||
| 293b9af4a7 | |||
| 029f7e0378 | |||
| 58c5e25b9a | |||
| 25e72f7760 | |||
| 7613410855 | |||
| c40cb7dc56 | |||
| 9ae5f306bf | |||
| 6159cb536f | |||
| ae4f687754 | |||
| ce7d0572fe | |||
| 030b768eeb | |||
| f36dc6cfd8 | |||
| 6dad2bf9c4 | |||
| 1475cf61ee | |||
| ebc003d478 | |||
| 543169c893 | |||
| d5769519b2 | |||
| 5974fb4cfd | |||
| eef5b78eac | |||
| 6b86f81c4a | |||
| a490443630 | |||
| b5cdf3b2c7 | |||
| 6886e5f90a | |||
| 9ac40dcf04 | |||
| a5a5b93b64 | |||
| cb39f26f27 | |||
| 8d7b2575ba | |||
| d7498b0c03 | |||
| 59051ba425 | |||
| d7268af555 | |||
| 8c1deb4064 | |||
| 986102401b | |||
| 196762d9d3 | |||
| 350a6f2bf5 | |||
| 3de3b2f734 | |||
| 4698ec8dee | |||
| 8c6181db9f | |||
| 64d3836645 | |||
| 87e2679744 | |||
| 61c814bd79 | |||
| be9a558867 | |||
| b00bfe8e0b | |||
| bc015995d8 | |||
| d01770107e | |||
| 18c359cb86 | |||
| e933ef9e1a | |||
| ea71b78e6d | |||
| 04fa10ea7b | |||
| 8e0be7cff1 | |||
| 3c13080fe1 | |||
| b6cd54d854 | |||
| f0cfad8efa | |||
| ed54d150fe | |||
| ca4887fb19 | |||
| eaf03305bd | |||
| 051908aaef | |||
| acd03e36e6 | |||
| c96b9e8d8f | |||
| 4a4a1bb6b1 | |||
| 0eebc7cef9 | |||
| 53a1fa46a8 | |||
| 86a7af3dc4 | |||
| 407c9550d7 | |||
| 2319003e10 | |||
| 1e6399c2f0 | |||
| b05d826d22 | |||
| d21989dba4 | |||
| a986355a61 | |||
| 812244d79f | |||
| be7b7c7149 | |||
| 03d7320a44 | |||
| ba4c8d8849 | |||
| 2c9b7361b7 | |||
| 8e8513e325 | |||
| 607dcff697 | |||
| 1d1cf6a827 | |||
| 61f3fab757 | |||
| e2ff7f65fc | |||
| da0792a321 | |||
| fa061ab54e | |||
| 197d65659a | |||
| 85d0afd3b0 | |||
| 20623255dd | |||
| afa5e68fe5 | |||
| 747427f816 | |||
| f99b6c6421 | |||
| 250bc54110 | |||
| bb530b87dd | |||
| 02587c1edc | |||
| 26fc20d406 | |||
| 8a829b773a | |||
| 71b98e6111 | |||
| fee13dc62f | |||
| 094e0c7592 | |||
| b57a953caa | |||
| d8ebde37ca | |||
| 0708a49848 | |||
| b26f3082e6 | |||
| cb6d424765 | |||
| 5753aa9f18 | |||
| bfae12008d | |||
| c52e0dc8ef | |||
| 55346444c9 | |||
| ba785ec58d | |||
| 3be2545ad4 | |||
| 4a8597c245 | |||
| 741cd66c9e | |||
| 7acea1ac99 | |||
| 22d771b51d | |||
| 00d6ab2eb7 | |||
| cc0a5ac3df | |||
| 5fb938d6d6 | |||
| cd5bdb837e | |||
| 107abf9d29 | |||
| 218ee30206 | |||
| 8764fbfdfa | |||
| 2463d6b92c | |||
| 810d268c99 | |||
| f5b479dbbc | |||
| b52728e829 | |||
| 8eeb452cce | |||
| 9323844ea7 | |||
| b698fe5ef3 | |||
| edb980d439 | |||
| 310bd0dbf1 | |||
| 1a96889141 | |||
| 60de05bfc1 | |||
| 3deef6197f | |||
| 70114c2145 | |||
| 5499c77e62 | |||
| 292a7f07a2 | |||
| 679f36303a | |||
| 1b61d699c4 | |||
| a7562dff68 | |||
| 8daf1e882f | |||
| 20466dacb7 | |||
| 5b3b592108 | |||
| 9952ce0069 | |||
| 1cbc8fb8ac | |||
| edc69694cb | |||
| bd15c6b8c3 | |||
| 42fe2e7266 | |||
| 44e3be2c88 | |||
| 61d5461138 | |||
| b696ca8b1c | |||
| 6a4d020c27 | |||
| 4791ba3b87 | |||
| cf1c9a3083 | |||
| d61e3fb130 | |||
| ca7d6797e1 | |||
| af0d04d0f3 | |||
| 6274567e16 | |||
| 8836ee1dda | |||
| 7afde79600 | |||
| 3d782f7162 |
@@ -0,0 +1,20 @@
|
||||
on: [push, pull_request]
|
||||
name: Check
|
||||
jobs:
|
||||
check:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.17.x]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- name: Install go-sumtype
|
||||
run: go get github.com/sudarshan-reddy/go-sumtype
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Test
|
||||
run: make test
|
||||
+10
-9
@@ -1,17 +1,18 @@
|
||||
.GOPATH/
|
||||
bin/
|
||||
tmp/
|
||||
guide/public
|
||||
/.GOPATH
|
||||
/tmp
|
||||
/bin
|
||||
.idea
|
||||
.build
|
||||
.vscode
|
||||
\#*\#
|
||||
cscope.*
|
||||
cloudflared
|
||||
cloudflared.pkg
|
||||
cloudflared.exe
|
||||
!cmd/cloudflared/
|
||||
/cloudflared
|
||||
/cloudflared.pkg
|
||||
/cloudflared.exe
|
||||
/cloudflared.msi
|
||||
/cloudflared-x86-64*
|
||||
/packaging
|
||||
.DS_Store
|
||||
*-session.log
|
||||
ssh_server_tests/.env
|
||||
/.cover
|
||||
built_artifacts/
|
||||
|
||||
Vendored
+169
-3
@@ -1,7 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "$(uname)" != "Darwin" ]] ; then
|
||||
echo "This should be run on macOS"
|
||||
exit 1
|
||||
@@ -13,9 +11,177 @@ export GO111MODULE=on
|
||||
# build 'cloudflared-darwin-amd64.tgz'
|
||||
mkdir -p artifacts
|
||||
FILENAME="$(pwd)/artifacts/cloudflared-darwin-amd64.tgz"
|
||||
PKGNAME="$(pwd)/artifacts/cloudflared-amd64.pkg"
|
||||
TARGET_DIRECTORY=".build"
|
||||
BINARY_NAME="cloudflared"
|
||||
VERSION=$(git describe --tags --always --dirty="-dev")
|
||||
PRODUCT="cloudflared"
|
||||
CODE_SIGN_PRIV="code_sign.p12"
|
||||
CODE_SIGN_CERT="code_sign.cer"
|
||||
INSTALLER_PRIV="installer.p12"
|
||||
INSTALLER_CERT="installer.cer"
|
||||
BUNDLE_ID="com.cloudflare.cloudflared"
|
||||
SEC_DUP_MSG="security: SecKeychainItemImport: The specified item already exists in the keychain."
|
||||
export PATH="$PATH:/usr/local/bin"
|
||||
mkdir -p ../src/github.com/cloudflare/
|
||||
cp -r . ../src/github.com/cloudflare/cloudflared
|
||||
cd ../src/github.com/cloudflare/cloudflared
|
||||
GOCACHE="$PWD/../../../../" GOPATH="$PWD/../../../../" CGO_ENABLED=1 make cloudflared
|
||||
tar czf "$FILENAME" cloudflared
|
||||
|
||||
# Add code signing private key to the key chain
|
||||
if [[ ! -z "$CFD_CODE_SIGN_KEY" ]]; then
|
||||
if [[ ! -z "$CFD_CODE_SIGN_PASS" ]]; then
|
||||
# write private key to disk and then import it keychain
|
||||
echo -n -e ${CFD_CODE_SIGN_KEY} | base64 -D > ${CODE_SIGN_PRIV}
|
||||
out=$(security import ${CODE_SIGN_PRIV} -A -P "${CFD_CODE_SIGN_PASS}" 2>&1)
|
||||
exitcode=$?
|
||||
if [ -n "$out" ]; then
|
||||
if [ $exitcode -eq 0 ]; then
|
||||
echo "$out"
|
||||
else
|
||||
if [ "$out" != "${SEC_DUP_MSG}" ]; then
|
||||
echo "$out" >&2
|
||||
exit $exitcode
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
rm ${CODE_SIGN_PRIV}
|
||||
fi
|
||||
fi
|
||||
|
||||
# Add code signing certificate to the key chain
|
||||
if [[ ! -z "$CFD_CODE_SIGN_CERT" ]]; then
|
||||
# write certificate to disk and then import it keychain
|
||||
echo -n -e ${CFD_CODE_SIGN_CERT} | base64 -D > ${CODE_SIGN_CERT}
|
||||
out1=$(security import ${CODE_SIGN_CERT} -A 2>&1)
|
||||
exitcode1=$?
|
||||
if [ -n "$out1" ]; then
|
||||
if [ $exitcode1 -eq 0 ]; then
|
||||
echo "$out1"
|
||||
else
|
||||
if [ "$out1" != "${SEC_DUP_MSG}" ]; then
|
||||
echo "$out1" >&2
|
||||
exit $exitcode1
|
||||
else
|
||||
echo "already imported code signing certificate"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
rm ${CODE_SIGN_CERT}
|
||||
fi
|
||||
|
||||
# Add package signing private key to the key chain
|
||||
if [[ ! -z "$CFD_INSTALLER_KEY" ]]; then
|
||||
if [[ ! -z "$CFD_INSTALLER_PASS" ]]; then
|
||||
# write private key to disk and then import it into the keychain
|
||||
echo -n -e ${CFD_INSTALLER_KEY} | base64 -D > ${INSTALLER_PRIV}
|
||||
out2=$(security import ${INSTALLER_PRIV} -A -P "${CFD_INSTALLER_PASS}" 2>&1)
|
||||
exitcode2=$?
|
||||
if [ -n "$out2" ]; then
|
||||
if [ $exitcode2 -eq 0 ]; then
|
||||
echo "$out2"
|
||||
else
|
||||
if [ "$out2" != "${SEC_DUP_MSG}" ]; then
|
||||
echo "$out2" >&2
|
||||
exit $exitcode2
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
rm ${INSTALLER_PRIV}
|
||||
fi
|
||||
fi
|
||||
|
||||
# Add package signing certificate to the key chain
|
||||
if [[ ! -z "$CFD_INSTALLER_CERT" ]]; then
|
||||
# write certificate to disk and then import it keychain
|
||||
echo -n -e ${CFD_INSTALLER_CERT} | base64 -D > ${INSTALLER_CERT}
|
||||
out3=$(security import ${INSTALLER_CERT} -A 2>&1)
|
||||
exitcode3=$?
|
||||
if [ -n "$out3" ]; then
|
||||
if [ $exitcode3 -eq 0 ]; then
|
||||
echo "$out3"
|
||||
else
|
||||
if [ "$out3" != "${SEC_DUP_MSG}" ]; then
|
||||
echo "$out3" >&2
|
||||
exit $exitcode3
|
||||
else
|
||||
echo "already imported installer certificate"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
rm ${INSTALLER_CERT}
|
||||
fi
|
||||
|
||||
# get the code signing certificate name
|
||||
if [[ ! -z "$CFD_CODE_SIGN_NAME" ]]; then
|
||||
CODE_SIGN_NAME="${CFD_CODE_SIGN_NAME}"
|
||||
else
|
||||
if [[ -n "$(security find-certificate -c "Developer ID Application" | cut -d'"' -f 4 -s | grep "Developer ID Application:" | head -1)" ]]; then
|
||||
CODE_SIGN_NAME=$(security find-certificate -c "Developer ID Application" | cut -d'"' -f 4 -s | grep "Developer ID Application:" | head -1)
|
||||
else
|
||||
CODE_SIGN_NAME=""
|
||||
fi
|
||||
fi
|
||||
|
||||
# get the package signing certificate name
|
||||
if [[ ! -z "$CFD_INSTALLER_NAME" ]]; then
|
||||
PKG_SIGN_NAME="${CFD_INSTALLER_NAME}"
|
||||
else
|
||||
if [[ -n "$(security find-certificate -c "Developer ID Installer" | cut -d'"' -f 4 -s | grep "Developer ID Installer:" | head -1)" ]]; then
|
||||
PKG_SIGN_NAME=$(security find-certificate -c "Developer ID Installer" | cut -d'"' -f 4 -s | grep "Developer ID Installer:" | head -1)
|
||||
else
|
||||
PKG_SIGN_NAME=""
|
||||
fi
|
||||
fi
|
||||
|
||||
# sign the cloudflared binary
|
||||
if [[ ! -z "$CODE_SIGN_NAME" ]]; then
|
||||
codesign -s "${CODE_SIGN_NAME}" -f -v --timestamp --options runtime ${BINARY_NAME}
|
||||
|
||||
# notarize the binary
|
||||
if [[ ! -z "$CFD_NOTE_PASSWORD" ]]; then
|
||||
zip "${BINARY_NAME}.zip" ${BINARY_NAME}
|
||||
xcrun altool --notarize-app -f "${BINARY_NAME}.zip" -t osx -u ${CFD_NOTE_USERNAME} -p ${CFD_NOTE_PASSWORD} --primary-bundle-id ${BUNDLE_ID}
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
# creating build directory
|
||||
rm -rf $TARGET_DIRECTORY
|
||||
mkdir "${TARGET_DIRECTORY}"
|
||||
mkdir "${TARGET_DIRECTORY}/contents"
|
||||
cp -r ".mac_resources/scripts" "${TARGET_DIRECTORY}/scripts"
|
||||
|
||||
# copy cloudflared into the build directory
|
||||
cp ${BINARY_NAME} "${TARGET_DIRECTORY}/contents/${PRODUCT}"
|
||||
|
||||
# compress cloudflared into a tar and gzipped file
|
||||
tar czf "$FILENAME" "${BINARY_NAME}"
|
||||
|
||||
# build the installer package
|
||||
if [[ ! -z "$PKG_SIGN_NAME" ]]; then
|
||||
pkgbuild --identifier com.cloudflare.${PRODUCT} \
|
||||
--version ${VERSION} \
|
||||
--scripts ${TARGET_DIRECTORY}/scripts \
|
||||
--root ${TARGET_DIRECTORY}/contents \
|
||||
--install-location /usr/local/bin \
|
||||
--sign "${PKG_SIGN_NAME}" \
|
||||
${PKGNAME}
|
||||
|
||||
# notarize the package
|
||||
if [[ ! -z "$CFD_NOTE_PASSWORD" ]]; then
|
||||
xcrun altool --notarize-app -f ${PKGNAME} -t osx -u ${CFD_NOTE_USERNAME} -p ${CFD_NOTE_PASSWORD} --primary-bundle-id ${BUNDLE_ID}
|
||||
xcrun stapler staple ${PKGNAME}
|
||||
fi
|
||||
else
|
||||
pkgbuild --identifier com.cloudflare.${PRODUCT} \
|
||||
--version ${VERSION} \
|
||||
--scripts ${TARGET_DIRECTORY}/scripts \
|
||||
--root ${TARGET_DIRECTORY}/contents \
|
||||
--install-location /usr/local/bin \
|
||||
${PKGNAME}
|
||||
fi
|
||||
|
||||
|
||||
# cleaning up the build directory
|
||||
rm -rf $TARGET_DIRECTORY
|
||||
|
||||
Vendored
+4
-4
@@ -43,11 +43,11 @@ git checkout -f master
|
||||
git reset --hard origin/master
|
||||
|
||||
# modify cloudflared.rb
|
||||
URL="https://developers.cloudflare.com/argo-tunnel/dl/cloudflared-$VERSION-darwin-amd64.tgz"
|
||||
URL="https://packages.argotunnel.com/dl/cloudflared-$VERSION-darwin-amd64.tgz"
|
||||
tee cloudflared.rb <<EOF
|
||||
class Cloudflared < Formula
|
||||
desc 'Argo Tunnel'
|
||||
homepage 'https://developers.cloudflare.com/argo-tunnel/'
|
||||
desc 'Cloudflare Tunnel'
|
||||
homepage 'https://developers.cloudflare.com/cloudflare-one/connections/connect-apps'
|
||||
url '$URL'
|
||||
sha256 '$SHA256'
|
||||
version '$VERSION'
|
||||
@@ -62,6 +62,6 @@ 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 commit -m "Release Cloudflare Tunnel $VERSION"
|
||||
|
||||
git push -v origin master
|
||||
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
**Experimental**: This is a new format for release notes. The format and availability is subject to change.
|
||||
|
||||
## 2021.12.2
|
||||
### Bug Fixes
|
||||
- Fix logging when `quic` transport is used and UDP traffic is proxied.
|
||||
- FIPS compliant cloudflared binaries will now be released as separate artifacts. Recall that these are only for linux
|
||||
and amd64.
|
||||
|
||||
## 2021.12.1
|
||||
### Bug Fixes
|
||||
- Fixes Github issue #530 where cloudflared 2021.12.0 could not reach origins that were HTTPS and using certain encryption
|
||||
methods forbidden by FIPS compliance (such as Let's Encrypt certificates). To address this fix we have temporarily reverted
|
||||
FIPS compliance from amd64 linux binaries that was recently introduced (or fixed actually as it was never working before).
|
||||
|
||||
## 2021.12.0
|
||||
### New Features
|
||||
- Cloudflared binary released for amd64 linux is now FIPS compliant.
|
||||
|
||||
### Improvements
|
||||
- Logging about connectivity to Cloudflare edge now only yields `ERR` level logging if there are no connections to
|
||||
Cloudflare edge that are active. Otherwise it logs `WARN` level.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixes Github issue #501.
|
||||
|
||||
## 2021.11.0
|
||||
### Improvements
|
||||
- Fallback from `protocol:quic` to `protocol:http2` immediately if UDP connectivity isn't available. This could be because of a firewall or
|
||||
egress rule.
|
||||
|
||||
## 2021.10.4
|
||||
### Improvements
|
||||
- Collect quic transport metrics on RTT, packets and bytes transferred.
|
||||
|
||||
### Bug Fixes
|
||||
- Fix race condition that was writing to the connection after the http2 handler returns.
|
||||
|
||||
## 2021.9.2
|
||||
|
||||
### New features
|
||||
- `cloudflared` can now run with `quic` as the underlying tunnel transport protocol. To try it, change or add "protocol: quic" to your config.yml file or
|
||||
run cloudflared with the `--protocol quic` flag. e.g:
|
||||
`cloudflared tunnel --protocol quic run <tunnel-name>`
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed some generic transport bugs in `quic` mode. It's advised to upgrade to at least this version (2021.9.2) when running `cloudflared`
|
||||
with `quic` protocol.
|
||||
- `cloudflared` docker images will now show version.
|
||||
|
||||
|
||||
## 2021.8.4
|
||||
### Improvements
|
||||
- Temporary tunnels (those hosted on trycloudflare.com that do not require a Cloudflare login) now run as Named Tunnels
|
||||
underneath. We recall that these tunnels should not be relied upon for production usage as they come with no guarantee
|
||||
of uptime. Previous cloudflared versions will soon be unable to run legacy temporary tunnels and will require an update
|
||||
(to this version or more recent).
|
||||
|
||||
## 2021.8.2
|
||||
### Improvements
|
||||
- Because Equinox os shutting down, all cloudflared releases are now present [here](https://github.com/cloudflare/cloudflared/releases).
|
||||
[Equinox](https://dl.equinox.io/cloudflare/cloudflared/stable) will no longer receive updates.
|
||||
|
||||
## 2021.8.0
|
||||
### Bug fixes
|
||||
- Prevents tunnel from accidentally running when only proxy-dns should run.
|
||||
|
||||
### Improvements
|
||||
- If auto protocol transport lookup fails, we now default to a transport instead of not connecting.
|
||||
|
||||
## 2021.6.0
|
||||
### Bug Fixes
|
||||
- Fixes a http2 transport (the new default for Named Tunnels) to work with unix socket origins.
|
||||
|
||||
|
||||
## 2021.5.10
|
||||
### Bug Fixes
|
||||
- Fixes a memory leak in h2mux transport that connects cloudflared to Cloudflare edge.
|
||||
|
||||
|
||||
## 2021.5.9
|
||||
### New Features
|
||||
- Uses new Worker based login helper service to facilitate token exchange in cloudflared flows.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixes Centos-7 builds.
|
||||
|
||||
## 2021.5.8
|
||||
### New Features
|
||||
- When creating a DNS record to point a hostname at a tunnel, you can now use --overwrite-dns to overwrite any existing
|
||||
DNS records with that hostname. This works both when using the CLI to provision DNS, as well as when starting an adhoc
|
||||
named tunnel, e.g.:
|
||||
- `cloudflared tunnel route dns --overwrite-dns foo-tunnel foo.example.com`
|
||||
- `cloudflared tunnel --overwrite-dns --name foo-tunnel --hostname foo.example.com`
|
||||
|
||||
## 2021.5.7
|
||||
### New Features
|
||||
- Named Tunnels will automatically select the protocol to connect to Cloudflare's edge network.
|
||||
|
||||
## 2021.5.0
|
||||
|
||||
### New Features
|
||||
- It is now possible to run the same tunnel using more than one `cloudflared` instance. This is a server-side change and
|
||||
is compatible with any client version that uses Named Tunnels.
|
||||
|
||||
To get started, visit our [developer documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/deploy-cloudflared-replicas).
|
||||
- `cloudflared tunnel ingress validate` will now warn about unused keys in your config file. This is helpful for
|
||||
detecting typos in your config.
|
||||
- If `cloudflared` detects it is running inside a Linux container, it will limit itself to use only the number of CPUs
|
||||
the pod has been granted, instead of trying to use every CPU available.
|
||||
|
||||
## 2021.4.0
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed proxying of websocket requests to avoid possibility of losing initial frames that were sent in the same TCP
|
||||
packet as response headers [#345](https://github.com/cloudflare/cloudflared/issues/345).
|
||||
- `proxy-dns` option now works in conjunction with running a named tunnel [#346](https://github.com/cloudflare/cloudflared/issues/346).
|
||||
|
||||
## 2021.3.6
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Reverted 2021.3.5 improvement to use HTTP/2 in a best-effort manner between cloudflared and origin services because
|
||||
it was found to break in some cases.
|
||||
|
||||
## 2021.3.5
|
||||
|
||||
### Improvements
|
||||
|
||||
- HTTP/2 transport is now always chosen if origin server supports it and the service url scheme is HTTPS.
|
||||
This was previously done in a best attempt manner.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- The MacOS binaries were not successfully released in 2021.3.3 and 2021.3.4. This release is aimed at addressing that.
|
||||
|
||||
## 2021.3.3
|
||||
|
||||
### Improvements
|
||||
|
||||
- Tunnel create command, as well as, running ad-hoc tunnels using `cloudflared tunnel -name NAME`, will not overwrite
|
||||
existing files when writing tunnel credentials.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Tunnel create and delete commands no longer use path to credentials from the configuration file.
|
||||
If you need ot place tunnel credentials file at a specific location, you must use `--credentials-file` flag.
|
||||
- Access ssh-gen creates properly named keys for SSH short lived certs.
|
||||
|
||||
|
||||
## 2021.3.2
|
||||
|
||||
### New Features
|
||||
|
||||
- It is now possible to obtain more detailed information about the cloudflared connectors to Cloudflare Edge via
|
||||
`cloudflared tunnel info <name/uuid>`. It is possible to sort the output as well as output in different formats,
|
||||
such as: `cloudflared tunnel info --sort-by version --invert-sort --output json <name/uuid>`.
|
||||
You can obtain more information via `cloudflared tunnel info --help`.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Don't look for configuration file in default paths when `--config FILE` flag is present after `tunnel` subcommand.
|
||||
- cloudflared access token command now functions correctly with the new token-per-app change from 2021.3.0.
|
||||
|
||||
|
||||
## 2021.3.0
|
||||
|
||||
### New Features
|
||||
|
||||
- [Cloudflare One Routing](https://developers.cloudflare.com/cloudflare-one/tutorials/warp-to-tunnel) specific commands
|
||||
now show up in the `cloudflared tunnel route --help` output.
|
||||
- There is a new ingress type that allows cloudflared to proxy SOCKS5 as a bastion. You can use it with an ingress
|
||||
rule by adding `service: socks-proxy`. Traffic is routed to any destination specified by the SOCKS5 packet but only
|
||||
if allowed by a rule. In the following example we allow proxying to a certain CIDR but explicitly forbid one address
|
||||
within it:
|
||||
```
|
||||
ingress:
|
||||
- hostname: socks.example.com
|
||||
service: socks-proxy
|
||||
originRequest:
|
||||
ipRules:
|
||||
- prefix: 192.168.1.8/32
|
||||
allow: false
|
||||
- prefix: 192.168.1.0/24
|
||||
ports: [80, 443]
|
||||
allow: true
|
||||
```
|
||||
|
||||
|
||||
### Improvements
|
||||
|
||||
- Nested commands, such as `cloudflared tunnel run`, now consider CLI arguments even if they appear earlier on the
|
||||
command. For instance, `cloudflared --config config.yaml tunnel run` will now behave the same as
|
||||
`cloudflared tunnel --config config.yaml run`
|
||||
- Warnings are now shown in the output logs whenever cloudflared is running without the most recent version and
|
||||
`no-autoupdate` is `true`.
|
||||
- Access tokens are now stored per Access App instead of per request path. This decreases the number of times that the
|
||||
user is required to authenticate with an Access policy redundantly.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- GitHub [PR #317](https://github.com/cloudflare/cloudflared/issues/317) was broken in 2021.2.5 and is now fixed again.
|
||||
|
||||
## 2021.2.5
|
||||
|
||||
### New Features
|
||||
|
||||
- We introduce [Cloudflare One Routing](https://developers.cloudflare.com/cloudflare-one/tutorials/warp-to-tunnel) in
|
||||
beta mode. Cloudflare customer can now connect users and private networks with RFC 1918 IP addresses via the
|
||||
Cloudflare edge network. Users running Cloudflare WARP client in the same organization can connect to the services
|
||||
made available by Argo Tunnel IP routes. Please share your feedback in the GitHub issue tracker.
|
||||
|
||||
## 2021.2.4
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Reverts the Improvement released in 2021.2.3 for CLI arguments as it introduced a regression where cloudflared failed
|
||||
to read URLs in configuration files.
|
||||
- cloudflared now logs the reason for failed connections if the error is recoverable.
|
||||
|
||||
## 2021.2.3
|
||||
|
||||
### Backward Incompatible Changes
|
||||
|
||||
- Removes db-connect. The Cloudflare Workers product will continue to support db-connect implementations with versions
|
||||
of cloudflared that predate this release and include support for db-connect.
|
||||
|
||||
### New Features
|
||||
|
||||
- Introduces support for proxy configurations with websockets in arbitrary TCP connections (#318).
|
||||
|
||||
### Improvements
|
||||
|
||||
- (reverted) Nested command line argument handling.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- The maximum number of upstream connections is now limited by default which should fix reported issues of cloudflared
|
||||
exhausting CPU usage when faced with connectivity issues.
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# use a builder image for building cloudflare
|
||||
ARG TARGET_GOOS
|
||||
ARG TARGET_GOARCH
|
||||
FROM golang:1.13.3 as builder
|
||||
FROM golang:1.17.1 as builder
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0 \
|
||||
TARGET_GOOS=${TARGET_GOOS} \
|
||||
|
||||
@@ -1,31 +1,62 @@
|
||||
VERSION := $(shell git describe --tags --always --dirty="-dev")
|
||||
VERSION := $(shell git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
|
||||
MSI_VERSION := $(shell git tag -l --sort=v:refname | grep "w" | tail -1 | cut -c2-)
|
||||
#MSI_VERSION expects the format of the tag to be: (wX.X.X). Starts with the w character to not break cfsetup.
|
||||
#e.g. w3.0.1 or w4.2.10. It trims off the w character when creating the MSI.
|
||||
|
||||
ifeq ($(ORIGINAL_NAME), true)
|
||||
# Used for builds that want FIPS compilation but want the artifacts generated to still have the original name.
|
||||
BINARY_NAME := cloudflared
|
||||
else ifeq ($(FIPS), true)
|
||||
# Used for FIPS compliant builds that do not match the case above.
|
||||
BINARY_NAME := cloudflared-fips
|
||||
else
|
||||
# Used for all other (non-FIPS) builds.
|
||||
BINARY_NAME := cloudflared
|
||||
endif
|
||||
|
||||
ifeq ($(NIGHTLY), true)
|
||||
DEB_PACKAGE_NAME := $(BINARY_NAME)-nightly
|
||||
NIGHTLY_FLAGS := --conflicts cloudflared --replaces cloudflared
|
||||
else
|
||||
DEB_PACKAGE_NAME := $(BINARY_NAME)
|
||||
endif
|
||||
|
||||
DATE := $(shell date -u '+%Y-%m-%d-%H%M UTC')
|
||||
VERSION_FLAGS := -ldflags='-X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)"'
|
||||
VERSION_FLAGS := -X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)"
|
||||
|
||||
LINK_FLAGS :=
|
||||
ifeq ($(FIPS), true)
|
||||
LINK_FLAGS := -linkmode=external -extldflags=-static $(LINK_FLAGS)
|
||||
# Prevent linking with libc regardless of CGO enabled or not.
|
||||
GO_BUILD_TAGS := $(GO_BUILD_TAGS) osusergo netgo fips
|
||||
VERSION_FLAGS := $(VERSION_FLAGS) -X "main.BuildType=FIPS"
|
||||
endif
|
||||
|
||||
LDFLAGS := -ldflags='$(VERSION_FLAGS) $(LINK_FLAGS)'
|
||||
ifneq ($(GO_BUILD_TAGS),)
|
||||
GO_BUILD_TAGS := -tags "$(GO_BUILD_TAGS)"
|
||||
endif
|
||||
|
||||
IMPORT_PATH := github.com/cloudflare/cloudflared
|
||||
PACKAGE_DIR := $(CURDIR)/packaging
|
||||
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)"
|
||||
|
||||
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),amd64)
|
||||
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 ($(LOCAL_ARCH),arm64)
|
||||
TARGET_ARCH ?= arm64
|
||||
else ifeq ($(shell echo $(LOCAL_ARCH) | head -c 4),armv)
|
||||
TARGET_ARCH ?= arm
|
||||
else
|
||||
@@ -39,20 +70,20 @@ 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
|
||||
EXECUTABLE_PATH=./$(BINARY_NAME).exe
|
||||
else
|
||||
EXECUTABLE_PATH=./cloudflared
|
||||
EXECUTABLE_PATH=./$(BINARY_NAME)
|
||||
endif
|
||||
|
||||
ifeq ($(FLAVOR), centos-7)
|
||||
TARGET_PUBLIC_REPO ?= el7
|
||||
else ifeq ($(FLAVOR), centos-6)
|
||||
TARGET_PUBLIC_REPO ?= el6
|
||||
else
|
||||
TARGET_PUBLIC_REPO ?= $(FLAVOR)
|
||||
endif
|
||||
@@ -65,8 +96,16 @@ clean:
|
||||
go clean
|
||||
|
||||
.PHONY: cloudflared
|
||||
cloudflared: tunnel-deps
|
||||
GOOS=$(TARGET_OS) GOARCH=$(TARGET_ARCH) go build -v -mod=vendor $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/cloudflared
|
||||
cloudflared:
|
||||
ifeq ($(FIPS), true)
|
||||
$(info Building cloudflared with go-fips)
|
||||
cp -f fips/fips.go.linux-amd64 cmd/cloudflared/fips.go
|
||||
endif
|
||||
GOOS=$(TARGET_OS) GOARCH=$(TARGET_ARCH) go build -v -mod=vendor $(GO_BUILD_TAGS) $(LDFLAGS) $(IMPORT_PATH)/cmd/cloudflared
|
||||
ifeq ($(FIPS), true)
|
||||
rm -f cmd/cloudflared/fips.go
|
||||
./check-fips.sh cloudflared
|
||||
endif
|
||||
|
||||
.PHONY: container
|
||||
container:
|
||||
@@ -74,16 +113,23 @@ container:
|
||||
|
||||
.PHONY: test
|
||||
test: vet
|
||||
go test -v -mod=vendor -race $(VERSION_FLAGS) ./...
|
||||
ifndef CI
|
||||
go test -v -mod=vendor -race $(LDFLAGS) ./...
|
||||
else
|
||||
@mkdir -p .cover
|
||||
go test -v -mod=vendor -race $(LDFLAGS) -coverprofile=".cover/c.out" ./...
|
||||
go tool cover -html ".cover/c.out" -o .cover/all.html
|
||||
endif
|
||||
|
||||
.PHONY: test-ssh-server
|
||||
test-ssh-server:
|
||||
docker-compose -f ssh_server_tests/docker-compose.yml up
|
||||
|
||||
define publish_package
|
||||
chmod 664 $(BINARY_NAME)*.$(1); \
|
||||
for HOST in $(CF_PKG_HOSTS); do \
|
||||
ssh-keyscan -t rsa $$HOST >> ~/.ssh/known_hosts; \
|
||||
scp -4 cloudflared*.$(1) cfsync@$$HOST:/state/cf-pkg/staging/$(2)/$(TARGET_PUBLIC_REPO)/cloudflared/; \
|
||||
ssh-keyscan -t ecdsa $$HOST >> ~/.ssh/known_hosts; \
|
||||
scp -p -4 $(BINARY_NAME)*.$(1) cfsync@$$HOST:/state/cf-pkg/staging/$(2)/$(TARGET_PUBLIC_REPO)/$(BINARY_NAME)/; \
|
||||
done
|
||||
endef
|
||||
|
||||
@@ -95,17 +141,19 @@ publish-deb: cloudflared-deb
|
||||
publish-rpm: cloudflared-rpm
|
||||
$(call publish_package,rpm,yum)
|
||||
|
||||
# When we build packages, the package name will be FIPS-aware.
|
||||
# But we keep the binary installed by it to be named "cloudflared" regardless.
|
||||
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' \
|
||||
fakeroot fpm -C $(PACKAGE_DIR) -s dir -t $(1) \
|
||||
--description 'Cloudflare 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 \
|
||||
-a $(TARGET_ARCH) -v $(VERSION) -n $(DEB_PACKAGE_NAME) $(NIGHTLY_FLAGS) --after-install postinst.sh --after-remove postrm.sh \
|
||||
cloudflared=$(INSTALL_BINDIR) cloudflared.1=$(MAN_DIR)
|
||||
endef
|
||||
|
||||
@@ -117,11 +165,71 @@ cloudflared-deb: cloudflared
|
||||
cloudflared-rpm: cloudflared
|
||||
$(call build_package,rpm)
|
||||
|
||||
.PHONY: cloudflared-pkg
|
||||
cloudflared-pkg: cloudflared
|
||||
$(call build_package,osxpkg)
|
||||
|
||||
.PHONY: cloudflared-msi
|
||||
cloudflared-msi: cloudflared
|
||||
wixl --define Version=$(VERSION) --define Path=$(EXECUTABLE_PATH) --output cloudflared-$(VERSION)-$(TARGET_ARCH).msi cloudflared.wxs
|
||||
|
||||
.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
|
||||
@@ -131,28 +239,44 @@ homebrew-upload: cloudflared-darwin-amd64.tgz
|
||||
homebrew-release: homebrew-upload
|
||||
./publish-homebrew-formula.sh cloudflared-darwin-amd64.tgz $(VERSION) homebrew-cloudflare
|
||||
|
||||
.PHONY: release
|
||||
release: bin/equinox
|
||||
bin/equinox release $(EQUINOX_FLAGS) -- $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/cloudflared
|
||||
|
||||
.PHONY: github-release
|
||||
github-release: cloudflared
|
||||
python3 github_release.py --path $(EXECUTABLE_PATH) --release-version $(VERSION)
|
||||
|
||||
bin/equinox:
|
||||
mkdir -p bin
|
||||
curl -s https://bin.equinox.io/c/75JtLRTsJ3n/release-tool-beta-$(EQUINOX_PLATFORM).tgz | tar xz -C bin/
|
||||
.PHONY: github-release-built-pkgs
|
||||
github-release-built-pkgs:
|
||||
python3 github_release.py --path $(PWD)/built_artifacts --release-version $(VERSION)
|
||||
|
||||
.PHONY: tunnel-deps
|
||||
tunnel-deps: tunnelrpc/tunnelrpc.capnp.go
|
||||
.PHONY: github-message
|
||||
github-message:
|
||||
python3 github_message.py --release-version $(VERSION)
|
||||
|
||||
tunnelrpc/tunnelrpc.capnp.go: tunnelrpc/tunnelrpc.capnp
|
||||
.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
|
||||
|
||||
.PHONY: tunnelrpc-deps
|
||||
tunnelrpc-deps:
|
||||
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: quic-deps
|
||||
quic-deps:
|
||||
which capnp
|
||||
which capnpc-go
|
||||
capnp compile -ogo quic/schema/quic_metadata_protocol.capnp
|
||||
|
||||
.PHONY: vet
|
||||
vet:
|
||||
go vet -mod=vendor ./...
|
||||
which go-sumtype # go get github.com/BurntSushi/go-sumtype
|
||||
# go get github.com/sudarshan-reddy/go-sumtype (don't do this in build directory or this will cause vendor issues)
|
||||
# Note: If you have github.com/BurntSushi/go-sumtype then you might have to use the repo above instead
|
||||
# for now because it uses an older version of golang.org/x/tools.
|
||||
which go-sumtype
|
||||
go-sumtype $$(go list -mod=vendor ./...)
|
||||
|
||||
.PHONY: goimports
|
||||
goimports:
|
||||
for d in $$(go list -mod=readonly -f '{{.Dir}}' -a ./... | fgrep -v tunnelrpc) ; do goimports -format-only -local github.com/cloudflare/cloudflared -w $$d ; done
|
||||
@@ -1,9 +1,60 @@
|
||||
# Argo Tunnel client
|
||||
# Cloudflare Tunnel client
|
||||
|
||||
Contains the command-line client and its libraries for Argo Tunnel, a tunneling daemon that proxies any local webserver through the Cloudflare network.
|
||||
Contains the command-line client for Cloudflare Tunnel, a tunneling daemon that proxies traffic from the Cloudflare network to your origins.
|
||||
This daemon sits between Cloudflare network and your origin (e.g. a webserver). Cloudflare attracts client requests and sends them to you
|
||||
via this daemon, without requiring you to poke holes on your firewall --- your origin can remain as closed as possible.
|
||||
Extensive documentation can be found in the [Cloudflare Tunnel section](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps) of the Cloudflare Docs.
|
||||
All usages related with proxying to your origins are available under `cloudflared tunnel help`.
|
||||
|
||||
## Getting started
|
||||
You can also use `cloudflared` to access Tunnel origins (that are protected with `cloudflared tunnel`) for TCP traffic
|
||||
at Layer 4 (i.e., not HTTP/websocket), which is relevant for use cases such as SSH, RDP, etc.
|
||||
Such usages are available under `cloudflared access help`.
|
||||
|
||||
go install github.com/cloudflare/cloudflared/cmd/cloudflared
|
||||
You can instead use [WARP client](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configuration/private-networks)
|
||||
to access private origins behind Tunnels for Layer 4 traffic without requiring `cloudflared access` commands on the client side.
|
||||
|
||||
User documentation for Argo Tunnel can be found at https://developers.cloudflare.com/argo-tunnel/
|
||||
|
||||
## Before you get started
|
||||
|
||||
Before you use Cloudflare Tunnel, you'll need to complete a few steps in the Cloudflare dashboard: you need to add a
|
||||
website to your Cloudflare account. Note that today it is possible to use Tunnel without a website (e.g. for private
|
||||
routing), but for legacy reasons this requirement is still necessary:
|
||||
1. [Add a website to Cloudflare](https://support.cloudflare.com/hc/en-us/articles/201720164-Creating-a-Cloudflare-account-and-adding-a-website)
|
||||
2. [Change your domain nameservers to Cloudflare](https://support.cloudflare.com/hc/en-us/articles/205195708)
|
||||
|
||||
|
||||
## Installing `cloudflared`
|
||||
|
||||
Downloads are available as standalone binaries, a Docker image, and Debian, RPM, and Homebrew packages. You can also find releases here on the `cloudflared` GitHub repository.
|
||||
|
||||
* You can [install on macOS](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#macos) via Homebrew or by downloading the [latest Darwin amd64 release](https://github.com/cloudflare/cloudflared/releases)
|
||||
* Binaries, Debian, and RPM packages for Linux [can be found here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#linux)
|
||||
* A Docker image of `cloudflared` is [available on DockerHub](https://hub.docker.com/r/cloudflare/cloudflared)
|
||||
* You can install on Windows machines with the [steps here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#windows)
|
||||
|
||||
User documentation for Cloudflare Tunnel can be found at https://developers.cloudflare.com/cloudflare-one/connections/connect-apps
|
||||
|
||||
|
||||
## Creating Tunnels and routing traffic
|
||||
|
||||
Once installed, you can authenticate `cloudflared` into your Cloudflare account and begin creating Tunnels to serve traffic to your origins.
|
||||
|
||||
* Create a Tunnel with [these instructions](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/create-tunnel)
|
||||
* Route traffic to that Tunnel:
|
||||
* Via public [DNS records in Cloudflare](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/routing-to-tunnel/dns)
|
||||
* Or via a public hostname guided by a [Cloudflare Load Balancer](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/routing-to-tunnel/lb)
|
||||
* Or from [WARP client private traffic](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configuration/private-networks)
|
||||
|
||||
|
||||
## TryCloudflare
|
||||
|
||||
Want to test Cloudflare Tunnel before adding a website to Cloudflare? You can do so with TryCloudflare using the documentation [available here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/trycloudflare).
|
||||
|
||||
## Deprecated versions
|
||||
|
||||
Cloudflare currently supports versions of `cloudflared` 2020.5.1 and later. Breaking changes unrelated to feature availability may be introduced that will impact versions released prior to 2020.5.1. You can read more about upgrading `cloudflared` in our [developer documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#updating-cloudflared).
|
||||
|
||||
| Version(s) | Deprecation status |
|
||||
|---|---|
|
||||
| 2020.5.1 and later | Supported |
|
||||
| Versions prior to 2020.5.1 | No longer supported |
|
||||
|
||||
+652
@@ -1,3 +1,655 @@
|
||||
2022.2.0
|
||||
- 2022-02-02 TUN-4947: Use http when talking to Unix sockets origins
|
||||
- 2022-02-02 TUN-5695: Define RPC method to update configuration
|
||||
- 2022-01-27 TUN-5621: Correctly manage QUIC stream closing
|
||||
- 2022-01-28 TUN-5702: Allow to deserialize config from JSON
|
||||
|
||||
2022.1.3
|
||||
- 2022-01-21 TUN-5477: Unhide vnet commands
|
||||
- 2022-01-24 TUN-5669: Change network command to vnet
|
||||
- 2022-01-25 TUN-5675: Remove github.com/dgrijalva/jwt-go dependency by upgrading coredns version
|
||||
- 2022-01-27 TUN-5719: Re-attempt connection to edge with QUIC despite network error when there is no fallback
|
||||
- 2022-01-28 TUN-5724: Fix SSE streaming by guaranteeing we write everything we read
|
||||
- 2022-01-17 TUN-5547: Bump golang x/net package to fix http2 transport bugs
|
||||
- 2022-01-19 TUN-5659: Proxy UDP with zero-byte payload
|
||||
- 2021-10-22 Add X-Forwarded-Host for http proxy
|
||||
|
||||
2022.1.2
|
||||
- 2022-01-13 TUN-5650: Fix pynacl version to 1.4.0 and pygithub version to 1.55 so release doesn't break unexpectedly
|
||||
|
||||
2022.1.1
|
||||
- 2022-01-10 TUN-5631: Build everything with go 1.17.5
|
||||
- 2022-01-06 TUN-5623: Configure quic max datagram frame size to 1350 bytes for none Windows platforms
|
||||
|
||||
2022.1.0
|
||||
- 2022-01-03 TUN-5612: Add support for specifying TLS min/max version
|
||||
- 2022-01-03 TUN-5612: Make tls min/max version public visible
|
||||
- 2022-01-03 TUN-5551: Internally published debian artifacts are now named just cloudflared even though they are FIPS compliant
|
||||
- 2022-01-04 TUN-5600: Close QUIC transports as soon as possible while respecting graceful shutdown
|
||||
- 2022-01-05 TUN-5616: Never fallback transport if user chooses it on purpose
|
||||
- 2022-01-05 TUN-5204: Unregister QUIC transports on disconnect
|
||||
- 2022-01-04 TUN-5600: Add coverage to component tests for various transports
|
||||
|
||||
2021.12.4
|
||||
- 2021-12-27 TUN-5482: Refactor tunnelstore client related packages for more coherent package
|
||||
- 2021-12-27 TUN-5551: Change internally published debian package to be FIPS compliant
|
||||
- 2021-12-27 TUN-5551: Show whether the binary was built for FIPS compliance
|
||||
|
||||
2021.12.3
|
||||
- 2021-12-22 TUN-5584: Changes for release 2021.12.2
|
||||
- 2021-12-22 TUN-5590: QUIC datagram max user payload is 1217 bytes
|
||||
- 2021-12-22 TUN-5593: Read full packet from UDP connection, even if it exceeds MTU of the transport. When packet length is greater than the MTU of the transport, we will silently drop packets (for now).
|
||||
- 2021-12-23 TUN-5597: Log session ID when session is terminated by edge
|
||||
|
||||
2021.12.2
|
||||
- 2021-12-20 TUN-5571: Remove redundant session manager log, it's already logged in origin/tunnel.ServeQUIC
|
||||
- 2021-12-20 TUN-5570: Only log RPC server events at error level to reduce noise
|
||||
- 2021-12-14 TUN-5494: Send a RPC with terminate reason to edge if the session is closed locally
|
||||
- 2021-11-09 TUN-5551: Reintroduce FIPS compliance for linux amd64 now as separate binaries
|
||||
|
||||
2021.12.1
|
||||
- 2021-12-16 TUN-5549: Revert "TUN-5277: Ensure cloudflared binary is FIPS compliant on linux amd64"
|
||||
|
||||
2021.12.0
|
||||
- 2021-12-13 TUN-5530: Get current time from ticker
|
||||
- 2021-12-15 TUN-5544: Update CHANGES.md for next release
|
||||
- 2021-12-07 TUN-5519: Adjust URL for virtual_networks endpoint to match what we will publish
|
||||
- 2021-12-02 TUN-5488: Close session after it's idle for a period defined by registerUdpSession RPC
|
||||
- 2021-12-09 TUN-5504: Fix upload of packages to public repo
|
||||
- 2021-11-30 TUN-5481: Create abstraction for Origin UDP Connection
|
||||
- 2021-11-30 TUN-5422: Define RPC to unregister session
|
||||
- 2021-11-26 TUN-5361: Commands for managing virtual networks
|
||||
- 2021-11-29 TUN-5362: Adjust route ip commands to be aware of virtual networks
|
||||
- 2021-11-23 TUN-5301: Separate datagram multiplex and session management logic from quic connection logic
|
||||
- 2021-11-10 TUN-5405: Update net package to v0.0.0-20211109214657-ef0fda0de508
|
||||
- 2021-11-10 TUN-5408: Update quic package to v0.24.0
|
||||
- 2021-11-12 Fix typos
|
||||
- 2021-11-13 Fix for Issue #501: Unexpected User-agent insertion when tunneling http request
|
||||
- 2021-11-16 TUN-5129: Remove `-dev` suffix when computing version and Git has uncommitted changes
|
||||
- 2021-11-18 TUN-5441: Fix message about available protocols
|
||||
- 2021-11-12 TUN-5300: Define RPC to register UDP sessions
|
||||
- 2021-11-14 TUN-5299: Send/receive QUIC datagram from edge and proxy to origin as UDP
|
||||
- 2021-11-04 TUN-5387: Updated CHANGES.md for 2021.11.0
|
||||
- 2021-11-08 TUN-5368: Log connection issues with LogLevel that depends on tunnel state
|
||||
- 2021-11-09 TUN-5397: Log cloudflared output when it fails to connect tunnel
|
||||
- 2021-11-09 TUN-5277: Ensure cloudflared binary is FIPS compliant on linux amd64
|
||||
- 2021-11-08 TUN-5393: Content-length is no longer a control header for non-h2mux transports
|
||||
|
||||
2021.11.0
|
||||
- 2021-11-03 TUN-5285: Fallback to HTTP2 immediately if connection times out with no network activity
|
||||
- 2021-09-29 Add flag to 'tunnel create' subcommand to specify a base64-encoded secret
|
||||
|
||||
2021.10.5
|
||||
- 2021-10-25 Update change log for release 2021.10.4
|
||||
- 2021-10-25 Revert "TUN-5184: Make sure outstanding websocket write is finished, and no more writes after shutdown"
|
||||
|
||||
2021.10.4
|
||||
- 2021-10-21 TUN-5287: Fix misuse of wait group in TestQUICServer that caused the test to exit immediately
|
||||
- 2021-10-21 TUN-5286: Upgrade crypto/ssh package to fix CVE-2020-29652
|
||||
- 2021-10-18 TUN-5262: Allow to configure max fetch size for listing queries
|
||||
- 2021-10-19 TUN-5262: Improvements to `max-fetch-size` that allow to deal with large number of tunnels in account
|
||||
- 2021-10-15 TUN-5261: Collect QUIC metrics about RTT, packets and bytes transfered and log events at tracing level
|
||||
- 2021-10-19 TUN-5184: Make sure outstanding websocket write is finished, and no more writes after shutdown
|
||||
|
||||
2021.10.3
|
||||
- 2021-10-14 TUN-5255: Fix potential panic if Cloudflare API fails to respond to GetTunnel(id) during delete command
|
||||
- 2021-10-14 TUN-5257: Fix more cfsetup targets that were broken by recent package changes
|
||||
|
||||
2021.10.2
|
||||
- 2021-10-11 TUN-5138: Switch to QUIC on auto protocol based on threshold
|
||||
- 2021-10-14 TUN-5250: Add missing packages for cfsetup to succeed in github release pkgs target
|
||||
|
||||
2021.10.1
|
||||
- 2021-10-12 TUN-5246: Use protocol: quic for Quick tunnels if one is not already set
|
||||
- 2021-10-13 TUN-5249: Revert "TUN-5138: Switch to QUIC on auto protocol based on threshold"
|
||||
|
||||
2021.10.0
|
||||
- 2021-10-11 TUN-5138: Switch to QUIC on auto protocol based on threshold
|
||||
- 2021-10-07 TUN-5195: Do not set empty body if not applicable
|
||||
- 2021-10-08 UN-5213: Increase MaxStreams value for QUIC transport
|
||||
- 2021-09-28 TUN-5169: Release 2021.9.2 CHANGES.md
|
||||
- 2021-09-28 TUN-5164: Update README and clean up references to Argo Tunnel (using Cloudflare Tunnel instead)
|
||||
|
||||
2021.9.2
|
||||
- 2021-09-21 TUN-5129: Use go 1.17 and copy .git folder to docker build to compute version
|
||||
- 2021-09-21 TUN-5128: Enforce maximum grace period
|
||||
- 2021-09-22 TUN-5141: Make sure websocket pinger returns before streaming returns
|
||||
- 2021-09-24 TUN-5142: Add asynchronous servecontrolstream for QUIC
|
||||
- 2021-09-24 TUN-5142: defer close rpcconn inside unregister instead of ServeControlStream
|
||||
- 2021-09-27 TUN-5160: Set request.ContentLength when this value is in request header
|
||||
|
||||
2021.9.1
|
||||
- 2021-09-21 TUN-5118: Quic connection now detects duplicate connections similar to http2
|
||||
- 2021-09-15 Fix TryCloudflare link
|
||||
|
||||
2021.9.0
|
||||
- 2021-09-02 Fix broken TryCloudflare link
|
||||
- 2021-09-03 Add support for taking named tunnel credentials from an environment variable
|
||||
- 2021-08-30 TUN-5012: Use patched go-sumtype
|
||||
- 2021-08-31 TUN-5011: Use the region parameter in fallback SRV lookup
|
||||
- 2021-08-31 TUN-5029: Do not strip cf- prefixed headers
|
||||
- 2021-08-29 TUN-5009: Updated github action to use go 1.17.x for checks
|
||||
- 2021-08-28 TUN-5010: --region should be a string flag
|
||||
- 2021-08-10 Allow building on arm64 platforms
|
||||
- 2021-06-09 Update README.md
|
||||
- 2021-05-31 🖌️ Allow providing TokenID and TokenSecret as env vars when calling cloudflared access
|
||||
- 2021-05-31 🎨 Prefix env var parameters with TUNNEL
|
||||
|
||||
2021.8.7
|
||||
- 2021-08-28 Revert "TUN-4926: Implement --region configuration option"
|
||||
|
||||
2021.8.6
|
||||
- 2021-08-27 TUN-5000: De-flake logging to dir component test in Windows by increasing to buffer to cope with more logging
|
||||
- 2021-08-27 TUN-5003: Fix cfsetup for non-FIPS golang version
|
||||
|
||||
2021.8.5
|
||||
- 2021-08-27 TUN-4961: Update quic-go to latest
|
||||
- 2021-08-27 Release 2021.8.4
|
||||
|
||||
2021.8.4
|
||||
- 2021-08-26 TUN-4974: Fix regression where we were debug logging by accident
|
||||
- 2021-08-26 TUN-4970: Only default to http2 for warp-routing if protocol is h2mux
|
||||
- 2021-08-26 TUN-4981: Improve readability of prepareTunnelConfig method
|
||||
- 2021-08-26 TUN-4926: Implement --region configuration option
|
||||
- 2021-07-09 TUN-4821: Make quick tunnels the default in cloudflared
|
||||
|
||||
2021.8.3
|
||||
- 2021-08-23 TUN-4889: Add back appendtagheaders function
|
||||
- 2021-08-21 TUN-4940: Fix cloudflared not picking up correct NextProtos for quic
|
||||
- 2021-08-21 TUN-4613: Add a no-op protocol version slot
|
||||
- 2021-08-13 TUN-4922: Downgrade quic-go library to 0.20.0
|
||||
- 2021-08-17 TUN-4866: Add Control Stream for QUIC
|
||||
- 2021-08-17 TUN-4927: Parameterize region in edge discovery code
|
||||
- 2021-08-06 TUN-4602: Added UDP resolves to Edge discovery
|
||||
|
||||
2021.8.2
|
||||
- 2021-08-03 TUN-4597: Added HTTPProxy for QUIC
|
||||
- 2021-08-04 TUN-4795: Remove Equinox releases
|
||||
- 2021-08-09 TUN-4911: Append Environment variable to Path instead of overwriting it
|
||||
|
||||
2021.8.1
|
||||
- 2021-08-02 TUN-4855: Added CHANGES.md for release 2021.8.0
|
||||
- 2021-08-03 TUN-4597: Add a QUIC server skeleton
|
||||
- 2021-08-03 TUN-4873: Disable unix domain socket test for windows unit tests
|
||||
- 2021-08-04 TUN-4875: Added amd64-linux builds back to releases
|
||||
|
||||
2021.8.0
|
||||
- 2021-07-30 TUN-4847: Allow to list tunnels by prefix name or exclusion prefix name
|
||||
- 2021-07-30 TUN-4772: Release built executables with packages
|
||||
- 2021-07-30 TUN-4851: Component tests to smoke test that Proxy DNS and Tunnel are only run when expected
|
||||
- 2021-07-28 TUN-4811: Publish quick tunnels' hostname in /metrics under `userHostname` for backwards-compatibility
|
||||
- 2021-07-29 TUN-4832: Prevent tunnel from running accidentally when only proxy-dns should run
|
||||
- 2021-07-28 TUN-4819: Tolerate protocol TXT record lookup failing
|
||||
|
||||
2021.7.4
|
||||
- 2021-07-28 TUN-4814: Revert "TUN-4699: Make quick tunnels the default in cloudflared"
|
||||
- 2021-07-28 TUN-4812: Disable CGO for cloudflared builds
|
||||
|
||||
2021.7.3
|
||||
- 2021-07-27 TUN-4799: Build deb, msi and rpm packages with fips
|
||||
|
||||
2021.7.2
|
||||
- 2021-07-27 Fixed a syntax error with python logging.
|
||||
|
||||
2021.7.1
|
||||
- 2021-07-21 TUN-4755: Add a windows msi release option to Make
|
||||
- 2021-07-22 TUN-4761: Added a build-all-packages target to cfsetup
|
||||
- 2021-07-26 TUN-4771: Upload deb, rpm and msi packages to github
|
||||
- 2021-07-14 TUN-4714: Name nightly package cloudflared-nightly to avoid apt conflict
|
||||
- 2021-07-16 TUN-4701: Split Proxy into ProxyHTTP and ProxyTCP
|
||||
- 2021-07-08 TUN-4596: Add QUIC application protocol for QUIC stream handshake
|
||||
- 2021-07-09 TUN-4699: Make quick tunnels the default in cloudflared
|
||||
|
||||
2021.7.0
|
||||
- 2021-07-01 TUN-4626: Proxy non-stream based origin websockets with http Roundtrip.
|
||||
- 2021-07-01 TUN-4655: ingress.StreamBasedProxy.EstablishConnection takes dest input
|
||||
- 2021-07-09 TUN-4698: Add cloudflared metrics endpoint to serve quick tunnel hostname
|
||||
- 2021-06-21 TUN-4521: Modify cloudflared to use zoneless-tunnels-worker for free tunnels
|
||||
- 2021-04-05 AUTH-3475: Updated GetAppInfo error message
|
||||
|
||||
2021.6.0
|
||||
- 2021-06-21 TUN-4571: Changelog for 2021.6.0
|
||||
- 2021-06-18 TUN-4571: Fix proxying to unix sockets when using HTTP2 transport to Cloudflare Edge
|
||||
- 2021-06-07 TUN-4502: Make `cloudflared tunnel route` subcommands described consistently
|
||||
- 2021-06-08 TUN-4504: Fix component tests in windows
|
||||
- 2021-05-27 TUN-4461: Log resulting DNS hostname if one is received from Cloudflare API
|
||||
|
||||
2021.5.10
|
||||
- 2021-05-25 TUN-4456: Replaced instances of Tick() with Ticker() in h2mux paths
|
||||
|
||||
2021.5.9
|
||||
- 2021-05-20 TUN-4426: Fix centos builds
|
||||
- 2021-05-20 Update changelog
|
||||
- 2021-04-30 AUTH-3426: Point to new transfer service URL and eliminate PUT /ok
|
||||
|
||||
2021.5.8
|
||||
- 2021-05-14 TUN-4419: Improve error message when cloudflared cannot reach origin
|
||||
- 2021-05-19 TUN-4425: --overwrite-dns flag for in adhoc and route dns cmds
|
||||
|
||||
2021.5.7
|
||||
- 2021-05-17 Fix typo in Changes.md
|
||||
- 2021-05-17 TUN-4421: Named Tunnels will automatically select the protocol to connect to Cloudflare's edge network
|
||||
|
||||
2021.5.6
|
||||
- 2021-05-14 TUN-4418: Downgrade to Go 1.16.3
|
||||
|
||||
2021.5.5
|
||||
|
||||
|
||||
2021.5.4
|
||||
- Fix release pipeline
|
||||
|
||||
2021.5.1
|
||||
- 2021-05-10 TUN-4342: Fix false positive warning about unused hostname property
|
||||
- 2021-05-10 Release 2021.5.0
|
||||
|
||||
2021.5.0
|
||||
- 2021-05-10 TUN-4384: Silence log from automaxprocs
|
||||
- 2021-05-10 AUTH-3537: AUDs in JWTs are now always arrays
|
||||
- 2021-05-10 Update changelog for 2021.5.0
|
||||
- 2021-05-03 TUN-4343: Fix broken build by setting debug field correctly
|
||||
- 2021-05-06 TUN-4356: Set AUTOMAXPROCS to the CPU limit when running in a Linux container
|
||||
- 2021-05-06 TUN-4357: Bump Go to 1.16
|
||||
- 2021-05-06 TUN-4359: Warn about unused keys in 'tunnel ingress validate'
|
||||
- 2021-04-30 debug: log host / path
|
||||
- 2021-04-20 AUTH-3513: Checks header for app info in case response is a 403/401 from the edge
|
||||
- 2021-04-29 TUN-4000: Release notes for cloudflared replica model
|
||||
- 2021-04-09 TUN-2853: rename STDIN-CONTROL env var to STDIN_CONTROL
|
||||
- 2021-04-09 TUN-4206: Better error message when user is only using one ingress rule
|
||||
|
||||
2021.4.0
|
||||
- 2021-04-05 TUN-4178: Fix component test for running as a service in MacOS to not assume a named tunnel
|
||||
- 2021-04-05 TUN-4177: Running with proxy-dns should not prevent running Named Tunnels
|
||||
- 2021-04-02 TUN-4168: Transparently proxy websocket connections using stdlib HTTP client instead of gorilla/websocket; move websocket client code into carrier package since it's only used by access subcommands now (#345).
|
||||
- 2021-04-07 Publish change log for 2021.4.0
|
||||
|
||||
2021.3.6
|
||||
- 2021-03-30 TUN-4150: Only show the connector table in 'tunnel info' if there are connectors. Don't show rows with zero connections.
|
||||
- 2021-03-31 TUN-4153: Revert best-effort HTTP2 usage when talking to origins
|
||||
- 2021-03-26 TUN-4141: Better error messages for tunnel info subcommand.
|
||||
- 2021-03-29 TUN-4146: Unhide and document grace-period
|
||||
- 2021-03-25 TUN-3863: Consolidate header handling logic in the connection package; move headers definitions from h2mux to packages that manage them; cleanup header conversions
|
||||
|
||||
2021.3.5
|
||||
- 2021-03-26 TUN-3896: http-service and tunnelstore client use http2 transport.
|
||||
- 2021-03-25 TUN-4125: Change component tests to run in CI with its own dedicated resources
|
||||
- 2021-03-26 Publish change log for 2021.3.5
|
||||
|
||||
2021.3.4
|
||||
|
||||
|
||||
2021.3.3
|
||||
- 2021-03-23 TUN-4111: Warn the user if both properties "tunnel" and "hostname" are used
|
||||
- 2021-03-23 TUN-4082: Test logging when running as a service
|
||||
- 2021-03-23 TUN-4112: Skip testing graceful shutdown with SIGINT on Windows
|
||||
- 2021-03-23 TUN-4116: Ingore credentials-file setting in configuration file during tunnel create and delete opeations.
|
||||
- 2021-03-23 TUN-4118: Don't overwrite existing file with tunnel credentials. For ad-hoc tunnels, this means tunnel won't start if there's a file in the way.
|
||||
- 2021-03-24 TUN-4123: Don't capture output in reconnect componet test
|
||||
- 2021-03-23 TUN-4067: Reformat code for consistent import order, grouping, and fix formatting. Added goimports target to the Makefile to make this easier in the future.
|
||||
- 2021-03-24 AUTH-3455: Generate short-lived ssh cert per hostname
|
||||
- 2021-03-25 Update changelog 2021.3.3
|
||||
|
||||
2021.3.2
|
||||
- 2021-03-23 TUN-4042: Capture cloudflared output to debug component tests
|
||||
- 2021-03-23 Publish changelog for 2021.3.2
|
||||
- 2021-03-16 TUN-4089: Address flakiness in component tests for termination
|
||||
- 2021-03-16 TUN-4060: Fix Go Vet warnings (new with go 1.16) where t.Fatalf is called from a test goroutine
|
||||
- 2021-03-16 TUN-4091: Use flaky decorator to rerun reconnect component tests when they fail
|
||||
- 2021-03-12 TUN-4081: Update log severities to use Zerolog's levels
|
||||
- 2021-03-16 TUN-4094: Don't read configuration file for access commands
|
||||
- 2021-03-15 TUN-3993: New `cloudflared tunnel info` to obtain details about the active connectors for a tunnel
|
||||
- 2021-03-17 TUN-3715: Apply input source to the correct context
|
||||
- 2021-03-17 AUTH-3394: Ensure scheme on token command
|
||||
- 2021-03-18 TUN-4096: Reduce tunnel not connected assertion backoff to address flaky termination tests
|
||||
- 2021-03-19 TUN-3998: Allow to cleanup the connections of a tunnel limited to a single client
|
||||
- 2021-02-04 TUN-3715: Only read config file once, right before invoking the command
|
||||
|
||||
2021.3.1
|
||||
- 2021-03-11 TUN-4051: Add component-tests to test graceful shutdown
|
||||
- 2021-03-12 TUN-4052: Add component tests to assert service mode behavior
|
||||
|
||||
2021.3.0
|
||||
- 2021-03-10 TUN-4075: Dedup test should not compare order of list
|
||||
- 2021-03-10 Revert "AUTH-3394: Creates a token per app instead of per path"
|
||||
- 2021-03-11 TUN-4066: Remove unnecessary chmod during package publish to CF_PKG_HOSTS
|
||||
- 2021-03-11 TUN-4066: Set permissions in build agent before 'scp'-ing to machine hosting package repo
|
||||
- 2021-03-11 TUN-4050: Add component tests to assert reconnect behavior
|
||||
- 2021-03-10 AUTH-3394: Creates a token per app instead of per path - with fix for free tunnels
|
||||
- 2021-03-15 Publish change log for 2021.3.0
|
||||
- 2021-03-01 Issue #285 - Makefile does not detect TARGET_ARCH correctly on FreeBSD (#325)
|
||||
- 2021-03-01 TUN-3988: Log why it cannot check if origin cert exists
|
||||
- 2021-03-02 TUN-3995: Optional --features flag for tunnel run.
|
||||
- 2021-03-02 TUN-3994: Log client_id when running a named tunnel
|
||||
- 2021-03-04 TUN-4026: Fix regression where HTTP2 edge transport was no longer propagating control plane errors
|
||||
- 2021-03-05 TUN-4055: Skeleton for component tests
|
||||
- 2021-03-08 TUN-4047: Add cfsetup target to run component test
|
||||
- 2021-03-08 TUN-4016: Delegate decision to update for Worker service
|
||||
- 2021-03-02 TUN-3905: Cannot run go mod vendor in cloudflared due to fips
|
||||
- 2021-03-08 TUN-4063: Cleanup dependencies between packages.
|
||||
- 2021-03-09 Allow partial reads from a GorillaConn; add SetDeadline (from net.Conn) (#330)
|
||||
- 2021-03-09 TUN-4069: Fix regression on support for websocket over proxy
|
||||
- 2021-03-02 AUTH-3394: Creates a token per app instead of per path
|
||||
- 2021-03-01 TUN-4017: Add support for using cloudflared as a full socks proxy.
|
||||
- 2021-03-08 TUN-4062: Read component tests config from yaml file
|
||||
- 2021-03-08 TUN-4049: Add component tests to assert logging behavior when running from terminal
|
||||
- 2021-02-23 TUN-3963: Repoint urfave/cli/v2 library at patched branch at github.com/ipostelnik/cli/v2@fixed which correctly handles reading flags declared at multiple levels of subcommands.
|
||||
- 2021-02-25 TUN-3970: Route ip show has alias route ip list
|
||||
- 2021-02-26 TUN-3978: Unhide teamnet commands and improve their help
|
||||
- 2021-02-26 TUN-3983: Renew CA certs in cloudflared
|
||||
- 2021-02-28 TUN-3989: Check in with Updater service in more situations and convey messages to user
|
||||
- 2021-02-11 TUN-3819: Remove client-side check that deleted tunnels have no connections
|
||||
|
||||
2021.2.5
|
||||
- 2021-02-23 Publish change notes for 2021.2.5
|
||||
- 2021-02-11 TUN-3838: ResponseWriter no longer reads and origin error tests
|
||||
- 2021-02-10 TUN-3895: Tests for socks stream handler
|
||||
- 2021-02-19 TUN-3939: Add logging that shows that Warp-routing is enabled
|
||||
- 2021-02-02 TUN-3817: Adds tests for websocket based streaming regression
|
||||
- 2021-02-04 TUN-3799: extended the Stream interface to take a logger and added debug logs for io.Copy errors
|
||||
- 2021-02-03 TUN-3855: Add ability to override target of 'access ssh' command to a different host for testing
|
||||
- 2021-02-04 TUN-3853: Respond with ws headers from the origin service rather than generating our own
|
||||
- 2021-02-08 TUN-3889: Move host header override logic to httpService
|
||||
- 2021-02-05 TUN-3868: Refactor singleTCPService and bridgeService to tcpOverWSService and rawTCPService
|
||||
- 2021-01-21 TUN-3753: Select http2 protocol when warp routing is enabled
|
||||
- 2021-01-26 TUN-3809: Allow routes ip show to output as JSON or YAML
|
||||
- 2021-01-11 TUN-3615: added support to proxy tcp streams
|
||||
- 2021-01-17 TUN-3725: Warp-routing is independent of ingress
|
||||
- 2021-01-15 TUN-3764: Actively flush data for TCP streams
|
||||
- 2020-12-09 TUN-3617: Separate service from client, and implement different client for http vs. tcp origins
|
||||
|
||||
2021.2.4
|
||||
- 2021-02-22 TUN-3948: Log error when retrying connection
|
||||
- 2021-02-23 TUN-3964: Revert "TUN-3922: Repoint urfave/cli/v2 library at patched branch at github.com/ipostelnik/cli/v2@fixed which correctly handles reading flags declared at multiple levels of subcommands."
|
||||
- 2021-02-23 Publish release notes for 2021.2.4
|
||||
|
||||
2021.2.3
|
||||
- 2021-02-23 Publish release notes for 2021.2.3
|
||||
- 2021-02-10 TUN-3902: Add jitter to backoffhandler
|
||||
- 2021-02-11 TUN-3913: Help gives wrong exit code for autoupdate
|
||||
- 2021-02-12 Add max upstream connections dns-proxy option (#290)
|
||||
- 2021-02-16 TUN-3924: Removed db-connect command. Added a placeholder handler for this command that informs users that command is no longer supported.
|
||||
- 2021-02-12 TUN-3922: Repoint urfave/cli/v2 library at patched branch at github.com/ipostelnik/cli/v2@fixed which correctly handles reading flags declared at multiple levels of subcommands.
|
||||
- 2021-02-19 Added support for proxy (#318)
|
||||
- 2021-02-19 TUN-3945: Fix runApp signature for generic service
|
||||
- 2021-02-09 Update README.md
|
||||
- 2021-02-09 Update the TryCloudflare link
|
||||
|
||||
2021.2.2
|
||||
- 2021-02-04 TUN-3864: Users can choose where credentials file is written after creating a tunnel
|
||||
- 2021-02-04 TUN-3869: Improve reliability of graceful shutdown.
|
||||
- 2021-02-07 TUN-3878: Do not supply -tags when none are specified
|
||||
- 2021-02-04 TUN-3635: Send event when unregistering tunnel for gracful shutdown so /ready endpoint reports down status befoe connections finish handling pending requests.
|
||||
- 2021-02-08 TUN-3890: Code coverage for cloudflared in CI
|
||||
- 2021-02-09 AUTH-3375 exchangeOrgToken deleted cookie fix
|
||||
- 2020-11-18 Update error message to use login command
|
||||
|
||||
2021.2.1
|
||||
- 2021-02-04 TUN-3858: Do not suffix cloudflared version with -fips
|
||||
|
||||
2021.2.0
|
||||
- 2021-02-01 TUN-3837: Remove automation_email from cloudflared status page test
|
||||
- 2021-02-03 TUN-3848: Use transport logger for h2mux
|
||||
- 2021-02-03 TUN-3854: cloudflared tunnel list flags to sort output
|
||||
- 2021-01-21 TUN-3195: Don't colorize console logs when stderr is not a terminal
|
||||
- 2021-01-20 Fixed connection error handling by removing duplicated errors, standardizing on non-pointer error types
|
||||
- 2021-01-20 TUN-3118: Changed graceful shutdown to immediately unregister tunnel from the edge, keep the connection open until the edge drops it or grace period expires
|
||||
- 2021-01-25 TUN-3165: Add reference to Argo Tunnel documentation in the help output
|
||||
- 2021-01-25 TUN-3806: Use a .dockerignore
|
||||
- 2021-01-21 TUN-3795: Use RFC-3339 style date format for logs, produce timestamp in UTC
|
||||
- 2021-01-26 TUN-3795: Removed errant test
|
||||
- 2021-01-25 TUN-3792: Handle graceful shutdown correctly when running as a windows service. Only expose one shutdown channel globally, which now triggers the graceful shutdown sequence across all modes. Removed separate handling of zero-duration grace period, instead it's checked only when we need to wait for exit.
|
||||
- 2021-01-27 TUN-3811: Better error reporting on http2 connection termination. Registration errors from control loop are now propagated out of the connection server code. Unified error handling between h2mux and http2 connections so we log and retry errors the same way, regardless of underlying transport.
|
||||
- 2021-01-28 TUN-3830: Use Go 1.15.7
|
||||
- 2021-01-28 TUN-3826: Use go-fips when building cloudflared for linux/amd64
|
||||
- 2021-01-19 TUN-3777: Fix /ready endpoint for classic tunnels
|
||||
- 2021-01-19 TUN-3773: Add back pprof endpoints
|
||||
|
||||
2021.1.5
|
||||
- 2021-01-15 TUN-3594: Log ingress response at debug level
|
||||
- 2021-01-15 TUN-3765: Fix doubly nested log output by `logfile` option
|
||||
- 2021-01-16 TUN-3767: Tolerate logging errors
|
||||
- 2021-01-17 TUN-3768: Reuse file loggers
|
||||
- 2021-01-14 TUN-3738: Refactor observer to avoid potential of blocking on tunnel notifications
|
||||
- 2021-01-15 TUN-3766: Print flags defined at all levels of command hierarchy, not just locally defined flags for a command. This fixes output of overriden settings for subcommand.
|
||||
|
||||
2021.1.4
|
||||
- 2021-01-14 TUN-3759: Single file logging output should always append
|
||||
|
||||
2021.1.3
|
||||
- 2021-01-14 TUN-3756: File logging output must consider the directory
|
||||
- 2021-01-14 TUN-3757: Fix legacy Uint flags that are incorrectly handled by ufarve library
|
||||
|
||||
2021.1.2
|
||||
- 2021-01-13 TUN-3747: Fix logging in Windows
|
||||
|
||||
2021.1.1
|
||||
- 2021-01-13 TUN-3744: Fix compilation error in windows service
|
||||
|
||||
2021.1.0
|
||||
- 2021-01-11 TUN-3670: Update Teamnet API gateway prefixes
|
||||
- 2021-01-13 TUN-3738: Consume UI events even when UI is disabled
|
||||
- 2021-01-06 TUN-3722: Teamnet API paths include /network
|
||||
- 2021-01-05 TUN-3688: Subcommand for users to check which route an IP proxies through
|
||||
- 2021-01-08 TUN-3691: Edit Teamnet help text
|
||||
- 2020-12-30 TUN-3706: Quit if any origin service fails to start
|
||||
- 2020-12-31 TUN-3708: Better info message about system root certpool on Windows
|
||||
- 2020-12-21 TUN-3669: Teamnet commands to add/show Teamnet routes.
|
||||
- 2020-12-29 TUN-3689: Delete routes via cloudflared CLI
|
||||
- 2020-12-28 TUN-3471: Add structured log context to logs
|
||||
- 2020-12-15 TUN-3650: Remove unused awsuploader package
|
||||
- 2020-12-03 Update to add deprecated version note (#271)
|
||||
- 2020-12-02 TUN-3472: Set up rolling logger with zerolog and lumberjack
|
||||
- 2020-12-08 TUN-3607: Set up single-file logger with zerolog
|
||||
- 2020-12-03 Update to add deprecated version note (#271)
|
||||
- 2020-11-25 TUN-3470: Replace in-house logger calls with zerolog
|
||||
|
||||
2020.12.0
|
||||
- 2020-12-04 TUN-3599: improved delete if credentials isnt found.
|
||||
- 2020-12-04 TUN-3612: Upgrade to Go 1.15.6
|
||||
- 2020-11-30 TUN-3593: /ready endpoint for k8s readiness. Move tunnel events out of UI package, into connection package.
|
||||
- 2020-11-27 TUN-3594: Log response status at debug level
|
||||
|
||||
2020.11.11
|
||||
- 2020-11-20 TUN-3578: cloudflared tunnel route dns should allow wildcard subdomains
|
||||
- 2020-11-21 EDGEPLAT-2958 remove deb-compression, defaulting to gzip
|
||||
- 2020-11-23 TUN-3581: Tunnels can be run by name using only --credentials-file, no origin cert necessary.
|
||||
- 2020-11-15 TUN-3561: Unified logger configuration
|
||||
- 2020-11-08 AUTH-3221: Saves org token to disk and uses it to refresh the app token
|
||||
|
||||
2020.11.10
|
||||
- 2020-11-20 TUN-3562: Fix panic when using bastion mode ingress rule
|
||||
- 2020-11-20 EDGEPLAT-2958 build cloudflared for Bullseye
|
||||
|
||||
2020.11.9
|
||||
- 2020-11-18 TUN-3557: Detect SSE if content-type starts with text/event-stream
|
||||
- 2020-11-18 TUN-3559: Share response meta header with other packages
|
||||
- 2020-11-18 DEVTOOLS-7936: Remove redundant chgrp from publish
|
||||
- 2020-11-18 TUN-3558: cloudflared allows empty config files
|
||||
- 2020-11-18 TUN-3544: Upgrade to Go 1.15.5
|
||||
|
||||
2020.11.8
|
||||
- 2020-11-17 TUN-3555: Single origin service should default to localhost:8080
|
||||
|
||||
2020.11.7
|
||||
- 2020-11-13 TUN-3514: Stop setting --is-autoupdated flag after autoupdate because it can break named tunnel running in k8s
|
||||
- 2020-11-15 TUN-3548, TUN-3547: Bastion mode can be specified as a service, doesn't require URL.
|
||||
- 2020-11-16 TUN-3549: Use a separate handler for each websocket proxy
|
||||
|
||||
2020.11.6
|
||||
- 2020-11-14 TUN-3546: Fix panic in tlsconfig.LoadOriginCA
|
||||
|
||||
2020.11.5
|
||||
- 2020-11-12 TUN-3540: Better copy in ingress rules error messages
|
||||
- 2020-11-12 DEVTOOLS-7936: Set permissions on public packages
|
||||
- 2020-11-13 TUN-3543: ProxyAddress not using default in single-origin mode
|
||||
|
||||
2020.11.4
|
||||
- 2020-11-11 TUN-3534: Specific error message when credentials file is a .pem not .json
|
||||
- 2020-11-02 TUN-3500: Integrate replace h2mux by http2 work with multiple origin support
|
||||
- 2020-11-09 TUN-3514: Transport logger write to UI when UI is enabled
|
||||
- 2020-10-30 TUN-3490: Make sure OriginClient implementation doesn't write after Proxy return
|
||||
- 2020-10-20 TUN-3403: Unit test for origin/proxy to test serving HTTP and Websocket
|
||||
- 2020-10-23 TUN-3480: Support SSE with http2 connection, and add SSE handler to hello-world server
|
||||
- 2020-10-27 TUN-3489: Add unit tests to cover proxy logic in connection package of cloudflared
|
||||
- 2020-10-16 TUN-3467: Serialize cf-cloudflared-response-meta during package initialization using jsoniter
|
||||
- 2020-10-14 TUN-3456: New protocol option auto to automatically select between http2 and h2mux
|
||||
- 2020-10-14 TUN-3458: Upgrade to http2 when available, fallback to h2mux when we reach max retries
|
||||
- 2020-10-08 TUN-3449: Use flag to select transport protocol implementation
|
||||
- 2020-10-08 TUN-3462: Refactor cloudflared to separate origin from connection
|
||||
- 2020-09-21 TUN-3406: Proxy websocket requests over Go http2
|
||||
- 2020-09-25 TUN-3420: Establish control plane and send RPC over control plane
|
||||
- 2020-09-11 TUN-3400: Use Go HTTP2 library as transport to connect with the edge
|
||||
|
||||
2020.11.3
|
||||
- 2020-11-11 TUN-3533: Set config for single origin ingress
|
||||
|
||||
2020.11.2
|
||||
|
||||
|
||||
2020.11.1
|
||||
- 2020-11-10 TUN-3527: More specific error for invalid YAML/JSON
|
||||
- 2020-11-06 Update README.md (#256)
|
||||
|
||||
2020.11.0
|
||||
- 2020-11-04 TUN-3484: OriginService that responds with configured HTTP status
|
||||
- 2020-11-05 TUN-3505: Response body for status code origin returns EOF on Read
|
||||
- 2020-11-04 TUN-3503: Matching ingress rule should not take port into account
|
||||
- 2020-11-05 TUN-3506: OriginService needs to set request host and scheme for websocket requests
|
||||
- 2020-11-09 TUN-3516: Better error message when parsing invalid YAML config
|
||||
- 2020-11-09 TUN-3522: ingress validate checks that the config file exists
|
||||
- 2020-11-09 TUN-3524: Don't ignore errors from app-level action handler (#248)
|
||||
- 2020-11-09 TUN-3461: Show all origin services in the UI
|
||||
- 2020-10-30 TUN-3494: Proceed to create tunnel if at least one edge address can be resolved
|
||||
- 2020-10-30 TUN-3492: Refactor OriginService, shrink its interface
|
||||
- 2020-10-22 TUN-3478: Increase download timeout to 60s
|
||||
- 2020-10-15 TUN-2640: Users can configure per-origin config. Unify single-rule CLI flow with multi-rule config file code.
|
||||
|
||||
2020.10.2
|
||||
- 2020-10-21 Release 2020.10.1
|
||||
- 2020-10-21 AUTH-3185 fixed indention error
|
||||
- 2020-10-19 TUN-3459: Make service install on linux use named tunnels
|
||||
|
||||
2020.10.1
|
||||
- 2020-10-20 Split out typed config from legacy command-line switches; refactor ingress commands and fix tests
|
||||
- 2020-10-20 Move raw ingress rules to config package
|
||||
- 2020-10-21 TUN-3476: Fix conversion to string and int slice
|
||||
- 2020-10-12 TUN-3441: Multiple-origin routing via ingress rules
|
||||
- 2020-10-15 TUN-3464: Newtype to wrap []ingress.Rule
|
||||
- 2020-10-15 TUN-3465: Use Go 1.15.3
|
||||
- 2020-10-15 TUN-3463: Let users run a named tunnel via config file setting
|
||||
- 2020-10-19 TUN-3475: Unify config file handling with typed config for new fields
|
||||
- 2020-10-19 TUN-3459: Make service install on linux use named tunnels
|
||||
- 2020-10-06 AUTH-3148 fixed cloudflared copy and match all the files in the checksum upload
|
||||
- 2020-10-06 TUN-3436, TUN-3437: Parse ingress from YAML, ensure last rule catches everything
|
||||
- 2020-10-06 TUN-3446: Use go 1.15.2 and add a step to build cloudflared in the dev Dockerfile
|
||||
- 2020-10-07 TUN-3439: 'tunnel validate' command to check ingress rules
|
||||
- 2020-10-07 TUN-3440: 'tunnel rule' command to test ingress rules
|
||||
- 2020-10-08 TUN-3451: Cloudflared tunnel ingress command
|
||||
- 2020-10-09 TUN-3452: Fix loading of flags from config file for tunnel run subcommand. This change also cleans up building of tunnel subcommand list, hides deprecated subcommands and improves help.
|
||||
- 2020-10-08 TUN-3438: move ingress into own package, read into TunnelConfig
|
||||
|
||||
2020.10.0
|
||||
- 2020-10-02 AUTH-2993 cleaned up worker service tests
|
||||
- 2020-10-02 TUN-3443: Decode as v4api response on non-200 status
|
||||
- 2020-09-24 TRAFFIC-448: allow the user to specify the proxy address and port to bind to, falling back to 127.0.0.1 and random port if not specified
|
||||
- 2020-09-28 TUN-3427: Define a struct that only implements RegistrationServer in tunnelpogs
|
||||
- 2020-09-29 TUN-3430: Copy flags to configure proxy to run subcommand, print relevant tunnel flags in help
|
||||
- 2020-08-12 AUTH-2993 added workers updater logic
|
||||
|
||||
2020.9.3
|
||||
- 2020-09-22 TRAFFIC-448: build cloudflare for junos and publish to s3
|
||||
- 2020-09-22 TUN-3410: Request the v1 Tunnelstore API
|
||||
- 2020-09-23 Release 2020.9.2
|
||||
- 2020-09-17 updater service exit code should be 11
|
||||
- 2020-09-18 AUTH-3109 upload the checksum to workers kv on github releases
|
||||
|
||||
2020.9.2
|
||||
- 2020-09-22 TRAFFIC-448: build cloudflare for junos and publish to s3
|
||||
- 2020-09-22 TUN-3410: Request the v1 Tunnelstore API
|
||||
- 2020-09-17 AUTH-3103 CI build fixes
|
||||
- 2020-09-18 AUTH-3110-use-cfsetup-precache
|
||||
- 2020-09-17 TUN-3295: Show route command results
|
||||
- 2020-09-16 TUN-3291: cloudflared tunnel run -h explains how to use flags from parent command
|
||||
- 2020-09-18 AUTH-3109 upload the checksum to workers kv on github releases
|
||||
- 2020-09-17 updater service exit code should be 11
|
||||
- 2020-09-01 TUN-3216: UI improvements
|
||||
- 2020-08-25 Rebased and passed TunnelEventChan to LogServerInfo in new ReconnectTunnel function
|
||||
- 2020-08-25 TUN-3321: Add box around logs on UI
|
||||
- 2020-08-26 TUN-3328: Filter out free tunnel has started log from UI
|
||||
- 2020-08-27 TUN-3333: Add text to UI explaining how to exit
|
||||
- 2020-08-27 TUN-3335: Dynamically set connection table size for UI
|
||||
- 2020-08-10 TUN-3238: Update UI when connection re-connects
|
||||
- 2020-08-17 TUN-3261: Display connections on UI for free classic tunnels
|
||||
- 2020-07-24 TUN-3201: Create base cloudflared UI structure
|
||||
- 2020-07-29 TUN-3200: Add connection information to UI
|
||||
- 2020-07-24 TUN-3255: Update UI to display URL instead of hostname
|
||||
- 2020-07-29 TUN-3198: Handle errors while running tunnel UI
|
||||
|
||||
2020.9.1
|
||||
- 2020-09-14 TUN-3395: Unhide named tunnel subcommands, tweak help
|
||||
- 2020-09-15 TUN-3395: Improve help for list command
|
||||
- 2020-09-14 TUN-3294: Perform basic validation on arguments of route command; remove default pool name which wasn't valid
|
||||
- 2020-09-15 TUN-3395: Improve help for list command
|
||||
- 2020-09-16 Use Go 1.15.2
|
||||
|
||||
2020.9.0
|
||||
- 2020-09-11 TUN-3293: Try to use error information from the body of a failed tunnelstore reresponse if available
|
||||
- 2020-09-04 AUTH-2653 renabled signing
|
||||
- 2020-09-04 TUN-3377: Tunnel route should check dns/lb before checking tunnel ID
|
||||
- 2020-09-04 AUTH-2653 changed to proper file extension
|
||||
- 2020-09-04 AUTH-2653 handle duplicate key import errors
|
||||
- 2020-09-04 TUN-3345: tunnel run accepts name of tunnel as argument
|
||||
- 2020-09-08 AUTH-2653 disble error pipe to see what is failing
|
||||
- 2020-09-08 AUTH-2653 search for the certificate and not the identity
|
||||
- 2020-09-09 TUN-3284: Use cloudflared/<version> as user agent of tunnelstore client
|
||||
- 2020-09-09 TUN-3375: Upgrade x/text and gorilla websocket deps
|
||||
- 2020-09-09 TUN-3375: Upgrade coredns and prometheus dependencies
|
||||
- 2020-09-09 AUTH-2653 add notarization to mac build
|
||||
- 2020-09-08 TUN-3292: Mention cleanup in tunnel run help.
|
||||
- 2020-08-20 AUTH-2016 fixed variable fail
|
||||
- 2020-08-12 TUN-3352 extra debug logging for websockets
|
||||
|
||||
2020.8.2
|
||||
- 2020-08-20 AUTH-3021 fixed the git version call by using the older flag
|
||||
- 2020-08-18 TUN-3268: Each connection has its own event digest to reconnect
|
||||
|
||||
2020.8.1
|
||||
- 2020-08-14 AUTH-2975 don't check /etc on windows
|
||||
- 2020-08-14 AUTH-2977 log file protection
|
||||
- 2020-08-18 TUN-3286: Use either ID or name in Named Tunnel subcommands.
|
||||
- 2020-08-18 AUTH-2712 fixed the mac build script
|
||||
- 2020-08-19 AUTH-2653 disabling signing until we can get the keys
|
||||
- 2020-08-05 TUN-3233: List tunnels support filtering by deleted, name, existed at and id
|
||||
- 2020-08-05 TUN-3237: By default, don't show connections that are pending reconnect
|
||||
- 2020-08-06 TUN-3242: Build with go 1.14
|
||||
- 2020-08-07 TUN-3243: Refactor tunnel subcommands to allow commands to compose better
|
||||
- 2020-08-07 AUTH-2864 - add macos build to github release
|
||||
- 2020-07-31 AUTH-2857 update homebrew script to use new url
|
||||
- 2020-07-30 TUN-3213: Create, route and run named tunnels in one command
|
||||
- 2020-07-07 AUTH-2712 added MSI build for a windows agent
|
||||
|
||||
2020.8.0
|
||||
- 2020-07-30 TUN-3220: tunnel route reports created route
|
||||
- 2020-07-31 TUN-3221: ConnectionOptions tracks numPreviousAttempts.
|
||||
- 2020-07-20 TUN-3190: Initialize logger using command line flags in tunnels subcommands
|
||||
- 2020-07-21 TUN-3192: Use zone ID in tunnelstore request path; improve debug logging
|
||||
- 2020-07-23 TUN-3194: Don't render log output when level is not enabled
|
||||
- 2020-07-22 AUTH-2016 adds sha256 hashes to releases
|
||||
- 2020-07-27 Removes centos 6 build
|
||||
- 2020-07-27 TUN-3209: Add benchmark for header serialization
|
||||
- 2020-07-24 TUN-3209: improve performance and reduce allocations during user header serialization from h1 to h2
|
||||
- 2020-07-26 TUN-3208: Add benchmark for large response write
|
||||
- 2020-07-27 TUN-3208: Reduce copies and allocations on h2mux write path. Pre-allocate 16KB write buffer on the first write if possible. Use explicit byte array for chunks on write thread to avoid copying through intermediate buffer due to io.CopyN.
|
||||
- 2020-07-28 AUTH-2714: Adds arm64 cloudflared build
|
||||
- 2020-07-29 AUTH-2927 run message update after all github builds are done
|
||||
- 2020-07-17 AUTH-2902 redirect with just the root host on curl commands
|
||||
|
||||
2020.7.4
|
||||
- 2020-07-20 Build cloudflared for arm64 on native agents
|
||||
- 2020-07-10 TUN-3048: Handle error when user tries to delete active tunnel
|
||||
- 2020-07-14 AUTH-2890: adds error handler to cli actions
|
||||
- 2020-07-06 TUN-3156: Add route subcommand under tunnel
|
||||
|
||||
2020.7.3
|
||||
- 2020-07-13 Change scp command to use file glob that matches both cloudflared rpms and debs
|
||||
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
package awsuploader
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
// DirectoryUploadManager is used to manage file uploads on an interval from a directory
|
||||
type DirectoryUploadManager struct {
|
||||
logger logger.Service
|
||||
uploader Uploader
|
||||
rootDirectory string
|
||||
sweepInterval time.Duration
|
||||
ticker *time.Ticker
|
||||
shutdownC chan struct{}
|
||||
workQueue chan string
|
||||
}
|
||||
|
||||
// NewDirectoryUploadManager create a new DirectoryUploadManager
|
||||
// uploader is an Uploader to use as an actual uploading engine
|
||||
// directory is the directory to sweep for files to upload
|
||||
// sweepInterval is how often to iterate the directory and upload the files within
|
||||
func NewDirectoryUploadManager(logger logger.Service, uploader Uploader, directory string, sweepInterval time.Duration, shutdownC chan struct{}) *DirectoryUploadManager {
|
||||
workerCount := 10
|
||||
manager := &DirectoryUploadManager{
|
||||
logger: logger,
|
||||
uploader: uploader,
|
||||
rootDirectory: directory,
|
||||
sweepInterval: sweepInterval,
|
||||
shutdownC: shutdownC,
|
||||
workQueue: make(chan string, workerCount),
|
||||
}
|
||||
|
||||
//start workers
|
||||
for i := 0; i < workerCount; i++ {
|
||||
go manager.worker()
|
||||
}
|
||||
|
||||
return manager
|
||||
}
|
||||
|
||||
// Upload a file using the uploader
|
||||
// This is useful for "out of band" uploads that need to be triggered immediately instead of waiting for the sweep
|
||||
func (m *DirectoryUploadManager) Upload(filepath string) error {
|
||||
return m.uploader.Upload(filepath)
|
||||
}
|
||||
|
||||
// Start the upload ticker to walk the directories
|
||||
func (m *DirectoryUploadManager) Start() {
|
||||
m.ticker = time.NewTicker(m.sweepInterval)
|
||||
go m.run()
|
||||
}
|
||||
|
||||
func (m *DirectoryUploadManager) run() {
|
||||
for {
|
||||
select {
|
||||
case <-m.shutdownC:
|
||||
m.ticker.Stop()
|
||||
return
|
||||
case <-m.ticker.C:
|
||||
m.sweep()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sweep the directory and kick off uploads
|
||||
func (m *DirectoryUploadManager) sweep() {
|
||||
filepath.Walk(m.rootDirectory, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
//30 days ago
|
||||
retentionTime := 30 * (time.Hour * 24)
|
||||
checkTime := time.Now().Add(-time.Duration(retentionTime))
|
||||
|
||||
//delete the file it is stale
|
||||
if info.ModTime().Before(checkTime) {
|
||||
os.Remove(path)
|
||||
return nil
|
||||
}
|
||||
//add the upload to the work queue
|
||||
go func() {
|
||||
m.workQueue <- path
|
||||
}()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// worker handles upload requests
|
||||
func (m *DirectoryUploadManager) worker() {
|
||||
for {
|
||||
select {
|
||||
case <-m.shutdownC:
|
||||
return
|
||||
case filepath := <-m.workQueue:
|
||||
if err := m.Upload(filepath); err != nil {
|
||||
m.logger.Errorf("Cannot upload file to s3 bucket: %s", err)
|
||||
} else {
|
||||
os.Remove(filepath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
package awsuploader
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
type MockUploader struct {
|
||||
shouldFail bool
|
||||
}
|
||||
|
||||
func (m *MockUploader) Upload(filepath string) error {
|
||||
if m.shouldFail {
|
||||
return errors.New("upload set to fail")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewMockUploader(shouldFail bool) Uploader {
|
||||
return &MockUploader{shouldFail: shouldFail}
|
||||
}
|
||||
|
||||
func getDirectoryPath(t *testing.T) string {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal("couldn't create the test directory!", err)
|
||||
}
|
||||
return filepath.Join(dir, "uploads")
|
||||
}
|
||||
|
||||
func setupTestDirectory(t *testing.T) string {
|
||||
path := getDirectoryPath(t)
|
||||
os.RemoveAll(path)
|
||||
time.Sleep(100 * time.Millisecond) //short way to wait for the OS to delete the folder
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatal("couldn't create the test directory!", err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func createUploadManager(t *testing.T, shouldFailUpload bool) *DirectoryUploadManager {
|
||||
rootDirectory := setupTestDirectory(t)
|
||||
uploader := NewMockUploader(shouldFailUpload)
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
shutdownC := make(chan struct{})
|
||||
return NewDirectoryUploadManager(logger, uploader, rootDirectory, 1*time.Second, shutdownC)
|
||||
}
|
||||
|
||||
func createFile(t *testing.T, fileName string) (*os.File, string) {
|
||||
path := filepath.Join(getDirectoryPath(t), fileName)
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
t.Fatal("upload to create file for sweep test", err)
|
||||
}
|
||||
return f, path
|
||||
}
|
||||
|
||||
func TestUploadSuccess(t *testing.T) {
|
||||
manager := createUploadManager(t, false)
|
||||
path := filepath.Join(getDirectoryPath(t), "test_file")
|
||||
if err := manager.Upload(path); err != nil {
|
||||
t.Fatal("the upload request method failed", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadFailure(t *testing.T) {
|
||||
manager := createUploadManager(t, true)
|
||||
path := filepath.Join(getDirectoryPath(t), "test_file")
|
||||
if err := manager.Upload(path); err == nil {
|
||||
t.Fatal("the upload request method should have failed and didn't", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSweepSuccess(t *testing.T) {
|
||||
manager := createUploadManager(t, false)
|
||||
f, path := createFile(t, "test_file")
|
||||
defer f.Close()
|
||||
|
||||
manager.Start()
|
||||
time.Sleep(2 * time.Second)
|
||||
if _, err := os.Stat(path); os.IsExist(err) {
|
||||
//the file should have been deleted
|
||||
t.Fatal("the manager failed to delete the file", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSweepFailure(t *testing.T) {
|
||||
manager := createUploadManager(t, true)
|
||||
f, path := createFile(t, "test_file")
|
||||
defer f.Close()
|
||||
|
||||
manager.Start()
|
||||
time.Sleep(2 * time.Second)
|
||||
_, serr := f.Stat()
|
||||
if serr != nil {
|
||||
//the file should still exist
|
||||
os.Remove(path)
|
||||
t.Fatal("the manager failed to delete the file", serr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHighLoad(t *testing.T) {
|
||||
manager := createUploadManager(t, false)
|
||||
for i := 0; i < 30; i++ {
|
||||
f, _ := createFile(t, randomString(6))
|
||||
defer f.Close()
|
||||
}
|
||||
manager.Start()
|
||||
time.Sleep(4 * time.Second)
|
||||
|
||||
directory := getDirectoryPath(t)
|
||||
files, err := ioutil.ReadDir(directory)
|
||||
if err != nil || len(files) > 0 {
|
||||
t.Fatalf("the manager failed to upload all the files: %s files left: %d", err, len(files))
|
||||
}
|
||||
}
|
||||
|
||||
// LowerCase [a-z]
|
||||
const randSet = "abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
// String returns a string of length 'n' from a set of letters 'lset'
|
||||
func randomString(n int) string {
|
||||
b := make([]byte, n)
|
||||
lsetLen := len(randSet)
|
||||
for i := range b {
|
||||
b[i] = randSet[rand.Intn(lsetLen)]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package awsuploader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
)
|
||||
|
||||
//FileUploader aws compliant bucket upload
|
||||
type FileUploader struct {
|
||||
storage *s3.S3
|
||||
bucketName string
|
||||
clientID string
|
||||
secretID string
|
||||
}
|
||||
|
||||
// NewFileUploader creates a new S3 compliant bucket uploader
|
||||
func NewFileUploader(bucketName, region, accessKeyID, secretID, token, s3Host string) (*FileUploader, error) {
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String(region),
|
||||
Credentials: credentials.NewStaticCredentials(accessKeyID, secretID, token),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var storage *s3.S3
|
||||
if s3Host != "" {
|
||||
storage = s3.New(sess, &aws.Config{Endpoint: aws.String(s3Host)})
|
||||
} else {
|
||||
storage = s3.New(sess)
|
||||
}
|
||||
|
||||
return &FileUploader{
|
||||
storage: storage,
|
||||
bucketName: bucketName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Upload a file to the bucket
|
||||
func (u *FileUploader) Upload(filepath string) error {
|
||||
info, err := os.Stat(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, serr := u.storage.PutObjectWithContext(context.Background(), &s3.PutObjectInput{
|
||||
Bucket: aws.String(u.bucketName),
|
||||
Key: aws.String(info.Name()),
|
||||
Body: file,
|
||||
})
|
||||
return serr
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package awsuploader
|
||||
|
||||
// UploadManager is used to manage file uploads on an interval
|
||||
type UploadManager interface {
|
||||
Upload(string) error
|
||||
Start()
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package awsuploader
|
||||
|
||||
// Uploader the functions required to upload to a bucket
|
||||
type Uploader interface {
|
||||
//Upload a file to the bucket
|
||||
Upload(string) error
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
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)
|
||||
}
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
VERSION=$(git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
|
||||
echo $VERSION
|
||||
|
||||
# This controls the directory the built artifacts go into
|
||||
export ARTIFACT_DIR=built_artifacts/
|
||||
mkdir -p $ARTIFACT_DIR
|
||||
|
||||
arch=("amd64")
|
||||
export TARGET_ARCH=$arch
|
||||
export TARGET_OS=linux
|
||||
export FIPS=true
|
||||
# For BoringCrypto to link, we need CGO enabled. Otherwise compilation fails.
|
||||
export CGO_ENABLED=1
|
||||
|
||||
make cloudflared-deb
|
||||
mv cloudflared-fips\_$VERSION\_$arch.deb $ARTIFACT_DIR/cloudflared-fips-linux-$arch.deb
|
||||
|
||||
# rpm packages invert the - and _ and use x86_64 instead of amd64.
|
||||
RPMVERSION=$(echo $VERSION|sed -r 's/-/_/g')
|
||||
RPMARCH="x86_64"
|
||||
make cloudflared-rpm
|
||||
mv cloudflared-fips-$RPMVERSION-1.$RPMARCH.rpm $ARTIFACT_DIR/cloudflared-fips-linux-$RPMARCH.rpm
|
||||
|
||||
# finally move the linux binary as well.
|
||||
mv ./cloudflared $ARTIFACT_DIR/cloudflared-fips-linux-$arch
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
VERSION=$(git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
|
||||
echo $VERSION
|
||||
|
||||
# Avoid depending on C code since we don't need it.
|
||||
export CGO_ENABLED=0
|
||||
|
||||
# This controls the directory the built artifacts go into
|
||||
export ARTIFACT_DIR=built_artifacts/
|
||||
mkdir -p $ARTIFACT_DIR
|
||||
windowsArchs=("amd64" "386")
|
||||
export TARGET_OS=windows
|
||||
for arch in ${windowsArchs[@]}; do
|
||||
export TARGET_ARCH=$arch
|
||||
make cloudflared-msi
|
||||
mv ./cloudflared.exe $ARTIFACT_DIR/cloudflared-windows-$arch.exe
|
||||
mv cloudflared-$VERSION-$arch.msi $ARTIFACT_DIR/cloudflared-windows-$arch.msi
|
||||
done
|
||||
|
||||
|
||||
linuxArchs=("386" "amd64" "arm" "arm64")
|
||||
export TARGET_OS=linux
|
||||
for arch in ${linuxArchs[@]}; do
|
||||
export TARGET_ARCH=$arch
|
||||
make cloudflared-deb
|
||||
mv cloudflared\_$VERSION\_$arch.deb $ARTIFACT_DIR/cloudflared-linux-$arch.deb
|
||||
|
||||
# rpm packages invert the - and _ and use x86_64 instead of amd64.
|
||||
RPMVERSION=$(echo $VERSION|sed -r 's/-/_/g')
|
||||
RPMARCH=$arch
|
||||
if [ $arch == "amd64" ];then
|
||||
RPMARCH="x86_64"
|
||||
fi
|
||||
if [ $arch == "arm64" ]; then
|
||||
RPMARCH="aarch64"
|
||||
fi
|
||||
make cloudflared-rpm
|
||||
mv cloudflared-$RPMVERSION-1.$RPMARCH.rpm $ARTIFACT_DIR/cloudflared-linux-$RPMARCH.rpm
|
||||
|
||||
# finally move the linux binary as well.
|
||||
mv ./cloudflared $ARTIFACT_DIR/cloudflared-linux-$arch
|
||||
done
|
||||
+48
-17
@@ -4,36 +4,44 @@
|
||||
package carrier
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/token"
|
||||
)
|
||||
|
||||
const (
|
||||
LogFieldOriginURL = "originURL"
|
||||
CFAccessTokenHeader = "Cf-Access-Token"
|
||||
cfJumpDestinationHeader = "Cf-Access-Jump-Destination"
|
||||
)
|
||||
|
||||
type StartOptions struct {
|
||||
OriginURL string
|
||||
Headers http.Header
|
||||
AppInfo *token.AppInfo
|
||||
OriginURL string
|
||||
Headers http.Header
|
||||
Host string
|
||||
TLSClientConfig *tls.Config
|
||||
}
|
||||
|
||||
// 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 {
|
||||
}
|
||||
type StdinoutStream struct{}
|
||||
|
||||
// Read will read from Stdin
|
||||
func (c *StdinoutStream) Read(p []byte) (int, error) {
|
||||
@@ -46,10 +54,10 @@ 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
|
||||
// Helper to allow deferring the response close with a check that the resp is not nil
|
||||
func closeRespBody(resp *http.Response) {
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +111,7 @@ func Serve(remoteConn Connection, listener net.Listener, shutdownC <-chan struct
|
||||
// serveConnection handles connections for the Serve() call
|
||||
func serveConnection(remoteConn Connection, c net.Conn, options *StartOptions) {
|
||||
defer c.Close()
|
||||
remoteConn.ServeStream(options, c)
|
||||
_ = remoteConn.ServeStream(options, c)
|
||||
}
|
||||
|
||||
// IsAccessResponse checks the http Response to see if the url location
|
||||
@@ -117,7 +125,7 @@ func IsAccessResponse(resp *http.Response) bool {
|
||||
if err != nil || location == nil {
|
||||
return false
|
||||
}
|
||||
if strings.HasPrefix(location.Path, "/cdn-cgi/access/login") {
|
||||
if strings.HasPrefix(location.Path, token.AccessLoginWorkerPath) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -125,13 +133,13 @@ func IsAccessResponse(resp *http.Response) bool {
|
||||
}
|
||||
|
||||
// BuildAccessRequest builds an HTTP request with the Access token set
|
||||
func BuildAccessRequest(options *StartOptions, logger logger.Service) (*http.Request, error) {
|
||||
func BuildAccessRequest(options *StartOptions, log *zerolog.Logger) (*http.Request, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := token.FetchTokenWithRedirect(req.URL, logger)
|
||||
token, err := token.FetchTokenWithRedirect(req.URL, options.AppInfo, log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -142,7 +150,7 @@ func BuildAccessRequest(options *StartOptions, logger logger.Service) (*http.Req
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
originRequest.Header.Set(h2mux.CFAccessTokenHeader, token)
|
||||
originRequest.Header.Set(CFAccessTokenHeader, token)
|
||||
|
||||
for k, v := range options.Headers {
|
||||
if len(v) >= 1 {
|
||||
@@ -152,3 +160,26 @@ func BuildAccessRequest(options *StartOptions, logger logger.Service) (*http.Req
|
||||
|
||||
return originRequest, nil
|
||||
}
|
||||
|
||||
func SetBastionDest(header http.Header, destination string) {
|
||||
if destination != "" {
|
||||
header.Set(cfJumpDestinationHeader, destination)
|
||||
}
|
||||
}
|
||||
|
||||
func ResolveBastionDest(r *http.Request) (string, error) {
|
||||
jumpDestination := r.Header.Get(cfJumpDestinationHeader)
|
||||
if jumpDestination == "" {
|
||||
return "", fmt.Errorf("Did not receive final destination from client. The --destination flag is likely not set on the client side")
|
||||
}
|
||||
// Strip scheme and path set by client. Without a scheme
|
||||
// Parsing a hostname and path without scheme might not return an error due to parsing ambiguities
|
||||
if jumpURL, err := url.Parse(jumpDestination); err == nil && jumpURL.Host != "" {
|
||||
return removePath(jumpURL.Host), nil
|
||||
}
|
||||
return removePath(jumpDestination), nil
|
||||
}
|
||||
|
||||
func removePath(dest string) string {
|
||||
return strings.SplitN(dest, "/", 2)[0]
|
||||
}
|
||||
|
||||
+107
-10
@@ -9,8 +9,8 @@ import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
ws "github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -43,8 +43,8 @@ func (s *testStreamer) Write(p []byte) (int, error) {
|
||||
|
||||
func TestStartClient(t *testing.T) {
|
||||
message := "Good morning Austin! Time for another sunny day in the great state of Texas."
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
wsConn := NewWSConnection(logger, false)
|
||||
log := zerolog.Nop()
|
||||
wsConn := NewWSConnection(&log)
|
||||
ts := newTestWebSocketServer()
|
||||
defer ts.Close()
|
||||
|
||||
@@ -55,10 +55,10 @@ func TestStartClient(t *testing.T) {
|
||||
}
|
||||
err := StartClient(wsConn, buf, options)
|
||||
assert.NoError(t, err)
|
||||
buf.Write([]byte(message))
|
||||
_, _ = buf.Write([]byte(message))
|
||||
|
||||
readBuffer := make([]byte, len(message))
|
||||
buf.Read(readBuffer)
|
||||
_, _ = buf.Read(readBuffer)
|
||||
assert.Equal(t, message, string(readBuffer))
|
||||
}
|
||||
|
||||
@@ -68,9 +68,9 @@ func TestStartServer(t *testing.T) {
|
||||
t.Fatalf("Error starting listener: %v", err)
|
||||
}
|
||||
message := "Good morning Austin! Time for another sunny day in the great state of Texas."
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
log := zerolog.Nop()
|
||||
shutdownC := make(chan struct{})
|
||||
wsConn := NewWSConnection(logger, false)
|
||||
wsConn := NewWSConnection(&log)
|
||||
ts := newTestWebSocketServer()
|
||||
defer ts.Close()
|
||||
options := &StartOptions{
|
||||
@@ -81,15 +81,16 @@ func TestStartServer(t *testing.T) {
|
||||
go func() {
|
||||
err := Serve(wsConn, listener, shutdownC, options)
|
||||
if err != nil {
|
||||
t.Fatalf("Error running server: %v", err)
|
||||
t.Errorf("Error running server: %v", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
conn, err := net.Dial("tcp", listener.Addr().String())
|
||||
conn.Write([]byte(message))
|
||||
_, _ = conn.Write([]byte(message))
|
||||
|
||||
readBuffer := make([]byte, len(message))
|
||||
conn.Read(readBuffer)
|
||||
_, _ = conn.Read(readBuffer)
|
||||
assert.Equal(t, string(readBuffer), message)
|
||||
}
|
||||
|
||||
@@ -155,3 +156,99 @@ func testRequest(t *testing.T, url string, stream io.ReadWriter) *http.Request {
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func TestBastionDestination(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
header http.Header
|
||||
expectedDest string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "hostname destination",
|
||||
header: http.Header{
|
||||
cfJumpDestinationHeader: []string{"localhost"},
|
||||
},
|
||||
expectedDest: "localhost",
|
||||
},
|
||||
{
|
||||
name: "hostname destination with port",
|
||||
header: http.Header{
|
||||
cfJumpDestinationHeader: []string{"localhost:9000"},
|
||||
},
|
||||
expectedDest: "localhost:9000",
|
||||
},
|
||||
{
|
||||
name: "hostname destination with scheme and port",
|
||||
header: http.Header{
|
||||
cfJumpDestinationHeader: []string{"ssh://localhost:9000"},
|
||||
},
|
||||
expectedDest: "localhost:9000",
|
||||
},
|
||||
{
|
||||
name: "full hostname url",
|
||||
header: http.Header{
|
||||
cfJumpDestinationHeader: []string{"ssh://localhost:9000/metrics"},
|
||||
},
|
||||
expectedDest: "localhost:9000",
|
||||
},
|
||||
{
|
||||
name: "hostname destination with port and path",
|
||||
header: http.Header{
|
||||
cfJumpDestinationHeader: []string{"localhost:9000/metrics"},
|
||||
},
|
||||
expectedDest: "localhost:9000",
|
||||
},
|
||||
{
|
||||
name: "ip destination",
|
||||
header: http.Header{
|
||||
cfJumpDestinationHeader: []string{"127.0.0.1"},
|
||||
},
|
||||
expectedDest: "127.0.0.1",
|
||||
},
|
||||
{
|
||||
name: "ip destination with port",
|
||||
header: http.Header{
|
||||
cfJumpDestinationHeader: []string{"127.0.0.1:9000"},
|
||||
},
|
||||
expectedDest: "127.0.0.1:9000",
|
||||
},
|
||||
{
|
||||
name: "ip destination with port and path",
|
||||
header: http.Header{
|
||||
cfJumpDestinationHeader: []string{"127.0.0.1:9000/metrics"},
|
||||
},
|
||||
expectedDest: "127.0.0.1:9000",
|
||||
},
|
||||
{
|
||||
name: "ip destination with schem and port",
|
||||
header: http.Header{
|
||||
cfJumpDestinationHeader: []string{"tcp://127.0.0.1:9000"},
|
||||
},
|
||||
expectedDest: "127.0.0.1:9000",
|
||||
},
|
||||
{
|
||||
name: "full ip url",
|
||||
header: http.Header{
|
||||
cfJumpDestinationHeader: []string{"ssh://127.0.0.1:9000/metrics"},
|
||||
},
|
||||
expectedDest: "127.0.0.1:9000",
|
||||
},
|
||||
{
|
||||
name: "no destination",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
r := &http.Request{
|
||||
Header: test.header,
|
||||
}
|
||||
dest, err := ResolveBastionDest(r)
|
||||
if test.wantErr {
|
||||
assert.Error(t, err, "Test %s expects error", test.name)
|
||||
} else {
|
||||
assert.NoError(t, err, "Test %s expects no error, got error %v", test.name, err)
|
||||
assert.Equal(t, test.expectedDest, dest, "Test %s expect dest %s, got %s", test.name, test.expectedDest, dest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+112
-58
@@ -1,93 +1,83 @@
|
||||
package carrier
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/socks"
|
||||
cfwebsocket "github.com/cloudflare/cloudflared/websocket"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/token"
|
||||
cfwebsocket "github.com/cloudflare/cloudflared/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
|
||||
log *zerolog.Logger
|
||||
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 {
|
||||
func NewWSConnection(log *zerolog.Logger) Connection {
|
||||
return &Websocket{
|
||||
logger: logger,
|
||||
isSocks: isSocks,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// ServeStream will create a Websocket client stream connection to the edge
|
||||
// it blocks and writes the raw data from conn over the tunnel
|
||||
func (ws *Websocket) ServeStream(options *StartOptions, conn io.ReadWriter) error {
|
||||
wsConn, err := createWebsocketStream(options, ws.logger)
|
||||
wsConn, err := createWebsocketStream(options, ws.log)
|
||||
if err != nil {
|
||||
ws.logger.Errorf("failed to connect to %s with error: %s", options.OriginURL, err)
|
||||
ws.log.Err(err).Str(LogFieldOriginURL, options.OriginURL).Msg("failed to connect to origin")
|
||||
return err
|
||||
}
|
||||
defer wsConn.Close()
|
||||
|
||||
if ws.isSocks {
|
||||
dialer := &wsdialer{conn: wsConn}
|
||||
requestHandler := socks.NewRequestHandler(dialer)
|
||||
socksServer := socks.NewConnectionHandler(requestHandler)
|
||||
|
||||
socksServer.Serve(conn)
|
||||
} else {
|
||||
cfwebsocket.Stream(wsConn, conn)
|
||||
}
|
||||
cfwebsocket.Stream(wsConn, conn, ws.log)
|
||||
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) {
|
||||
func createWebsocketStream(options *StartOptions, log *zerolog.Logger) (*cfwebsocket.GorillaConn, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header = options.Headers
|
||||
if options.Host != "" {
|
||||
req.Host = options.Host
|
||||
}
|
||||
|
||||
dump, err := httputil.DumpRequest(req, false)
|
||||
logger.Debugf("Websocket request: %s", string(dump))
|
||||
log.Debug().Msgf("Websocket request: %s", string(dump))
|
||||
|
||||
wsConn, resp, err := cfwebsocket.ClientConnect(req, nil)
|
||||
dialer := &websocket.Dialer{
|
||||
TLSClientConfig: options.TLSClientConfig,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
wsConn, resp, err := clientConnect(req, dialer)
|
||||
defer closeRespBody(resp)
|
||||
|
||||
if err != nil && IsAccessResponse(resp) {
|
||||
wsConn, err = createAccessAuthenticatedStream(options, logger)
|
||||
// Only get Access app info if we know the origin is protected by Access
|
||||
originReq, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appInfo, err := token.GetAppInfo(originReq.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
options.AppInfo = appInfo
|
||||
|
||||
wsConn, err = createAccessAuthenticatedStream(options, log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -95,7 +85,64 @@ func createWebsocketStream(options *StartOptions, logger logger.Service) (*cfweb
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cfwebsocket.Conn{Conn: wsConn}, nil
|
||||
return &cfwebsocket.GorillaConn{Conn: wsConn}, nil
|
||||
}
|
||||
|
||||
var stripWebsocketHeaders = []string{
|
||||
"Upgrade",
|
||||
"Connection",
|
||||
"Sec-Websocket-Key",
|
||||
"Sec-Websocket-Version",
|
||||
"Sec-Websocket-Extensions",
|
||||
}
|
||||
|
||||
// the gorilla websocket library sets its own Upgrade, Connection, Sec-WebSocket-Key,
|
||||
// Sec-WebSocket-Version and Sec-Websocket-Extensions headers.
|
||||
// https://github.com/gorilla/websocket/blob/master/client.go#L189-L194.
|
||||
func websocketHeaders(req *http.Request) http.Header {
|
||||
wsHeaders := make(http.Header)
|
||||
for key, val := range req.Header {
|
||||
wsHeaders[key] = val
|
||||
}
|
||||
// Assume the header keys are in canonical format.
|
||||
for _, header := range stripWebsocketHeaders {
|
||||
wsHeaders.Del(header)
|
||||
}
|
||||
wsHeaders.Set("Host", req.Host) // See TUN-1097
|
||||
return wsHeaders
|
||||
}
|
||||
|
||||
// clientConnect creates a WebSocket client connection for provided request. Caller is responsible for closing
|
||||
// the connection. The response body may not contain the entire response and does
|
||||
// not need to be closed by the application.
|
||||
func clientConnect(req *http.Request, dialler *websocket.Dialer) (*websocket.Conn, *http.Response, error) {
|
||||
req.URL.Scheme = changeRequestScheme(req.URL)
|
||||
wsHeaders := websocketHeaders(req)
|
||||
if dialler == nil {
|
||||
dialler = &websocket.Dialer{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
}
|
||||
conn, response, err := dialler.Dial(req.URL.String(), wsHeaders)
|
||||
if err != nil {
|
||||
return nil, response, err
|
||||
}
|
||||
return conn, response, nil
|
||||
}
|
||||
|
||||
// changeRequestScheme is needed as the gorilla websocket library requires the ws scheme.
|
||||
// (even though it changes it back to http/https, but ¯\_(ツ)_/¯.)
|
||||
func changeRequestScheme(reqURL *url.URL) string {
|
||||
switch reqURL.Scheme {
|
||||
case "https":
|
||||
return "wss"
|
||||
case "http":
|
||||
return "ws"
|
||||
case "":
|
||||
return "ws"
|
||||
default:
|
||||
return reqURL.Scheme
|
||||
}
|
||||
}
|
||||
|
||||
// createAccessAuthenticatedStream will try load a token from storage and make
|
||||
@@ -103,8 +150,8 @@ func createWebsocketStream(options *StartOptions, logger logger.Service) (*cfweb
|
||||
// this probably means the token in storage is invalid (expired/revoked). If that
|
||||
// happens it deletes the token and runs the connection again, so the user can
|
||||
// login again and generate a new one.
|
||||
func createAccessAuthenticatedStream(options *StartOptions, logger logger.Service) (*websocket.Conn, error) {
|
||||
wsConn, resp, err := createAccessWebSocketStream(options, logger)
|
||||
func createAccessAuthenticatedStream(options *StartOptions, log *zerolog.Logger) (*websocket.Conn, error) {
|
||||
wsConn, resp, err := createAccessWebSocketStream(options, log)
|
||||
defer closeRespBody(resp)
|
||||
if err == nil {
|
||||
return wsConn, nil
|
||||
@@ -115,14 +162,10 @@ func createAccessAuthenticatedStream(options *StartOptions, logger logger.Servic
|
||||
}
|
||||
|
||||
// Access Token is invalid for some reason. Go through regen flow
|
||||
originReq, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
if err := token.RemoveTokenIfExists(options.AppInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := token.RemoveTokenIfExists(originReq.URL); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wsConn, resp, err = createAccessWebSocketStream(options, logger)
|
||||
wsConn, resp, err = createAccessWebSocketStream(options, log)
|
||||
defer closeRespBody(resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -132,14 +175,25 @@ func createAccessAuthenticatedStream(options *StartOptions, logger logger.Servic
|
||||
}
|
||||
|
||||
// createAccessWebSocketStream builds an Access request and makes a connection
|
||||
func createAccessWebSocketStream(options *StartOptions, logger logger.Service) (*websocket.Conn, *http.Response, error) {
|
||||
req, err := BuildAccessRequest(options, logger)
|
||||
func createAccessWebSocketStream(options *StartOptions, log *zerolog.Logger) (*websocket.Conn, *http.Response, error) {
|
||||
req, err := BuildAccessRequest(options, log)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
dump, err := httputil.DumpRequest(req, false)
|
||||
logger.Debugf("Access Websocket request: %s", string(dump))
|
||||
log.Debug().Msgf("Access Websocket request: %s", string(dump))
|
||||
|
||||
return cfwebsocket.ClientConnect(req, nil)
|
||||
conn, resp, err := clientConnect(req, nil)
|
||||
|
||||
if resp != nil {
|
||||
r, err := httputil.DumpResponse(resp, true)
|
||||
if r != nil {
|
||||
log.Debug().Msgf("Websocket response: %q", r)
|
||||
} else if err != nil {
|
||||
log.Debug().Msgf("Websocket response error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return conn, resp, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package carrier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
gws "github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/websocket"
|
||||
|
||||
"github.com/cloudflare/cloudflared/hello"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
cfwebsocket "github.com/cloudflare/cloudflared/websocket"
|
||||
)
|
||||
|
||||
func websocketClientTLSConfig(t *testing.T) *tls.Config {
|
||||
certPool := x509.NewCertPool()
|
||||
helloCert, err := tlsconfig.GetHelloCertificateX509()
|
||||
assert.NoError(t, err)
|
||||
certPool.AddCert(helloCert)
|
||||
assert.NotNil(t, certPool)
|
||||
return &tls.Config{RootCAs: certPool}
|
||||
}
|
||||
|
||||
func TestWebsocketHeaders(t *testing.T) {
|
||||
req := testRequest(t, "http://example.com", nil)
|
||||
wsHeaders := websocketHeaders(req)
|
||||
for _, header := range stripWebsocketHeaders {
|
||||
assert.Empty(t, wsHeaders[header])
|
||||
}
|
||||
assert.Equal(t, "curl/7.59.0", wsHeaders.Get("User-Agent"))
|
||||
}
|
||||
|
||||
func TestServe(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
shutdownC := make(chan struct{})
|
||||
errC := make(chan error)
|
||||
listener, err := hello.CreateTLSListener("localhost:1111")
|
||||
assert.NoError(t, err)
|
||||
defer listener.Close()
|
||||
|
||||
go func() {
|
||||
errC <- hello.StartHelloWorldServer(&log, listener, shutdownC)
|
||||
}()
|
||||
|
||||
req := testRequest(t, "https://localhost:1111/ws", nil)
|
||||
|
||||
tlsConfig := websocketClientTLSConfig(t)
|
||||
assert.NotNil(t, tlsConfig)
|
||||
d := gws.Dialer{TLSClientConfig: tlsConfig}
|
||||
conn, resp, err := clientConnect(req, &d)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "websocket", resp.Header.Get("Upgrade"))
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
messageSize := rand.Int()%2048 + 1
|
||||
clientMessage := make([]byte, messageSize)
|
||||
// rand.Read always returns len(clientMessage) and a nil error
|
||||
rand.Read(clientMessage)
|
||||
err = conn.WriteMessage(websocket.BinaryFrame, clientMessage)
|
||||
assert.NoError(t, err)
|
||||
|
||||
messageType, message, err := conn.ReadMessage()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, websocket.BinaryFrame, messageType)
|
||||
assert.Equal(t, clientMessage, message)
|
||||
}
|
||||
|
||||
_ = conn.Close()
|
||||
close(shutdownC)
|
||||
<-errC
|
||||
}
|
||||
|
||||
func TestWebsocketWrapper(t *testing.T) {
|
||||
listener, err := hello.CreateTLSListener("localhost:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
serverErrorChan := make(chan error)
|
||||
helloSvrCtx, cancelHelloSvr := context.WithCancel(context.Background())
|
||||
defer func() { <-serverErrorChan }()
|
||||
defer cancelHelloSvr()
|
||||
go func() {
|
||||
log := zerolog.Nop()
|
||||
serverErrorChan <- hello.StartHelloWorldServer(&log, listener, helloSvrCtx.Done())
|
||||
}()
|
||||
|
||||
tlsConfig := websocketClientTLSConfig(t)
|
||||
d := gws.Dialer{TLSClientConfig: tlsConfig, HandshakeTimeout: time.Minute}
|
||||
testAddr := fmt.Sprintf("https://%s/ws", listener.Addr().String())
|
||||
req := testRequest(t, testAddr, nil)
|
||||
conn, resp, err := clientConnect(req, &d)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "websocket", resp.Header.Get("Upgrade"))
|
||||
|
||||
// Websocket now connected to test server so lets check our wrapper
|
||||
wrapper := cfwebsocket.GorillaConn{Conn: conn}
|
||||
buf := make([]byte, 100)
|
||||
wrapper.Write([]byte("abc"))
|
||||
n, err := wrapper.Read(buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, n, 3)
|
||||
require.Equal(t, "abc", string(buf[:n]))
|
||||
|
||||
// Test partial read, read 1 of 3 bytes in one read and the other 2 in another read
|
||||
wrapper.Write([]byte("abc"))
|
||||
buf = buf[:1]
|
||||
n, err = wrapper.Read(buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, n, 1)
|
||||
require.Equal(t, "a", string(buf[:n]))
|
||||
buf = buf[:cap(buf)]
|
||||
n, err = wrapper.Read(buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, n, 2)
|
||||
require.Equal(t, "bc", string(buf[:n]))
|
||||
}
|
||||
@@ -66,7 +66,7 @@ func DecodeOriginCert(blocks []byte) (*OriginCert, error) {
|
||||
originCert.ServiceKey = ntt.ServiceKey
|
||||
originCert.AccountID = ntt.AccountID
|
||||
} else {
|
||||
// Try the older format, where the zoneID and service key are seperated by
|
||||
// Try the older format, where the zoneID and service key are separated by
|
||||
// a new line character
|
||||
token := string(block.Bytes)
|
||||
s := strings.Split(token, "\n")
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTimeout = 15 * time.Second
|
||||
jsonContentType = "application/json"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
ErrBadRequest = errors.New("incorrect request parameters")
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrAPINoSuccess = errors.New("API call failed")
|
||||
)
|
||||
|
||||
type RESTClient struct {
|
||||
baseEndpoints *baseEndpoints
|
||||
authToken string
|
||||
userAgent string
|
||||
client http.Client
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
type baseEndpoints struct {
|
||||
accountLevel url.URL
|
||||
zoneLevel url.URL
|
||||
accountRoutes url.URL
|
||||
accountVnets url.URL
|
||||
}
|
||||
|
||||
var _ Client = (*RESTClient)(nil)
|
||||
|
||||
func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, log *zerolog.Logger) (*RESTClient, error) {
|
||||
if strings.HasSuffix(baseURL, "/") {
|
||||
baseURL = baseURL[:len(baseURL)-1]
|
||||
}
|
||||
accountLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/tunnels", baseURL, accountTag))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create account level endpoint")
|
||||
}
|
||||
accountRoutesEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/routes", baseURL, accountTag))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create route account-level endpoint")
|
||||
}
|
||||
accountVnetsEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/virtual_networks", baseURL, accountTag))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create virtual network account-level endpoint")
|
||||
}
|
||||
zoneLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/zones/%s/tunnels", baseURL, zoneTag))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create account level endpoint")
|
||||
}
|
||||
httpTransport := http.Transport{
|
||||
TLSHandshakeTimeout: defaultTimeout,
|
||||
ResponseHeaderTimeout: defaultTimeout,
|
||||
}
|
||||
http2.ConfigureTransport(&httpTransport)
|
||||
return &RESTClient{
|
||||
baseEndpoints: &baseEndpoints{
|
||||
accountLevel: *accountLevelEndpoint,
|
||||
zoneLevel: *zoneLevelEndpoint,
|
||||
accountRoutes: *accountRoutesEndpoint,
|
||||
accountVnets: *accountVnetsEndpoint,
|
||||
},
|
||||
authToken: authToken,
|
||||
userAgent: userAgent,
|
||||
client: http.Client{
|
||||
Transport: &httpTransport,
|
||||
Timeout: defaultTimeout,
|
||||
},
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *RESTClient) sendRequest(method string, url url.URL, body interface{}) (*http.Response, error) {
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
if bodyBytes, err := json.Marshal(body); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to serialize json body")
|
||||
} else {
|
||||
bodyReader = bytes.NewBuffer(bodyBytes)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, url.String(), bodyReader)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "can't create %s request", method)
|
||||
}
|
||||
req.Header.Set("User-Agent", r.userAgent)
|
||||
if bodyReader != nil {
|
||||
req.Header.Set("Content-Type", jsonContentType)
|
||||
}
|
||||
req.Header.Add("X-Auth-User-Service-Key", r.authToken)
|
||||
req.Header.Add("Accept", "application/json;version=1")
|
||||
return r.client.Do(req)
|
||||
}
|
||||
|
||||
func parseResponse(reader io.Reader, data interface{}) error {
|
||||
// Schema for Tunnelstore responses in the v1 API.
|
||||
// Roughly, it's a wrapper around a particular result that adds failures/errors/etc
|
||||
var result response
|
||||
// First, parse the wrapper and check the API call succeeded
|
||||
if err := json.NewDecoder(reader).Decode(&result); err != nil {
|
||||
return errors.Wrap(err, "failed to decode response")
|
||||
}
|
||||
if err := result.checkErrors(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !result.Success {
|
||||
return ErrAPINoSuccess
|
||||
}
|
||||
// At this point we know the API call succeeded, so, parse out the inner
|
||||
// result into the datatype provided as a parameter.
|
||||
if err := json.Unmarshal(result.Result, &data); err != nil {
|
||||
return errors.Wrap(err, "the Cloudflare API response was an unexpected type")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type response struct {
|
||||
Success bool `json:"success,omitempty"`
|
||||
Errors []apiErr `json:"errors,omitempty"`
|
||||
Messages []string `json:"messages,omitempty"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
}
|
||||
|
||||
func (r *response) checkErrors() error {
|
||||
if len(r.Errors) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(r.Errors) == 1 {
|
||||
return r.Errors[0]
|
||||
}
|
||||
var messages string
|
||||
for _, e := range r.Errors {
|
||||
messages += fmt.Sprintf("%s; ", e)
|
||||
}
|
||||
return fmt.Errorf("API errors: %s", messages)
|
||||
}
|
||||
|
||||
type apiErr struct {
|
||||
Code json.Number `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func (e apiErr) Error() string {
|
||||
return fmt.Sprintf("code: %v, reason: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
func (r *RESTClient) statusCodeToError(op string, resp *http.Response) error {
|
||||
if resp.Header.Get("Content-Type") == "application/json" {
|
||||
var errorsResp response
|
||||
if json.NewDecoder(resp.Body).Decode(&errorsResp) == nil {
|
||||
if err := errorsResp.checkErrors(); err != nil {
|
||||
return errors.Errorf("Failed to %s: %s", op, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return nil
|
||||
case http.StatusBadRequest:
|
||||
return ErrBadRequest
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return ErrUnauthorized
|
||||
case http.StatusNotFound:
|
||||
return ErrNotFound
|
||||
}
|
||||
return errors.Errorf("API call to %s failed with status %d: %s", op,
|
||||
resp.StatusCode, http.StatusText(resp.StatusCode))
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type TunnelClient interface {
|
||||
CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error)
|
||||
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
|
||||
DeleteTunnel(tunnelID uuid.UUID) error
|
||||
ListTunnels(filter *TunnelFilter) ([]*Tunnel, error)
|
||||
ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error)
|
||||
CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error
|
||||
}
|
||||
|
||||
type HostnameClient interface {
|
||||
RouteTunnel(tunnelID uuid.UUID, route HostnameRoute) (HostnameRouteResult, error)
|
||||
}
|
||||
|
||||
type IPRouteClient interface {
|
||||
ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error)
|
||||
AddRoute(newRoute NewRoute) (Route, error)
|
||||
DeleteRoute(params DeleteRouteParams) error
|
||||
GetByIP(params GetRouteByIpParams) (DetailedRoute, error)
|
||||
}
|
||||
|
||||
type VnetClient interface {
|
||||
CreateVirtualNetwork(newVnet NewVirtualNetwork) (VirtualNetwork, error)
|
||||
ListVirtualNetworks(filter *VnetFilter) ([]*VirtualNetwork, error)
|
||||
DeleteVirtualNetwork(id uuid.UUID) error
|
||||
UpdateVirtualNetwork(id uuid.UUID, updates UpdateVirtualNetwork) error
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
TunnelClient
|
||||
HostnameClient
|
||||
IPRouteClient
|
||||
VnetClient
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Change = string
|
||||
|
||||
const (
|
||||
ChangeNew = "new"
|
||||
ChangeUpdated = "updated"
|
||||
ChangeUnchanged = "unchanged"
|
||||
)
|
||||
|
||||
// HostnameRoute represents a record type that can route to a tunnel
|
||||
type HostnameRoute interface {
|
||||
json.Marshaler
|
||||
RecordType() string
|
||||
UnmarshalResult(body io.Reader) (HostnameRouteResult, error)
|
||||
String() string
|
||||
}
|
||||
|
||||
type HostnameRouteResult interface {
|
||||
// SuccessSummary explains what will route to this tunnel when it's provisioned successfully
|
||||
SuccessSummary() string
|
||||
}
|
||||
|
||||
type DNSRoute struct {
|
||||
userHostname string
|
||||
overwriteExisting bool
|
||||
}
|
||||
|
||||
type DNSRouteResult struct {
|
||||
route *DNSRoute
|
||||
CName Change `json:"cname"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func NewDNSRoute(userHostname string, overwriteExisting bool) HostnameRoute {
|
||||
return &DNSRoute{
|
||||
userHostname: userHostname,
|
||||
overwriteExisting: overwriteExisting,
|
||||
}
|
||||
}
|
||||
|
||||
func (dr *DNSRoute) MarshalJSON() ([]byte, error) {
|
||||
s := struct {
|
||||
Type string `json:"type"`
|
||||
UserHostname string `json:"user_hostname"`
|
||||
OverwriteExisting bool `json:"overwrite_existing"`
|
||||
}{
|
||||
Type: dr.RecordType(),
|
||||
UserHostname: dr.userHostname,
|
||||
OverwriteExisting: dr.overwriteExisting,
|
||||
}
|
||||
return json.Marshal(&s)
|
||||
}
|
||||
|
||||
func (dr *DNSRoute) UnmarshalResult(body io.Reader) (HostnameRouteResult, error) {
|
||||
var result DNSRouteResult
|
||||
err := parseResponse(body, &result)
|
||||
result.route = dr
|
||||
return &result, err
|
||||
}
|
||||
|
||||
func (dr *DNSRoute) RecordType() string {
|
||||
return "dns"
|
||||
}
|
||||
|
||||
func (dr *DNSRoute) String() string {
|
||||
return fmt.Sprintf("%s %s", dr.RecordType(), dr.userHostname)
|
||||
}
|
||||
|
||||
func (res *DNSRouteResult) SuccessSummary() string {
|
||||
var msgFmt string
|
||||
switch res.CName {
|
||||
case ChangeNew:
|
||||
msgFmt = "Added CNAME %s which will route to this tunnel"
|
||||
case ChangeUpdated: // this is not currently returned by tunnelsore
|
||||
msgFmt = "%s updated to route to your tunnel"
|
||||
case ChangeUnchanged:
|
||||
msgFmt = "%s is already configured to route to your tunnel"
|
||||
}
|
||||
return fmt.Sprintf(msgFmt, res.hostname())
|
||||
}
|
||||
|
||||
// hostname yields the resulting name for the DNS route; if that is not available from Cloudflare API, then the
|
||||
// requested name is returned instead (should not be the common path, it is just a fall-back).
|
||||
func (res *DNSRouteResult) hostname() string {
|
||||
if res.Name != "" {
|
||||
return res.Name
|
||||
}
|
||||
return res.route.userHostname
|
||||
}
|
||||
|
||||
type LBRoute struct {
|
||||
lbName string
|
||||
lbPool string
|
||||
}
|
||||
|
||||
type LBRouteResult struct {
|
||||
route *LBRoute
|
||||
LoadBalancer Change `json:"load_balancer"`
|
||||
Pool Change `json:"pool"`
|
||||
}
|
||||
|
||||
func NewLBRoute(lbName, lbPool string) HostnameRoute {
|
||||
return &LBRoute{
|
||||
lbName: lbName,
|
||||
lbPool: lbPool,
|
||||
}
|
||||
}
|
||||
|
||||
func (lr *LBRoute) MarshalJSON() ([]byte, error) {
|
||||
s := struct {
|
||||
Type string `json:"type"`
|
||||
LBName string `json:"lb_name"`
|
||||
LBPool string `json:"lb_pool"`
|
||||
}{
|
||||
Type: lr.RecordType(),
|
||||
LBName: lr.lbName,
|
||||
LBPool: lr.lbPool,
|
||||
}
|
||||
return json.Marshal(&s)
|
||||
}
|
||||
|
||||
func (lr *LBRoute) RecordType() string {
|
||||
return "lb"
|
||||
}
|
||||
|
||||
func (lb *LBRoute) String() string {
|
||||
return fmt.Sprintf("%s %s %s", lb.RecordType(), lb.lbName, lb.lbPool)
|
||||
}
|
||||
|
||||
func (lr *LBRoute) UnmarshalResult(body io.Reader) (HostnameRouteResult, error) {
|
||||
var result LBRouteResult
|
||||
err := parseResponse(body, &result)
|
||||
result.route = lr
|
||||
return &result, err
|
||||
}
|
||||
|
||||
func (res *LBRouteResult) SuccessSummary() string {
|
||||
var msg string
|
||||
switch res.LoadBalancer + "," + res.Pool {
|
||||
case "new,new":
|
||||
msg = "Created load balancer %s and added a new pool %s with this tunnel as an origin"
|
||||
case "new,updated":
|
||||
msg = "Created load balancer %s with an existing pool %s which was updated to use this tunnel as an origin"
|
||||
case "new,unchanged":
|
||||
msg = "Created load balancer %s with an existing pool %s which already has this tunnel as an origin"
|
||||
case "updated,new":
|
||||
msg = "Added new pool %[2]s with this tunnel as an origin to load balancer %[1]s"
|
||||
case "updated,updated":
|
||||
msg = "Updated pool %[2]s to use this tunnel as an origin and added it to load balancer %[1]s"
|
||||
case "updated,unchanged":
|
||||
msg = "Added pool %[2]s, which already has this tunnel as an origin, to load balancer %[1]s"
|
||||
case "unchanged,updated":
|
||||
msg = "Added this tunnel as an origin in pool %[2]s which is already used by load balancer %[1]s"
|
||||
case "unchanged,unchanged":
|
||||
msg = "Load balancer %s already uses pool %s which has this tunnel as an origin"
|
||||
case "unchanged,new":
|
||||
// this state is not possible
|
||||
fallthrough
|
||||
default:
|
||||
msg = "Something went wrong: failed to modify load balancer %s with pool %s; please check traffic manager configuration in the dashboard"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(msg, res.route.lbName, res.route.lbPool)
|
||||
}
|
||||
|
||||
func (r *RESTClient) RouteTunnel(tunnelID uuid.UUID, route HostnameRoute) (HostnameRouteResult, error) {
|
||||
endpoint := r.baseEndpoints.zoneLevel
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/routes", tunnelID))
|
||||
resp, err := r.sendRequest("PUT", endpoint, route)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return route.UnmarshalResult(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("add route", resp)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDNSRouteUnmarshalResult(t *testing.T) {
|
||||
route := &DNSRoute{
|
||||
userHostname: "example.com",
|
||||
}
|
||||
|
||||
result, err := route.UnmarshalResult(strings.NewReader(`{"success": true, "result": {"cname": "new"}}`))
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &DNSRouteResult{
|
||||
route: route,
|
||||
CName: ChangeNew,
|
||||
}, result)
|
||||
|
||||
badJSON := []string{
|
||||
`abc`,
|
||||
`{"success": false, "result": {"cname": "new"}}`,
|
||||
`{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": {"cname": "new"}}`,
|
||||
`{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}, {"code": 1004, "message":"Cannot use tunnel as origin for non-proxied load balancer"}], "result": {"cname": "new"}}`,
|
||||
`{"result": {"cname": "new"}}`,
|
||||
`{"result": {"cname": "new"}}`,
|
||||
}
|
||||
|
||||
for _, j := range badJSON {
|
||||
_, err = route.UnmarshalResult(strings.NewReader(j))
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLBRouteUnmarshalResult(t *testing.T) {
|
||||
route := &LBRoute{
|
||||
lbName: "lb.example.com",
|
||||
lbPool: "pool",
|
||||
}
|
||||
|
||||
result, err := route.UnmarshalResult(strings.NewReader(`{"success": true, "result": {"pool": "unchanged", "load_balancer": "updated"}}`))
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &LBRouteResult{
|
||||
route: route,
|
||||
LoadBalancer: ChangeUpdated,
|
||||
Pool: ChangeUnchanged,
|
||||
}, result)
|
||||
|
||||
badJSON := []string{
|
||||
`abc`,
|
||||
`{"success": false, "result": {"pool": "unchanged", "load_balancer": "updated"}}`,
|
||||
`{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": {"pool": "unchanged", "load_balancer": "updated"}}`,
|
||||
`{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}, {"code": 1004, "message":"Cannot use tunnel as origin for non-proxied load balancer"}], "result": {"pool": "unchanged", "load_balancer": "updated"}}`,
|
||||
`{"result": {"pool": "unchanged", "load_balancer": "updated"}}`,
|
||||
}
|
||||
|
||||
for _, j := range badJSON {
|
||||
_, err = route.UnmarshalResult(strings.NewReader(j))
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLBRouteResultSuccessSummary(t *testing.T) {
|
||||
route := &LBRoute{
|
||||
lbName: "lb.example.com",
|
||||
lbPool: "POOL",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
lb Change
|
||||
pool Change
|
||||
expected string
|
||||
}{
|
||||
{ChangeNew, ChangeNew, "Created load balancer lb.example.com and added a new pool POOL with this tunnel as an origin"},
|
||||
{ChangeNew, ChangeUpdated, "Created load balancer lb.example.com with an existing pool POOL which was updated to use this tunnel as an origin"},
|
||||
{ChangeNew, ChangeUnchanged, "Created load balancer lb.example.com with an existing pool POOL which already has this tunnel as an origin"},
|
||||
{ChangeUpdated, ChangeNew, "Added new pool POOL with this tunnel as an origin to load balancer lb.example.com"},
|
||||
{ChangeUpdated, ChangeUpdated, "Updated pool POOL to use this tunnel as an origin and added it to load balancer lb.example.com"},
|
||||
{ChangeUpdated, ChangeUnchanged, "Added pool POOL, which already has this tunnel as an origin, to load balancer lb.example.com"},
|
||||
{ChangeUnchanged, ChangeNew, "Something went wrong: failed to modify load balancer lb.example.com with pool POOL; please check traffic manager configuration in the dashboard"},
|
||||
{ChangeUnchanged, ChangeUpdated, "Added this tunnel as an origin in pool POOL which is already used by load balancer lb.example.com"},
|
||||
{ChangeUnchanged, ChangeUnchanged, "Load balancer lb.example.com already uses pool POOL which has this tunnel as an origin"},
|
||||
{"", "", "Something went wrong: failed to modify load balancer lb.example.com with pool POOL; please check traffic manager configuration in the dashboard"},
|
||||
{"a", "b", "Something went wrong: failed to modify load balancer lb.example.com with pool POOL; please check traffic manager configuration in the dashboard"},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
res := &LBRouteResult{
|
||||
route: route,
|
||||
LoadBalancer: tt.lb,
|
||||
Pool: tt.pool,
|
||||
}
|
||||
actual := res.SuccessSummary()
|
||||
assert.Equal(t, tt.expected, actual, "case %d", i+1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Route is a mapping from customer's IP space to a tunnel.
|
||||
// Each route allows the customer to route eyeballs in their corporate network
|
||||
// to certain private IP ranges. Each Route represents an IP range in their
|
||||
// network, and says that eyeballs can reach that route using the corresponding
|
||||
// tunnel.
|
||||
type Route struct {
|
||||
Network CIDR `json:"network"`
|
||||
TunnelID uuid.UUID `json:"tunnel_id"`
|
||||
// Optional field. When unset, it means the Route belongs to the default virtual network.
|
||||
VNetID *uuid.UUID `json:"virtual_network_id,omitempty"`
|
||||
Comment string `json:"comment"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeletedAt time.Time `json:"deleted_at"`
|
||||
}
|
||||
|
||||
// CIDR is just a newtype wrapper around net.IPNet. It adds JSON unmarshalling.
|
||||
type CIDR net.IPNet
|
||||
|
||||
func (c CIDR) String() string {
|
||||
n := net.IPNet(c)
|
||||
return n.String()
|
||||
}
|
||||
|
||||
func (c CIDR) MarshalJSON() ([]byte, error) {
|
||||
str := c.String()
|
||||
json, err := json.Marshal(str)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error serializing CIDR into JSON")
|
||||
}
|
||||
return json, nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON parses a JSON string into net.IPNet
|
||||
func (c *CIDR) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return errors.Wrap(err, "error parsing cidr string")
|
||||
}
|
||||
_, network, err := net.ParseCIDR(s)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error parsing invalid network from backend")
|
||||
}
|
||||
if network == nil {
|
||||
return fmt.Errorf("backend returned invalid network %s", s)
|
||||
}
|
||||
*c = CIDR(*network)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewRoute has all the parameters necessary to add a new route to the table.
|
||||
type NewRoute struct {
|
||||
Network net.IPNet
|
||||
TunnelID uuid.UUID
|
||||
Comment string
|
||||
// Optional field. If unset, backend will assume the default vnet for the account.
|
||||
VNetID *uuid.UUID
|
||||
}
|
||||
|
||||
// MarshalJSON handles fields with non-JSON types (e.g. net.IPNet).
|
||||
func (r NewRoute) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(&struct {
|
||||
TunnelID uuid.UUID `json:"tunnel_id"`
|
||||
Comment string `json:"comment"`
|
||||
VNetID *uuid.UUID `json:"virtual_network_id,omitempty"`
|
||||
}{
|
||||
TunnelID: r.TunnelID,
|
||||
Comment: r.Comment,
|
||||
VNetID: r.VNetID,
|
||||
})
|
||||
}
|
||||
|
||||
// DetailedRoute is just a Route with some extra fields, e.g. TunnelName.
|
||||
type DetailedRoute struct {
|
||||
Network CIDR `json:"network"`
|
||||
TunnelID uuid.UUID `json:"tunnel_id"`
|
||||
// Optional field. When unset, it means the DetailedRoute belongs to the default virtual network.
|
||||
VNetID *uuid.UUID `json:"virtual_network_id,omitempty"`
|
||||
Comment string `json:"comment"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeletedAt time.Time `json:"deleted_at"`
|
||||
TunnelName string `json:"tunnel_name"`
|
||||
}
|
||||
|
||||
// IsZero checks if DetailedRoute is the zero value.
|
||||
func (r *DetailedRoute) IsZero() bool {
|
||||
return r.TunnelID == uuid.Nil
|
||||
}
|
||||
|
||||
// TableString outputs a table row summarizing the route, to be used
|
||||
// when showing the user their routing table.
|
||||
func (r DetailedRoute) TableString() string {
|
||||
deletedColumn := "-"
|
||||
if !r.DeletedAt.IsZero() {
|
||||
deletedColumn = r.DeletedAt.Format(time.RFC3339)
|
||||
}
|
||||
vnetColumn := "default"
|
||||
if r.VNetID != nil {
|
||||
vnetColumn = r.VNetID.String()
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"%s\t%s\t%s\t%s\t%s\t%s\t%s\t",
|
||||
r.Network.String(),
|
||||
vnetColumn,
|
||||
r.Comment,
|
||||
r.TunnelID,
|
||||
r.TunnelName,
|
||||
r.CreatedAt.Format(time.RFC3339),
|
||||
deletedColumn,
|
||||
)
|
||||
}
|
||||
|
||||
type DeleteRouteParams struct {
|
||||
Network net.IPNet
|
||||
// Optional field. If unset, backend will assume the default vnet for the account.
|
||||
VNetID *uuid.UUID
|
||||
}
|
||||
|
||||
type GetRouteByIpParams struct {
|
||||
Ip net.IP
|
||||
// Optional field. If unset, backend will assume the default vnet for the account.
|
||||
VNetID *uuid.UUID
|
||||
}
|
||||
|
||||
// ListRoutes calls the Tunnelstore GET endpoint for all routes under an account.
|
||||
func (r *RESTClient) ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error) {
|
||||
endpoint := r.baseEndpoints.accountRoutes
|
||||
endpoint.RawQuery = filter.Encode()
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseListDetailedRoutes(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("list routes", resp)
|
||||
}
|
||||
|
||||
// AddRoute calls the Tunnelstore POST endpoint for a given route.
|
||||
func (r *RESTClient) AddRoute(newRoute NewRoute) (Route, error) {
|
||||
endpoint := r.baseEndpoints.accountRoutes
|
||||
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(newRoute.Network.String()))
|
||||
resp, err := r.sendRequest("POST", endpoint, newRoute)
|
||||
if err != nil {
|
||||
return Route{}, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseRoute(resp.Body)
|
||||
}
|
||||
|
||||
return Route{}, r.statusCodeToError("add route", resp)
|
||||
}
|
||||
|
||||
// DeleteRoute calls the Tunnelstore DELETE endpoint for a given route.
|
||||
func (r *RESTClient) DeleteRoute(params DeleteRouteParams) error {
|
||||
endpoint := r.baseEndpoints.accountRoutes
|
||||
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(params.Network.String()))
|
||||
setVnetParam(&endpoint, params.VNetID)
|
||||
|
||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
_, err := parseRoute(resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
return r.statusCodeToError("delete route", resp)
|
||||
}
|
||||
|
||||
// GetByIP checks which route will proxy a given IP.
|
||||
func (r *RESTClient) GetByIP(params GetRouteByIpParams) (DetailedRoute, error) {
|
||||
endpoint := r.baseEndpoints.accountRoutes
|
||||
endpoint.Path = path.Join(endpoint.Path, "ip", url.PathEscape(params.Ip.String()))
|
||||
setVnetParam(&endpoint, params.VNetID)
|
||||
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return DetailedRoute{}, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseDetailedRoute(resp.Body)
|
||||
}
|
||||
|
||||
return DetailedRoute{}, r.statusCodeToError("get route by IP", resp)
|
||||
}
|
||||
|
||||
func parseListDetailedRoutes(body io.ReadCloser) ([]*DetailedRoute, error) {
|
||||
var routes []*DetailedRoute
|
||||
err := parseResponse(body, &routes)
|
||||
return routes, err
|
||||
}
|
||||
|
||||
func parseRoute(body io.ReadCloser) (Route, error) {
|
||||
var route Route
|
||||
err := parseResponse(body, &route)
|
||||
return route, err
|
||||
}
|
||||
|
||||
func parseDetailedRoute(body io.ReadCloser) (DetailedRoute, error) {
|
||||
var route DetailedRoute
|
||||
err := parseResponse(body, &route)
|
||||
return route, err
|
||||
}
|
||||
|
||||
// setVnetParam overwrites the URL's query parameters with a query param to scope the HostnameRoute action to a certain
|
||||
// virtual network (if one is provided).
|
||||
func setVnetParam(endpoint *url.URL, vnetID *uuid.UUID) {
|
||||
queryParams := url.Values{}
|
||||
if vnetID != nil {
|
||||
queryParams.Set("virtual_network_id", vnetID.String())
|
||||
}
|
||||
endpoint.RawQuery = queryParams.Encode()
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
filterIpRouteDeleted = cli.BoolFlag{
|
||||
Name: "filter-is-deleted",
|
||||
Usage: "If false (default), only show non-deleted routes. If true, only show deleted routes.",
|
||||
}
|
||||
filterIpRouteTunnelID = cli.StringFlag{
|
||||
Name: "filter-tunnel-id",
|
||||
Usage: "Show only routes with the given tunnel ID.",
|
||||
}
|
||||
filterSubsetIpRoute = cli.StringFlag{
|
||||
Name: "filter-network-is-subset-of",
|
||||
Aliases: []string{"nsub"},
|
||||
Usage: "Show only routes whose network is a subset of the given network.",
|
||||
}
|
||||
filterSupersetIpRoute = cli.StringFlag{
|
||||
Name: "filter-network-is-superset-of",
|
||||
Aliases: []string{"nsup"},
|
||||
Usage: "Show only routes whose network is a superset of the given network.",
|
||||
}
|
||||
filterIpRouteComment = cli.StringFlag{
|
||||
Name: "filter-comment-is",
|
||||
Usage: "Show only routes with this comment.",
|
||||
}
|
||||
filterIpRouteByVnet = cli.StringFlag{
|
||||
Name: "filter-vnet-id",
|
||||
Usage: "Show only routes that are attached to the given virtual network ID.",
|
||||
}
|
||||
|
||||
// Flags contains all filter flags.
|
||||
IpRouteFilterFlags = []cli.Flag{
|
||||
&filterIpRouteDeleted,
|
||||
&filterIpRouteTunnelID,
|
||||
&filterSubsetIpRoute,
|
||||
&filterSupersetIpRoute,
|
||||
&filterIpRouteComment,
|
||||
&filterIpRouteByVnet,
|
||||
}
|
||||
)
|
||||
|
||||
// IpRouteFilter which routes get queried.
|
||||
type IpRouteFilter struct {
|
||||
queryParams url.Values
|
||||
}
|
||||
|
||||
// NewIpRouteFilterFromCLI parses CLI flags to discover which filters should get applied.
|
||||
func NewIpRouteFilterFromCLI(c *cli.Context) (*IpRouteFilter, error) {
|
||||
f := &IpRouteFilter{
|
||||
queryParams: url.Values{},
|
||||
}
|
||||
|
||||
// Set deletion filter
|
||||
if flag := filterIpRouteDeleted.Name; c.IsSet(flag) && c.Bool(flag) {
|
||||
f.deleted()
|
||||
} else {
|
||||
f.notDeleted()
|
||||
}
|
||||
|
||||
if subset, err := cidrFromFlag(c, filterSubsetIpRoute); err != nil {
|
||||
return nil, err
|
||||
} else if subset != nil {
|
||||
f.networkIsSupersetOf(*subset)
|
||||
}
|
||||
|
||||
if superset, err := cidrFromFlag(c, filterSupersetIpRoute); err != nil {
|
||||
return nil, err
|
||||
} else if superset != nil {
|
||||
f.networkIsSupersetOf(*superset)
|
||||
}
|
||||
|
||||
if comment := c.String(filterIpRouteComment.Name); comment != "" {
|
||||
f.commentIs(comment)
|
||||
}
|
||||
|
||||
if tunnelID := c.String(filterIpRouteTunnelID.Name); tunnelID != "" {
|
||||
u, err := uuid.Parse(tunnelID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterIpRouteTunnelID.Name)
|
||||
}
|
||||
f.tunnelID(u)
|
||||
}
|
||||
|
||||
if vnetId := c.String(filterIpRouteByVnet.Name); vnetId != "" {
|
||||
u, err := uuid.Parse(vnetId)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterIpRouteByVnet.Name)
|
||||
}
|
||||
f.vnetID(u)
|
||||
}
|
||||
|
||||
if maxFetch := c.Int("max-fetch-size"); maxFetch > 0 {
|
||||
f.MaxFetchSize(uint(maxFetch))
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// Parses a CIDR from the flag. If the flag was unset, returns (nil, nil).
|
||||
func cidrFromFlag(c *cli.Context, flag cli.StringFlag) (*net.IPNet, error) {
|
||||
if !c.IsSet(flag.Name) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
_, subset, err := net.ParseCIDR(c.String(flag.Name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if subset == nil {
|
||||
return nil, fmt.Errorf("Invalid CIDR supplied for %s", flag.Name)
|
||||
}
|
||||
|
||||
return subset, nil
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) commentIs(comment string) {
|
||||
f.queryParams.Set("comment", comment)
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) notDeleted() {
|
||||
f.queryParams.Set("is_deleted", "false")
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) deleted() {
|
||||
f.queryParams.Set("is_deleted", "true")
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) networkIsSubsetOf(superset net.IPNet) {
|
||||
f.queryParams.Set("network_subset", superset.String())
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) networkIsSupersetOf(subset net.IPNet) {
|
||||
f.queryParams.Set("network_superset", subset.String())
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) existedAt(existedAt time.Time) {
|
||||
f.queryParams.Set("existed_at", existedAt.Format(time.RFC3339))
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) tunnelID(id uuid.UUID) {
|
||||
f.queryParams.Set("tunnel_id", id.String())
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) vnetID(id uuid.UUID) {
|
||||
f.queryParams.Set("virtual_network_id", id.String())
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) MaxFetchSize(max uint) {
|
||||
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
||||
}
|
||||
|
||||
func (f IpRouteFilter) Encode() string {
|
||||
return f.queryParams.Encode()
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUnmarshalRoute(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Json string
|
||||
HasVnet bool
|
||||
}{
|
||||
{
|
||||
`{
|
||||
"network":"10.1.2.40/29",
|
||||
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
|
||||
"comment":"test",
|
||||
"created_at":"2020-12-22T02:00:15.587008Z",
|
||||
"deleted_at":null
|
||||
}`,
|
||||
false,
|
||||
},
|
||||
{
|
||||
`{
|
||||
"network":"10.1.2.40/29",
|
||||
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
|
||||
"comment":"test",
|
||||
"created_at":"2020-12-22T02:00:15.587008Z",
|
||||
"deleted_at":null,
|
||||
"virtual_network_id":"38c95083-8191-4110-8339-3f438d44fdb9"
|
||||
}`,
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
data := testCase.Json
|
||||
|
||||
var r Route
|
||||
err := json.Unmarshal([]byte(data), &r)
|
||||
|
||||
// Check everything worked
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uuid.MustParse("fba6ffea-807f-4e7a-a740-4184ee1b82c8"), r.TunnelID)
|
||||
require.Equal(t, "test", r.Comment)
|
||||
_, cidr, err := net.ParseCIDR("10.1.2.40/29")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, CIDR(*cidr), r.Network)
|
||||
require.Equal(t, "test", r.Comment)
|
||||
|
||||
if testCase.HasVnet {
|
||||
require.Equal(t, uuid.MustParse("38c95083-8191-4110-8339-3f438d44fdb9"), *r.VNetID)
|
||||
} else {
|
||||
require.Nil(t, r.VNetID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetailedRouteJsonRoundtrip(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Json string
|
||||
HasVnet bool
|
||||
}{
|
||||
{
|
||||
`{
|
||||
"network":"10.1.2.40/29",
|
||||
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
|
||||
"comment":"test",
|
||||
"created_at":"2020-12-22T02:00:15.587008Z",
|
||||
"deleted_at":"2021-01-14T05:01:42.183002Z",
|
||||
"tunnel_name":"Mr. Tun"
|
||||
}`,
|
||||
false,
|
||||
},
|
||||
{
|
||||
`{
|
||||
"network":"10.1.2.40/29",
|
||||
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
|
||||
"virtual_network_id":"38c95083-8191-4110-8339-3f438d44fdb9",
|
||||
"comment":"test",
|
||||
"created_at":"2020-12-22T02:00:15.587008Z",
|
||||
"deleted_at":"2021-01-14T05:01:42.183002Z",
|
||||
"tunnel_name":"Mr. Tun"
|
||||
}`,
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
data := testCase.Json
|
||||
|
||||
var r DetailedRoute
|
||||
err := json.Unmarshal([]byte(data), &r)
|
||||
|
||||
// Check everything worked
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uuid.MustParse("fba6ffea-807f-4e7a-a740-4184ee1b82c8"), r.TunnelID)
|
||||
require.Equal(t, "test", r.Comment)
|
||||
_, cidr, err := net.ParseCIDR("10.1.2.40/29")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, CIDR(*cidr), r.Network)
|
||||
require.Equal(t, "test", r.Comment)
|
||||
require.Equal(t, "Mr. Tun", r.TunnelName)
|
||||
|
||||
if testCase.HasVnet {
|
||||
require.Equal(t, uuid.MustParse("38c95083-8191-4110-8339-3f438d44fdb9"), *r.VNetID)
|
||||
} else {
|
||||
require.Nil(t, r.VNetID)
|
||||
}
|
||||
|
||||
bytes, err := json.Marshal(r)
|
||||
require.NoError(t, err)
|
||||
obtainedJson := string(bytes)
|
||||
data = strings.Replace(data, "\t", "", -1)
|
||||
data = strings.Replace(data, "\n", "", -1)
|
||||
require.Equal(t, data, obtainedJson)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalNewRoute(t *testing.T) {
|
||||
_, network, err := net.ParseCIDR("1.2.3.4/32")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, network)
|
||||
vnetId := uuid.New()
|
||||
|
||||
newRoutes := []NewRoute{
|
||||
{
|
||||
Network: *network,
|
||||
TunnelID: uuid.New(),
|
||||
Comment: "hi",
|
||||
},
|
||||
{
|
||||
Network: *network,
|
||||
TunnelID: uuid.New(),
|
||||
Comment: "hi",
|
||||
VNetID: &vnetId,
|
||||
},
|
||||
}
|
||||
|
||||
for _, newRoute := range newRoutes {
|
||||
// Test where receiver is struct
|
||||
serialized, err := json.Marshal(newRoute)
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.Contains(string(serialized), "tunnel_id"))
|
||||
|
||||
// Test where receiver is pointer to struct
|
||||
serialized, err = json.Marshal(&newRoute)
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.Contains(string(serialized), "tunnel_id"))
|
||||
|
||||
if newRoute.VNetID == nil {
|
||||
require.False(t, strings.Contains(string(serialized), "virtual_network_id"))
|
||||
} else {
|
||||
require.True(t, strings.Contains(string(serialized), "virtual_network_id"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouteTableString(t *testing.T) {
|
||||
_, network, err := net.ParseCIDR("1.2.3.4/32")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, network)
|
||||
r := DetailedRoute{
|
||||
Network: CIDR(*network),
|
||||
}
|
||||
row := r.TableString()
|
||||
fmt.Println(row)
|
||||
require.True(t, strings.HasPrefix(row, "1.2.3.4/32"))
|
||||
}
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var ErrTunnelNameConflict = errors.New("tunnel with name already exists")
|
||||
|
||||
type Tunnel struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeletedAt time.Time `json:"deleted_at"`
|
||||
Connections []Connection `json:"connections"`
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
ColoName string `json:"colo_name"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
IsPendingReconnect bool `json:"is_pending_reconnect"`
|
||||
OriginIP net.IP `json:"origin_ip"`
|
||||
OpenedAt time.Time `json:"opened_at"`
|
||||
}
|
||||
|
||||
type ActiveClient struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Features []string `json:"features"`
|
||||
Version string `json:"version"`
|
||||
Arch string `json:"arch"`
|
||||
RunAt time.Time `json:"run_at"`
|
||||
Connections []Connection `json:"conns"`
|
||||
}
|
||||
|
||||
type newTunnel struct {
|
||||
Name string `json:"name"`
|
||||
TunnelSecret []byte `json:"tunnel_secret"`
|
||||
}
|
||||
|
||||
type CleanupParams struct {
|
||||
queryParams url.Values
|
||||
}
|
||||
|
||||
func NewCleanupParams() *CleanupParams {
|
||||
return &CleanupParams{
|
||||
queryParams: url.Values{},
|
||||
}
|
||||
}
|
||||
|
||||
func (cp *CleanupParams) ForClient(clientID uuid.UUID) {
|
||||
cp.queryParams.Set("client_id", clientID.String())
|
||||
}
|
||||
|
||||
func (cp CleanupParams) encode() string {
|
||||
return cp.queryParams.Encode()
|
||||
}
|
||||
|
||||
func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error) {
|
||||
if name == "" {
|
||||
return nil, errors.New("tunnel name required")
|
||||
}
|
||||
if _, err := uuid.Parse(name); err == nil {
|
||||
return nil, errors.New("you cannot use UUIDs as tunnel names")
|
||||
}
|
||||
body := &newTunnel{
|
||||
Name: name,
|
||||
TunnelSecret: tunnelSecret,
|
||||
}
|
||||
|
||||
resp, err := r.sendRequest("POST", r.baseEndpoints.accountLevel, body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return unmarshalTunnel(resp.Body)
|
||||
case http.StatusConflict:
|
||||
return nil, ErrTunnelNameConflict
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("create tunnel", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) GetTunnel(tunnelID uuid.UUID) (*Tunnel, error) {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return unmarshalTunnel(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("get tunnel", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID) error {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
|
||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return r.statusCodeToError("delete tunnel", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) ListTunnels(filter *TunnelFilter) ([]*Tunnel, error) {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.RawQuery = filter.encode()
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseListTunnels(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("list tunnels", resp)
|
||||
}
|
||||
|
||||
func parseListTunnels(body io.ReadCloser) ([]*Tunnel, error) {
|
||||
var tunnels []*Tunnel
|
||||
err := parseResponse(body, &tunnels)
|
||||
return tunnels, err
|
||||
}
|
||||
|
||||
func (r *RESTClient) ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error) {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseConnectionsDetails(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("list connection details", resp)
|
||||
}
|
||||
|
||||
func parseConnectionsDetails(reader io.Reader) ([]*ActiveClient, error) {
|
||||
var clients []*ActiveClient
|
||||
err := parseResponse(reader, &clients)
|
||||
return clients, err
|
||||
}
|
||||
|
||||
func (r *RESTClient) CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.RawQuery = params.encode()
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
|
||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return r.statusCodeToError("cleanup connections", resp)
|
||||
}
|
||||
|
||||
func unmarshalTunnel(reader io.Reader) (*Tunnel, error) {
|
||||
var tunnel Tunnel
|
||||
err := parseResponse(reader, &tunnel)
|
||||
return &tunnel, err
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
TimeLayout = time.RFC3339
|
||||
)
|
||||
|
||||
type TunnelFilter struct {
|
||||
queryParams url.Values
|
||||
}
|
||||
|
||||
func NewTunnelFilter() *TunnelFilter {
|
||||
return &TunnelFilter{
|
||||
queryParams: url.Values{},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *TunnelFilter) ByName(name string) {
|
||||
f.queryParams.Set("name", name)
|
||||
}
|
||||
|
||||
func (f *TunnelFilter) ByNamePrefix(namePrefix string) {
|
||||
f.queryParams.Set("name_prefix", namePrefix)
|
||||
}
|
||||
|
||||
func (f *TunnelFilter) ExcludeNameWithPrefix(excludePrefix string) {
|
||||
f.queryParams.Set("exclude_prefix", excludePrefix)
|
||||
}
|
||||
|
||||
func (f *TunnelFilter) NoDeleted() {
|
||||
f.queryParams.Set("is_deleted", "false")
|
||||
}
|
||||
|
||||
func (f *TunnelFilter) ByExistedAt(existedAt time.Time) {
|
||||
f.queryParams.Set("existed_at", existedAt.Format(TimeLayout))
|
||||
}
|
||||
|
||||
func (f *TunnelFilter) ByTunnelID(tunnelID uuid.UUID) {
|
||||
f.queryParams.Set("uuid", tunnelID.String())
|
||||
}
|
||||
|
||||
func (f *TunnelFilter) MaxFetchSize(max uint) {
|
||||
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
||||
}
|
||||
|
||||
func (f TunnelFilter) encode() string {
|
||||
return f.queryParams.Encode()
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var loc, _ = time.LoadLocation("UTC")
|
||||
|
||||
func Test_parseListTunnels(t *testing.T) {
|
||||
type args struct {
|
||||
body string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []*Tunnel
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty list",
|
||||
args: args{body: `{"success": true, "result": []}`},
|
||||
want: []*Tunnel{},
|
||||
},
|
||||
{
|
||||
name: "success is false",
|
||||
args: args{body: `{"success": false, "result": []}`},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "errors are present",
|
||||
args: args{body: `{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": []}`},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid response",
|
||||
args: args{body: `abc`},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
body := ioutil.NopCloser(bytes.NewReader([]byte(tt.args.body)))
|
||||
got, err := parseListTunnels(body)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseListTunnels() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("parseListTunnels() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_unmarshalTunnel(t *testing.T) {
|
||||
type args struct {
|
||||
body string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *Tunnel
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty list",
|
||||
args: args{body: `{"success": true, "result": {"id":"b34cc7ce-925b-46ee-bc23-4cb5c18d8292","created_at":"2021-07-29T13:46:14.090955Z","deleted_at":"2021-07-29T14:07:27.559047Z","name":"qt-bIWWN7D662ogh61pCPfu5s2XgqFY1OyV","account_id":6946212,"account_tag":"5ab4e9dfbd435d24068829fda0077963","conns_active_at":null,"conns_inactive_at":"2021-07-29T13:47:22.548482Z","tun_type":"cfd_tunnel","metadata":{"qtid":"a6fJROgkXutNruBGaJjD"}}}`},
|
||||
want: &Tunnel{
|
||||
ID: uuid.MustParse("b34cc7ce-925b-46ee-bc23-4cb5c18d8292"),
|
||||
Name: "qt-bIWWN7D662ogh61pCPfu5s2XgqFY1OyV",
|
||||
CreatedAt: time.Date(2021, 07, 29, 13, 46, 14, 90955000, loc),
|
||||
DeletedAt: time.Date(2021, 07, 29, 14, 7, 27, 559047000, loc),
|
||||
Connections: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := unmarshalTunnel(strings.NewReader(tt.args.body))
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("unmarshalTunnel() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("unmarshalTunnel() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalTunnelOk(t *testing.T) {
|
||||
|
||||
jsonBody := `{"success": true, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}`
|
||||
expected := Tunnel{
|
||||
ID: uuid.Nil,
|
||||
Name: "test",
|
||||
CreatedAt: time.Time{},
|
||||
Connections: []Connection{},
|
||||
}
|
||||
actual, err := unmarshalTunnel(bytes.NewReader([]byte(jsonBody)))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &expected, actual)
|
||||
}
|
||||
|
||||
func TestUnmarshalTunnelErr(t *testing.T) {
|
||||
|
||||
tests := []string{
|
||||
`abc`,
|
||||
`{"success": true, "result": abc}`,
|
||||
`{"success": false, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}}`,
|
||||
`{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}}`,
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
_, err := unmarshalTunnel(bytes.NewReader([]byte(test)))
|
||||
assert.Error(t, err, fmt.Sprintf("Test #%v failed", i))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalConnections(t *testing.T) {
|
||||
jsonBody := `{"success":true,"messages":[],"errors":[],"result":[{"id":"d4041254-91e3-4deb-bd94-b46e11680b1e","features":["ha-origin"],"version":"2021.2.5","arch":"darwin_amd64","conns":[{"colo_name":"LIS","id":"ac2286e5-c708-4588-a6a0-ba6b51940019","is_pending_reconnect":false,"origin_ip":"148.38.28.2","opened_at":"0001-01-01T00:00:00Z"}],"run_at":"0001-01-01T00:00:00Z"}]}`
|
||||
expected := ActiveClient{
|
||||
ID: uuid.MustParse("d4041254-91e3-4deb-bd94-b46e11680b1e"),
|
||||
Features: []string{"ha-origin"},
|
||||
Version: "2021.2.5",
|
||||
Arch: "darwin_amd64",
|
||||
RunAt: time.Time{},
|
||||
Connections: []Connection{{
|
||||
ID: uuid.MustParse("ac2286e5-c708-4588-a6a0-ba6b51940019"),
|
||||
ColoName: "LIS",
|
||||
IsPendingReconnect: false,
|
||||
OriginIP: net.ParseIP("148.38.28.2"),
|
||||
OpenedAt: time.Time{},
|
||||
}},
|
||||
}
|
||||
actual, err := parseConnectionsDetails(bytes.NewReader([]byte(jsonBody)))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []*ActiveClient{&expected}, actual)
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type NewVirtualNetwork struct {
|
||||
Name string `json:"name"`
|
||||
Comment string `json:"comment"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
}
|
||||
|
||||
type VirtualNetwork struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Comment string `json:"comment"`
|
||||
Name string `json:"name"`
|
||||
IsDefault bool `json:"is_default_network"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeletedAt time.Time `json:"deleted_at"`
|
||||
}
|
||||
|
||||
type UpdateVirtualNetwork struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
IsDefault *bool `json:"is_default_network,omitempty"`
|
||||
}
|
||||
|
||||
func (virtualNetwork VirtualNetwork) TableString() string {
|
||||
deletedColumn := "-"
|
||||
if !virtualNetwork.DeletedAt.IsZero() {
|
||||
deletedColumn = virtualNetwork.DeletedAt.Format(time.RFC3339)
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"%s\t%s\t%s\t%s\t%s\t%s\t",
|
||||
virtualNetwork.ID,
|
||||
virtualNetwork.Name,
|
||||
strconv.FormatBool(virtualNetwork.IsDefault),
|
||||
virtualNetwork.Comment,
|
||||
virtualNetwork.CreatedAt.Format(time.RFC3339),
|
||||
deletedColumn,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *RESTClient) CreateVirtualNetwork(newVnet NewVirtualNetwork) (VirtualNetwork, error) {
|
||||
resp, err := r.sendRequest("POST", r.baseEndpoints.accountVnets, newVnet)
|
||||
if err != nil {
|
||||
return VirtualNetwork{}, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseVnet(resp.Body)
|
||||
}
|
||||
|
||||
return VirtualNetwork{}, r.statusCodeToError("add virtual network", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) ListVirtualNetworks(filter *VnetFilter) ([]*VirtualNetwork, error) {
|
||||
endpoint := r.baseEndpoints.accountVnets
|
||||
endpoint.RawQuery = filter.Encode()
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseListVnets(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("list virtual networks", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) DeleteVirtualNetwork(id uuid.UUID) error {
|
||||
endpoint := r.baseEndpoints.accountVnets
|
||||
endpoint.Path = path.Join(endpoint.Path, url.PathEscape(id.String()))
|
||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
_, err := parseVnet(resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
return r.statusCodeToError("delete virtual network", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) UpdateVirtualNetwork(id uuid.UUID, updates UpdateVirtualNetwork) error {
|
||||
endpoint := r.baseEndpoints.accountVnets
|
||||
endpoint.Path = path.Join(endpoint.Path, url.PathEscape(id.String()))
|
||||
resp, err := r.sendRequest("PATCH", endpoint, updates)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
_, err := parseVnet(resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
return r.statusCodeToError("update virtual network", resp)
|
||||
}
|
||||
|
||||
func parseListVnets(body io.ReadCloser) ([]*VirtualNetwork, error) {
|
||||
var vnets []*VirtualNetwork
|
||||
err := parseResponse(body, &vnets)
|
||||
return vnets, err
|
||||
}
|
||||
|
||||
func parseVnet(body io.ReadCloser) (VirtualNetwork, error) {
|
||||
var vnet VirtualNetwork
|
||||
err := parseResponse(body, &vnet)
|
||||
return vnet, err
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
filterVnetId = cli.StringFlag{
|
||||
Name: "id",
|
||||
Usage: "List virtual networks with the given `ID`",
|
||||
}
|
||||
filterVnetByName = cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "List virtual networks with the given `NAME`",
|
||||
}
|
||||
filterDefaultVnet = cli.BoolFlag{
|
||||
Name: "is-default",
|
||||
Usage: "If true, lists the virtual network that is the default one. If false, lists all non-default virtual networks for the account. If absent, all are included in the results regardless of their default status.",
|
||||
}
|
||||
filterDeletedVnet = cli.BoolFlag{
|
||||
Name: "show-deleted",
|
||||
Usage: "If false (default), only show non-deleted virtual networks. If true, only show deleted virtual networks.",
|
||||
}
|
||||
VnetFilterFlags = []cli.Flag{
|
||||
&filterVnetId,
|
||||
&filterVnetByName,
|
||||
&filterDefaultVnet,
|
||||
&filterDeletedVnet,
|
||||
}
|
||||
)
|
||||
|
||||
// VnetFilter which virtual networks get queried.
|
||||
type VnetFilter struct {
|
||||
queryParams url.Values
|
||||
}
|
||||
|
||||
func NewVnetFilter() *VnetFilter {
|
||||
return &VnetFilter{
|
||||
queryParams: url.Values{},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *VnetFilter) ById(vnetId uuid.UUID) {
|
||||
f.queryParams.Set("id", vnetId.String())
|
||||
}
|
||||
|
||||
func (f *VnetFilter) ByName(name string) {
|
||||
f.queryParams.Set("name", name)
|
||||
}
|
||||
|
||||
func (f *VnetFilter) ByDefaultStatus(isDefault bool) {
|
||||
f.queryParams.Set("is_default", strconv.FormatBool(isDefault))
|
||||
}
|
||||
|
||||
func (f *VnetFilter) WithDeleted(isDeleted bool) {
|
||||
f.queryParams.Set("is_deleted", strconv.FormatBool(isDeleted))
|
||||
}
|
||||
|
||||
func (f *VnetFilter) MaxFetchSize(max uint) {
|
||||
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
||||
}
|
||||
|
||||
func (f VnetFilter) Encode() string {
|
||||
return f.queryParams.Encode()
|
||||
}
|
||||
|
||||
// NewFromCLI parses CLI flags to discover which filters should get applied to list virtual networks.
|
||||
func NewFromCLI(c *cli.Context) (*VnetFilter, error) {
|
||||
f := NewVnetFilter()
|
||||
|
||||
if id := c.String("id"); id != "" {
|
||||
vnetId, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "%s is not a valid virtual network ID", id)
|
||||
}
|
||||
f.ById(vnetId)
|
||||
}
|
||||
|
||||
if name := c.String("name"); name != "" {
|
||||
f.ByName(name)
|
||||
}
|
||||
|
||||
if c.IsSet("is-default") {
|
||||
f.ByDefaultStatus(c.Bool("is-default"))
|
||||
}
|
||||
|
||||
f.WithDeleted(c.Bool("show-deleted"))
|
||||
|
||||
if maxFetch := c.Int("max-fetch-size"); maxFetch > 0 {
|
||||
f.MaxFetchSize(uint(maxFetch))
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestVirtualNetworkJsonRoundtrip(t *testing.T) {
|
||||
data := `{
|
||||
"id":"74fce949-351b-4752-b261-81a56cfd3130",
|
||||
"comment":"New York DC1",
|
||||
"name":"us-east-1",
|
||||
"is_default_network":true,
|
||||
"created_at":"2021-11-26T14:40:02.600673Z",
|
||||
"deleted_at":"2021-12-01T10:23:13.102645Z"
|
||||
}`
|
||||
var v VirtualNetwork
|
||||
err := json.Unmarshal([]byte(data), &v)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uuid.MustParse("74fce949-351b-4752-b261-81a56cfd3130"), v.ID)
|
||||
require.Equal(t, "us-east-1", v.Name)
|
||||
require.Equal(t, "New York DC1", v.Comment)
|
||||
require.Equal(t, true, v.IsDefault)
|
||||
|
||||
bytes, err := json.Marshal(v)
|
||||
require.NoError(t, err)
|
||||
obtainedJson := string(bytes)
|
||||
data = strings.Replace(data, "\t", "", -1)
|
||||
data = strings.Replace(data, "\n", "", -1)
|
||||
require.Equal(t, data, obtainedJson)
|
||||
}
|
||||
|
||||
func TestMarshalNewVnet(t *testing.T) {
|
||||
newVnet := NewVirtualNetwork{
|
||||
Name: "eu-west-1",
|
||||
Comment: "London office",
|
||||
IsDefault: true,
|
||||
}
|
||||
|
||||
serialized, err := json.Marshal(newVnet)
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.Contains(string(serialized), newVnet.Name))
|
||||
}
|
||||
|
||||
func TestMarshalUpdateVnet(t *testing.T) {
|
||||
newName := "bulgaria-1"
|
||||
updates := UpdateVirtualNetwork{
|
||||
Name: &newName,
|
||||
}
|
||||
|
||||
// Test where receiver is struct
|
||||
serialized, err := json.Marshal(updates)
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.Contains(string(serialized), newName))
|
||||
}
|
||||
|
||||
func TestVnetTableString(t *testing.T) {
|
||||
virtualNet := VirtualNetwork{
|
||||
ID: uuid.New(),
|
||||
Name: "us-east-1",
|
||||
Comment: "New York DC1",
|
||||
IsDefault: true,
|
||||
CreatedAt: time.Now(),
|
||||
DeletedAt: time.Time{},
|
||||
}
|
||||
|
||||
row := virtualNet.TableString()
|
||||
require.True(t, strings.HasPrefix(row, virtualNet.ID.String()))
|
||||
require.True(t, strings.Contains(row, virtualNet.Name))
|
||||
require.True(t, strings.Contains(row, virtualNet.Comment))
|
||||
require.True(t, strings.Contains(row, "true"))
|
||||
require.True(t, strings.HasSuffix(row, "-\t"))
|
||||
}
|
||||
+189
-171
@@ -1,6 +1,8 @@
|
||||
pinned_go: &pinned_go go=1.12.9-1
|
||||
pinned_go: &pinned_go go=1.17.5-1
|
||||
pinned_go_fips: &pinned_go_fips go-boring=1.17.5-1
|
||||
|
||||
build_dir: &build_dir /cfsetup_build
|
||||
default-flavor: stretch
|
||||
default-flavor: buster
|
||||
stretch: &stretch
|
||||
build:
|
||||
build_dir: *build_dir
|
||||
@@ -11,9 +13,68 @@ stretch: &stretch
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared
|
||||
build-deb:
|
||||
build-fips:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- make cloudflared
|
||||
# except FIPS (handled in github-fips-release-pkgs) and macos (handled in github-release-macos-amd64)
|
||||
github-release-pkgs:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
- rpm
|
||||
- wget
|
||||
# libmsi and libgcab are libraries the wixl binary depends on.
|
||||
- libmsi-dev
|
||||
- libgcab-dev
|
||||
- python3-dev
|
||||
- libffi-dev
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: &github_release_pkgs_pre_cache
|
||||
- wget https://github.com/sudarshan-reddy/msitools/releases/download/v0.101b/wixl -P /usr/local/bin
|
||||
- chmod a+x /usr/local/bin/wixl
|
||||
- pip3 install pynacl==1.4.0
|
||||
- pip3 install pygithub==1.55
|
||||
post-cache:
|
||||
# build all packages (except macos and FIPS) and move them to /cfsetup/built_artifacts
|
||||
- ./build-packages.sh
|
||||
# release the packages built and moved to /cfsetup/built_artifacts
|
||||
- make github-release-built-pkgs
|
||||
# handle FIPS separately so that we built with gofips compiler
|
||||
github-fips-release-pkgs:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
- rpm
|
||||
- wget
|
||||
# libmsi and libgcab are libraries the wixl binary depends on.
|
||||
- libmsi-dev
|
||||
- libgcab-dev
|
||||
- python3-dev
|
||||
- libffi-dev
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *github_release_pkgs_pre_cache
|
||||
post-cache:
|
||||
# same logic as above, but for FIPS packages only
|
||||
- ./build-packages-fips.sh
|
||||
- make github-release-built-pkgs
|
||||
build-deb:
|
||||
build_dir: *build_dir
|
||||
builddeps: &build_deb_deps
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- fakeroot
|
||||
@@ -22,6 +83,36 @@ stretch: &stretch
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared-deb
|
||||
build-fips-internal-deb:
|
||||
build_dir: *build_dir
|
||||
builddeps: &build_fips_deb_deps
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- export ORIGINAL_NAME=true
|
||||
- make cloudflared-deb
|
||||
build-fips-internal-deb-nightly:
|
||||
build_dir: *build_dir
|
||||
builddeps: *build_fips_deb_deps
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export NIGHTLY=true
|
||||
- export FIPS=true
|
||||
- export ORIGINAL_NAME=true
|
||||
- make cloudflared-deb
|
||||
build-deb-arm64:
|
||||
build_dir: *build_dir
|
||||
builddeps: *build_deb_deps
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=arm64
|
||||
- make cloudflared-deb
|
||||
publish-deb:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
@@ -34,204 +125,131 @@ stretch: &stretch
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make publish-deb
|
||||
release-linux-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make release
|
||||
github-release-linux-amd64:
|
||||
github-release-macos-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- python3-dev
|
||||
- libffi-dev
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: &install_pygithub
|
||||
- pip3 install pynacl==1.4.0
|
||||
- pip3 install pygithub==1.55
|
||||
post-cache:
|
||||
- pip3 install pygithub
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make github-release
|
||||
release-linux-armv6:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- crossbuild-essential-armhf
|
||||
- gcc-arm-linux-gnueabihf
|
||||
post-cache:
|
||||
- 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
|
||||
post-cache:
|
||||
- pip3 install pygithub
|
||||
- export GOOS=linux
|
||||
- export GOARCH=arm
|
||||
- export CC=arm-linux-gnueabihf-gcc
|
||||
- make github-release
|
||||
release-linux-386:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *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
|
||||
post-cache:
|
||||
- pip3 install pygithub
|
||||
- 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 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
|
||||
post-cache:
|
||||
- pip3 install pygithub
|
||||
- export GOOS=windows
|
||||
- export GOARCH=amd64
|
||||
- export CC=x86_64-w64-mingw32-gcc
|
||||
- make github-release
|
||||
release-windows-386:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-mingw-w64
|
||||
post-cache:
|
||||
- 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
|
||||
post-cache:
|
||||
- pip3 install pygithub
|
||||
- export GOOS=windows
|
||||
- export GOARCH=386
|
||||
- export CC=i686-w64-mingw32-gcc-win32
|
||||
- make github-release
|
||||
- make github-mac-upload
|
||||
test:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- gotest-to-teamcity
|
||||
pre-cache: &test_pre_cache
|
||||
- go get golang.org/x/tools/cmd/goimports
|
||||
- go get github.com/sudarshan-reddy/go-sumtype@v0.0.0-20210827105221-82eca7e5abb1
|
||||
post-cache:
|
||||
- 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
|
||||
- ./fmt-check.sh
|
||||
- make test | gotest-to-teamcity
|
||||
test-fips:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
- gotest-to-teamcity
|
||||
pre-cache: *test_pre_cache
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- export PATH="$HOME/go/bin:$PATH"
|
||||
- ./fmt-check.sh
|
||||
- make test | gotest-to-teamcity
|
||||
component-test:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go_fips
|
||||
- python3.7
|
||||
- python3-pip
|
||||
- python3-setuptools
|
||||
# procps installs the ps command which is needed in test_sysv_service because the init script
|
||||
# uses ps pid to determine if the agent is running
|
||||
- procps
|
||||
pre-cache-copy-paths:
|
||||
- component-tests/requirements.txt
|
||||
pre-cache:
|
||||
- sudo pip3 install --upgrade -r component-tests/requirements.txt
|
||||
post-cache:
|
||||
# Creates and routes a Named Tunnel for this build. Also constructs config file from env vars.
|
||||
- python3 component-tests/setup.py --type create
|
||||
- pytest component-tests -o log_cli=true --log-cli-level=INFO
|
||||
# The Named Tunnel is deleted and its route unprovisioned here.
|
||||
- python3 component-tests/setup.py --type cleanup
|
||||
update-homebrew:
|
||||
builddeps:
|
||||
- openssh-client
|
||||
- s3cmd
|
||||
post-cache:
|
||||
- .teamcity/update-homebrew.sh
|
||||
|
||||
jessie: *stretch
|
||||
github-message-release:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- python3-dev
|
||||
- libffi-dev
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *install_pygithub
|
||||
post-cache:
|
||||
- make github-message
|
||||
build-junos:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- python3
|
||||
- genisoimage
|
||||
- jetez
|
||||
pre-cache:
|
||||
- ln -s /usr/bin/genisoimage /usr/bin/mkisofs
|
||||
post-cache:
|
||||
- export GOOS=freebsd
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared-junos
|
||||
publish-junos:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- python3
|
||||
- genisoimage
|
||||
- jetez
|
||||
- s4cmd
|
||||
pre-cache:
|
||||
- ln -s /usr/bin/genisoimage /usr/bin/mkisofs
|
||||
post-cache:
|
||||
- export GOOS=freebsd
|
||||
- export GOARCH=amd64
|
||||
- make publish-cloudflared-junos
|
||||
|
||||
buster: *stretch
|
||||
|
||||
centos-6:
|
||||
build-rpm:
|
||||
build_dir: *build_dir
|
||||
builddeps: &el6_builddeps
|
||||
- https://dl.fedoraproject.org/pub/epel/epel-release-latest-6.noarch.rpm
|
||||
post-cache:
|
||||
- source /usr/local/rvm/scripts/rvm
|
||||
- rvm use 1.9
|
||||
- sudo yum install -y fakeroot
|
||||
- wget https://golang.org/dl/go1.12.9.linux-amd64.tar.gz -P /tmp/
|
||||
- sudo tar -C /usr/local -xzf /tmp/go1.12.9.linux-amd64.tar.gz
|
||||
- export PATH=$PATH:/usr/local/go/bin
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared-rpm
|
||||
publish-rpm:
|
||||
build_dir: *build_dir
|
||||
builddeps: *el6_builddeps
|
||||
post-cache:
|
||||
- source /usr/local/rvm/scripts/rvm
|
||||
- rvm use 1.9
|
||||
- sudo yum install -y fakeroot
|
||||
- wget https://golang.org/dl/go1.12.9.linux-amd64.tar.gz -P /tmp/
|
||||
- sudo tar -C /usr/local -xzf /tmp/go1.12.9.linux-amd64.tar.gz
|
||||
- export PATH=$PATH:/usr/local/go/bin
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make publish-rpm
|
||||
|
||||
|
||||
bullseye: *stretch
|
||||
centos-7:
|
||||
publish-rpm:
|
||||
build_dir: *build_dir
|
||||
builddeps: &el7_builddeps
|
||||
- https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
|
||||
pre-cache:
|
||||
- yum install -y fakeroot
|
||||
- yum upgrade -y binutils-2.27-44.base.el7.x86_64
|
||||
- wget https://go.dev/dl/go1.17.5.linux-amd64.tar.gz -P /tmp/
|
||||
- tar -C /usr/local -xzf /tmp/go1.17.5.linux-amd64.tar.gz
|
||||
post-cache:
|
||||
- sudo yum install -y fakeroot
|
||||
- wget https://golang.org/dl/go1.12.9.linux-amd64.tar.gz -P /tmp/
|
||||
- sudo tar -C /usr/local -xzf /tmp/go1.12.9.linux-amd64.tar.gz
|
||||
- export PATH=$PATH:/usr/local/go/bin
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make publish-rpm
|
||||
build-rpm:
|
||||
build_dir: *build_dir
|
||||
builddeps: *el7_builddeps
|
||||
post-cache:
|
||||
- sudo yum install -y fakeroot
|
||||
- wget https://golang.org/dl/go1.12.9.linux-amd64.tar.gz -P /tmp/
|
||||
- sudo tar -C /usr/local -xzf /tmp/go1.12.9.linux-amd64.tar.gz
|
||||
- export PATH=$PATH:/usr/local/go/bin
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared-rpm
|
||||
|
||||
# cfsetup compose
|
||||
default-stack: test_dbconnect
|
||||
test_dbconnect:
|
||||
compose:
|
||||
up-args:
|
||||
- --renew-anon-volumes
|
||||
- --abort-on-container-exit
|
||||
- --exit-code-from=cloudflared
|
||||
files:
|
||||
- dbconnect_tests/dbconnect.yaml
|
||||
- make publish-rpm
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
# Pass the path to the executable to check for FIPS compliance
|
||||
exe=$1
|
||||
|
||||
if [ "$(go tool nm "${exe}" | grep -c '_Cfunc__goboringcrypto_')" -eq 0 ]; then
|
||||
# Asserts that executable is using FIPS-compliant boringcrypto
|
||||
echo "${exe}: missing goboring symbols" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$(go tool nm "${exe}" | grep -c 'crypto/internal/boring/sig.FIPSOnly')" -eq 0 ]; then
|
||||
# Asserts that executable is using FIPS-only schemes
|
||||
echo "${exe}: missing fipsonly symbols" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "${exe} is FIPS-compliant"
|
||||
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<?if $(var.Platform)="x86"?>
|
||||
<?define Program_Files="ProgramFilesFolder"?>
|
||||
<?else?>
|
||||
<?define Program_Files="ProgramFiles64Folder"?>
|
||||
<?endif?>
|
||||
<?ifndef var.Version?>
|
||||
<?error Undefined Version variable?>
|
||||
<?endif?>
|
||||
<?ifndef var.Path?>
|
||||
<?error Undefined Path variable?>
|
||||
<?endif?>
|
||||
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
|
||||
<Product Id="35e5e858-9372-4449-bf73-1cd6f7267128"
|
||||
UpgradeCode="23f90fdd-9328-47ea-ab52-5380855a4b12"
|
||||
Name="cloudflared"
|
||||
Version="$(var.Version)"
|
||||
Manufacturer="cloudflare"
|
||||
Language="1033">
|
||||
|
||||
<Package InstallerVersion="200" Compressed="yes" Comments="Windows Installer Package" InstallScope="perMachine"/>
|
||||
|
||||
<Media Id="1" Cabinet="product.cab" EmbedCab="yes"/>
|
||||
|
||||
<Upgrade Id="23f90fdd-9328-47ea-ab52-5380855a4b12">
|
||||
<UpgradeVersion Minimum="$(var.Version)" OnlyDetect="yes" Property="NEWERVERSIONDETECTED"/>
|
||||
<UpgradeVersion Minimum="2020.8.0" Maximum="$(var.Version)" IncludeMinimum="yes" IncludeMaximum="no"
|
||||
Property="OLDERVERSIONBEINGUPGRADED"/>
|
||||
</Upgrade>
|
||||
<Condition Message="A newer version of this software is already installed.">NOT NEWERVERSIONDETECTED</Condition>
|
||||
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
<!--This specifies where the cloudflared.exe is moved to in the windows Operation System-->
|
||||
<Directory Id="$(var.Program_Files)">
|
||||
<Directory Id="INSTALLDIR" Name="cloudflared">
|
||||
<Component Id="ApplicationFiles" Guid="35e5e858-9372-4449-bf73-1cd6f7267128">
|
||||
<File Id="ApplicationFile0" Source="$(var.Path)"/>
|
||||
</Component>
|
||||
</Directory>
|
||||
</Directory>
|
||||
<Component Id="ENVS" Guid="6bb74449-d10d-4f4a-933e-6fc9fa006eae">
|
||||
<!--Set the cloudflared bin location to the Path Environment Variable-->
|
||||
<Environment Id="ENV0"
|
||||
Name="PATH"
|
||||
Value="[INSTALLDIR]."
|
||||
Permanent="no"
|
||||
Part="last"
|
||||
Action="create"
|
||||
System="yes" />
|
||||
</Component>
|
||||
</Directory>
|
||||
|
||||
|
||||
<Feature Id='Complete' Level='1'>
|
||||
<ComponentRef Id="ENVS"/>
|
||||
<ComponentRef Id='ApplicationFiles' />
|
||||
</Feature>
|
||||
|
||||
</Product>
|
||||
</Wix>
|
||||
@@ -1,45 +1,45 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"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/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
"github.com/pkg/errors"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
LogFieldHost = "host"
|
||||
cfAccessClientIDHeader = "Cf-Access-Client-Id"
|
||||
cfAccessClientSecretHeader = "Cf-Access-Client-Secret"
|
||||
)
|
||||
|
||||
// StartForwarder starts a client side websocket forward
|
||||
func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, logger logger.Service) error {
|
||||
validURLString, err := validation.ValidateUrl(forwarder.Listener)
|
||||
func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, log *zerolog.Logger) error {
|
||||
validURL, err := validation.ValidateUrl(forwarder.Listener)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error validating origin URL")
|
||||
}
|
||||
|
||||
validURL, err := url.Parse(validURLString)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error parsing origin URL")
|
||||
}
|
||||
|
||||
// get the headers from the config file and add to the request
|
||||
headers := make(http.Header)
|
||||
if forwarder.TokenClientID != "" {
|
||||
headers.Set(h2mux.CFAccessClientIDHeader, forwarder.TokenClientID)
|
||||
headers.Set(cfAccessClientIDHeader, forwarder.TokenClientID)
|
||||
}
|
||||
|
||||
if forwarder.TokenSecret != "" {
|
||||
headers.Set(h2mux.CFAccessClientSecretHeader, forwarder.TokenSecret)
|
||||
headers.Set(cfAccessClientSecretHeader, forwarder.TokenSecret)
|
||||
}
|
||||
|
||||
if forwarder.Destination != "" {
|
||||
headers.Add(h2mux.CFJumpDestinationHeader, forwarder.Destination)
|
||||
}
|
||||
carrier.SetBastionDest(headers, forwarder.Destination)
|
||||
|
||||
options := &carrier.StartOptions{
|
||||
OriginURL: forwarder.URL,
|
||||
@@ -47,9 +47,9 @@ func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, logger
|
||||
}
|
||||
|
||||
// we could add a cmd line variable for this bool if we want the SOCK5 server to be on the client side
|
||||
wsConn := carrier.NewWSConnection(logger, false)
|
||||
wsConn := carrier.NewWSConnection(log)
|
||||
|
||||
logger.Infof("Start Websocket listener on: %s", validURL.Host)
|
||||
log.Info().Str(LogFieldHost, validURL.Host).Msg("Start Websocket listener")
|
||||
return carrier.StartForwarder(wsConn, validURL.Host, shutdown, options)
|
||||
}
|
||||
|
||||
@@ -58,22 +58,7 @@ func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, logger
|
||||
// useful for proxying other protocols (like ssh) over websockets
|
||||
// (which you can put Access in front of)
|
||||
func ssh(c *cli.Context) error {
|
||||
logDirectory, logLevel := config.FindLogSettings()
|
||||
|
||||
flagLogDirectory := c.String(sshLogDirectoryFlag)
|
||||
if flagLogDirectory != "" {
|
||||
logDirectory = flagLogDirectory
|
||||
}
|
||||
|
||||
flagLogLevel := c.String(sshLogLevelFlag)
|
||||
if flagLogLevel != "" {
|
||||
logLevel = flagLogLevel
|
||||
}
|
||||
|
||||
logger, err := logger.New(logger.DefaultFile(logDirectory), logger.LogLevelString(logLevel))
|
||||
if err != nil {
|
||||
return cliutil.PrintLoggerSetupError("error setting up logger", err)
|
||||
}
|
||||
log := logger.CreateSSHLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
// get the hostname from the cmdline and error out if its not provided
|
||||
rawHostName := c.String(sshHostnameFlag)
|
||||
@@ -86,41 +71,52 @@ func ssh(c *cli.Context) error {
|
||||
// get the headers from the cmdline and add them
|
||||
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
|
||||
if c.IsSet(sshTokenIDFlag) {
|
||||
headers.Set(h2mux.CFAccessClientIDHeader, c.String(sshTokenIDFlag))
|
||||
headers.Set(cfAccessClientIDHeader, c.String(sshTokenIDFlag))
|
||||
}
|
||||
if c.IsSet(sshTokenSecretFlag) {
|
||||
headers.Set(h2mux.CFAccessClientSecretHeader, c.String(sshTokenSecretFlag))
|
||||
headers.Set(cfAccessClientSecretHeader, c.String(sshTokenSecretFlag))
|
||||
}
|
||||
|
||||
destination := c.String(sshDestinationFlag)
|
||||
if destination != "" {
|
||||
headers.Add(h2mux.CFJumpDestinationHeader, destination)
|
||||
}
|
||||
carrier.SetBastionDest(headers, c.String(sshDestinationFlag))
|
||||
|
||||
options := &carrier.StartOptions{
|
||||
OriginURL: originURL,
|
||||
Headers: headers,
|
||||
Host: hostname,
|
||||
}
|
||||
|
||||
if connectTo := c.String(sshConnectTo); connectTo != "" {
|
||||
parts := strings.Split(connectTo, ":")
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
options.OriginURL = fmt.Sprintf("https://%s", parts[0])
|
||||
case 2:
|
||||
options.OriginURL = fmt.Sprintf("https://%s:%s", parts[0], parts[1])
|
||||
case 3:
|
||||
options.OriginURL = fmt.Sprintf("https://%s:%s", parts[2], parts[1])
|
||||
options.TLSClientConfig = &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: parts[0],
|
||||
}
|
||||
log.Warn().Msgf("Using insecure SSL connection because SNI overridden to %s", parts[0])
|
||||
default:
|
||||
return fmt.Errorf("invalid connection override: %s", connectTo)
|
||||
}
|
||||
}
|
||||
|
||||
// we could add a cmd line variable for this bool if we want the SOCK5 server to be on the client side
|
||||
wsConn := carrier.NewWSConnection(logger, false)
|
||||
wsConn := carrier.NewWSConnection(log)
|
||||
|
||||
if c.NArg() > 0 || c.IsSet(sshURLFlag) {
|
||||
localForwarder, err := config.ValidateUrl(c, true)
|
||||
forwarder, err := config.ValidateUrl(c, true)
|
||||
if err != nil {
|
||||
logger.Errorf("Error validating origin URL: %s", err)
|
||||
log.Err(err).Msg("Error validating origin URL")
|
||||
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)
|
||||
log.Info().Str(LogFieldHost, forwarder.Host).Msg("Start Websocket listener")
|
||||
err = carrier.StartForwarder(wsConn, forwarder.Host, shutdownC, options)
|
||||
if err != nil {
|
||||
logger.Errorf("Error on Websocket listener: %s", err)
|
||||
log.Err(err).Msg("Error on Websocket listener")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
+121
-89
@@ -2,44 +2,44 @@ package access
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"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/getsentry/raven-go"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
"golang.org/x/net/idna"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
"github.com/cloudflare/cloudflared/carrier"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/sshgen"
|
||||
"github.com/cloudflare/cloudflared/token"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
)
|
||||
|
||||
const (
|
||||
sshHostnameFlag = "hostname"
|
||||
sshDestinationFlag = "destination"
|
||||
sshURLFlag = "url"
|
||||
sshHeaderFlag = "header"
|
||||
sshTokenIDFlag = "service-token-id"
|
||||
sshTokenSecretFlag = "service-token-secret"
|
||||
sshGenCertFlag = "short-lived-cert"
|
||||
sshLogDirectoryFlag = "log-directory"
|
||||
sshLogLevelFlag = "log-level"
|
||||
sshConfigTemplate = `
|
||||
sshHostnameFlag = "hostname"
|
||||
sshDestinationFlag = "destination"
|
||||
sshURLFlag = "url"
|
||||
sshHeaderFlag = "header"
|
||||
sshTokenIDFlag = "service-token-id"
|
||||
sshTokenSecretFlag = "service-token-secret"
|
||||
sshGenCertFlag = "short-lived-cert"
|
||||
sshConnectTo = "connect-to"
|
||||
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'
|
||||
ProxyCommand bash -c '{{.Cloudflared}} access ssh-gen --hostname %h; ssh -tt %r@cfpipe-{{.Hostname}} >&2 <&1'
|
||||
|
||||
Host cfpipe-{{.Hostname}}
|
||||
HostName {{.Hostname}}
|
||||
@@ -55,13 +55,12 @@ Host cfpipe-{{.Hostname}}
|
||||
const sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b@sentry.io/189878"
|
||||
|
||||
var (
|
||||
shutdownC chan struct{}
|
||||
graceShutdownC chan struct{}
|
||||
shutdownC chan struct{}
|
||||
)
|
||||
|
||||
// Init will initialize and store vars from the main program
|
||||
func Init(s, g chan struct{}) {
|
||||
shutdownC, graceShutdownC = s, g
|
||||
func Init(shutdown chan struct{}) {
|
||||
shutdownC = shutdown
|
||||
}
|
||||
|
||||
// Flags return the global flags for Access related commands (hopefully none)
|
||||
@@ -77,30 +76,24 @@ func Commands() []*cli.Command {
|
||||
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
|
||||
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),
|
||||
Action: cliutil.Action(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
|
||||
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),
|
||||
Action: cliutil.Action(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.`,
|
||||
@@ -109,7 +102,7 @@ func Commands() []*cli.Command {
|
||||
},
|
||||
{
|
||||
Name: "token",
|
||||
Action: cliutil.ErrorHandler(generateToken),
|
||||
Action: cliutil.Action(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.`,
|
||||
@@ -121,7 +114,7 @@ func Commands() []*cli.Command {
|
||||
},
|
||||
{
|
||||
Name: "tcp",
|
||||
Action: cliutil.ErrorHandler(ssh),
|
||||
Action: cliutil.Action(ssh),
|
||||
Aliases: []string{"rdp", "ssh", "smb"},
|
||||
Usage: "",
|
||||
ArgsUsage: "",
|
||||
@@ -150,27 +143,34 @@ func Commands() []*cli.Command {
|
||||
Name: sshTokenIDFlag,
|
||||
Aliases: []string{"id"},
|
||||
Usage: "specify an Access service token ID you wish to use.",
|
||||
EnvVars: []string{"TUNNEL_SERVICE_TOKEN_ID"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshTokenSecretFlag,
|
||||
Aliases: []string{"secret"},
|
||||
Usage: "specify an Access service token secret you wish to use.",
|
||||
EnvVars: []string{"TUNNEL_SERVICE_TOKEN_SECRET"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshLogDirectoryFlag,
|
||||
Name: logger.LogSSHDirectoryFlag,
|
||||
Aliases: []string{"logfile"}, //added to match the tunnel side
|
||||
Usage: "Save application log to this directory for reporting issues.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshLogLevelFlag,
|
||||
Name: logger.LogSSHLevelFlag,
|
||||
Aliases: []string{"loglevel"}, //added to match the tunnel side
|
||||
Usage: "Application logging level {fatal, error, info, debug}. ",
|
||||
Usage: "Application logging level {debug, info, warn, error, fatal}. ",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshConnectTo,
|
||||
Hidden: true,
|
||||
Usage: "Connect to alternate location for testing, value is host, host:port, or sni:port:host",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ssh-config",
|
||||
Action: cliutil.ErrorHandler(sshConfig),
|
||||
Action: cliutil.Action(sshConfig),
|
||||
Usage: "",
|
||||
Description: `Prints an example configuration ~/.ssh/config`,
|
||||
Flags: []cli.Flag{
|
||||
@@ -186,7 +186,7 @@ func Commands() []*cli.Command {
|
||||
},
|
||||
{
|
||||
Name: "ssh-gen",
|
||||
Action: cliutil.ErrorHandler(sshGen),
|
||||
Action: cliutil.Action(sshGen),
|
||||
Usage: "",
|
||||
Description: `Generates a short lived certificate for given hostname`,
|
||||
Flags: []cli.Flag{
|
||||
@@ -207,24 +207,27 @@ func login(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
args := c.Args()
|
||||
rawURL := ensureURLScheme(args.First())
|
||||
appURL, err := url.Parse(rawURL)
|
||||
if args.Len() < 1 || err != nil {
|
||||
logger.Errorf("Please provide the url of the Access application\n")
|
||||
return err
|
||||
}
|
||||
if err := verifyTokenAtEdge(appURL, c, logger); err != nil {
|
||||
logger.Errorf("Could not verify token: %s", err)
|
||||
log.Error().Msg("Please provide the url of the Access application")
|
||||
return err
|
||||
}
|
||||
|
||||
cfdToken, err := token.GetTokenIfExists(appURL)
|
||||
appInfo, err := token.GetAppInfo(appURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := verifyTokenAtEdge(appURL, appInfo, c, log); err != nil {
|
||||
log.Err(err).Msg("Could not verify token")
|
||||
return err
|
||||
}
|
||||
|
||||
cfdToken, err := token.GetAppTokenIfExists(appInfo)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Unable to find token for provided application.")
|
||||
return err
|
||||
@@ -237,7 +240,7 @@ func login(c *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureURLScheme prepends a URL with https:// if it doesnt have a scheme. http:// URLs will not be converted.
|
||||
// ensureURLScheme prepends a URL with https:// if it doesn't 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://") {
|
||||
@@ -252,39 +255,61 @@ func curl(c *cli.Context) error {
|
||||
if err := raven.SetDSN(sentryDSN); err != nil {
|
||||
return err
|
||||
}
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
args := c.Args()
|
||||
if args.Len() < 1 {
|
||||
logger.Error("Please provide the access app and command you wish to run.")
|
||||
log.Error().Msg("Please provide the access app and command you wish to run.")
|
||||
return errors.New("incorrect args")
|
||||
}
|
||||
|
||||
cmdArgs, allowRequest := parseAllowRequest(args.Slice())
|
||||
appURL, err := getAppURL(cmdArgs, logger)
|
||||
appURL, err := getAppURL(cmdArgs, log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tok, err := token.GetTokenIfExists(appURL)
|
||||
appInfo, err := token.GetAppInfo(appURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tok, err := token.GetAppTokenIfExists(appInfo)
|
||||
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...)
|
||||
log.Info().Msg("You don't have an Access token set. Please run access token <access application> to fetch one.")
|
||||
return run("curl", cmdArgs...)
|
||||
}
|
||||
tok, err = token.FetchToken(appURL, logger)
|
||||
tok, err = token.FetchToken(appURL, appInfo, log)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to refresh token: %s", err)
|
||||
log.Err(err).Msg("Failed to refresh token")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cmdArgs = append(cmdArgs, "-H")
|
||||
cmdArgs = append(cmdArgs, fmt.Sprintf("%s: %s", h2mux.CFAccessTokenHeader, tok))
|
||||
return shell.Run("curl", cmdArgs...)
|
||||
cmdArgs = append(cmdArgs, fmt.Sprintf("%s: %s", carrier.CFAccessTokenHeader, tok))
|
||||
return run("curl", cmdArgs...)
|
||||
}
|
||||
|
||||
// run kicks 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()
|
||||
}
|
||||
|
||||
// token dumps provided token to stdout
|
||||
@@ -292,14 +317,19 @@ func generateToken(c *cli.Context) error {
|
||||
if err := raven.SetDSN(sentryDSN); err != nil {
|
||||
return err
|
||||
}
|
||||
appURL, err := url.Parse(c.String("app"))
|
||||
appURL, err := url.Parse(ensureURLScheme(c.String("app")))
|
||||
if err != nil || c.NumFlags() < 1 {
|
||||
fmt.Fprintln(os.Stderr, "Please provide a url.")
|
||||
return err
|
||||
}
|
||||
tok, err := token.GetTokenIfExists(appURL)
|
||||
|
||||
appInfo, err := token.GetAppInfo(appURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tok, err := token.GetAppTokenIfExists(appInfo)
|
||||
if err != nil || tok == "" {
|
||||
fmt.Fprintln(os.Stderr, "Unable to find token for provided application. Please run token command to generate token.")
|
||||
fmt.Fprintln(os.Stderr, "Unable to find token for provided application. Please run login command to generate token.")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -331,10 +361,7 @@ func sshConfig(c *cli.Context) error {
|
||||
|
||||
// sshGen generates a short lived certificate for provided hostname
|
||||
func sshGen(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
// get the hostname from the cmdline and error out if its not provided
|
||||
rawHostName := c.String(sshHostnameFlag)
|
||||
@@ -351,7 +378,12 @@ func sshGen(c *cli.Context) error {
|
||||
// this fetchToken function mutates the appURL param. We should refactor that
|
||||
fetchTokenURL := &url.URL{}
|
||||
*fetchTokenURL = *originURL
|
||||
cfdToken, err := token.FetchTokenWithRedirect(fetchTokenURL, logger)
|
||||
|
||||
appInfo, err := token.GetAppInfo(fetchTokenURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfdToken, err := token.FetchTokenWithRedirect(fetchTokenURL, appInfo, log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -363,16 +395,16 @@ func sshGen(c *cli.Context) error {
|
||||
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) {
|
||||
// getAppURL will pull the request URL needed for fetching a user's Access token
|
||||
func getAppURL(cmdArgs []string, log *zerolog.Logger) (*url.URL, error) {
|
||||
if len(cmdArgs) < 1 {
|
||||
logger.Error("Please provide a valid URL as the first argument to curl.")
|
||||
log.Error().Msg("Please provide a valid URL as the first argument to curl.")
|
||||
return nil, errors.New("not a valid url")
|
||||
}
|
||||
|
||||
u, err := processURL(cmdArgs[0])
|
||||
if err != nil {
|
||||
logger.Error("Please provide a valid URL as the first argument to curl.")
|
||||
log.Error().Msg("Please provide a valid URL as the first argument to curl.")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -438,27 +470,27 @@ func isFileThere(candidate string) bool {
|
||||
// verifyTokenAtEdge checks for a token on disk, or generates a new one.
|
||||
// Then makes a request to to the origin with the token to ensure it is valid.
|
||||
// Returns nil if token is valid.
|
||||
func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context, logger logger.Service) error {
|
||||
func verifyTokenAtEdge(appUrl *url.URL, appInfo *token.AppInfo, c *cli.Context, log *zerolog.Logger) error {
|
||||
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
|
||||
if c.IsSet(sshTokenIDFlag) {
|
||||
headers.Add(h2mux.CFAccessClientIDHeader, c.String(sshTokenIDFlag))
|
||||
headers.Add(cfAccessClientIDHeader, c.String(sshTokenIDFlag))
|
||||
}
|
||||
if c.IsSet(sshTokenSecretFlag) {
|
||||
headers.Add(h2mux.CFAccessClientSecretHeader, c.String(sshTokenSecretFlag))
|
||||
headers.Add(cfAccessClientSecretHeader, c.String(sshTokenSecretFlag))
|
||||
}
|
||||
options := &carrier.StartOptions{OriginURL: appUrl.String(), Headers: headers}
|
||||
options := &carrier.StartOptions{AppInfo: appInfo, OriginURL: appUrl.String(), Headers: headers}
|
||||
|
||||
if valid, err := isTokenValid(options, logger); err != nil {
|
||||
if valid, err := isTokenValid(options, log); err != nil {
|
||||
return err
|
||||
} else if valid {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := token.RemoveTokenIfExists(appUrl); err != nil {
|
||||
if err := token.RemoveTokenIfExists(appInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if valid, err := isTokenValid(options, logger); err != nil {
|
||||
if valid, err := isTokenValid(options, log); err != nil {
|
||||
return err
|
||||
} else if !valid {
|
||||
return errors.New("failed to verify token")
|
||||
@@ -468,8 +500,8 @@ func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context, logger logger.Service) e
|
||||
}
|
||||
|
||||
// isTokenValid makes a request to the origin and returns true if the response was not a 302.
|
||||
func isTokenValid(options *carrier.StartOptions, logger logger.Service) (bool, error) {
|
||||
req, err := carrier.BuildAccessRequest(options, logger)
|
||||
func isTokenValid(options *carrier.StartOptions, log *zerolog.Logger) (bool, error) {
|
||||
req, err := carrier.BuildAccessRequest(options, log)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "Could not create access request")
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
)
|
||||
|
||||
// ForwardServiceType is used to identify what kind of overwatch service this is
|
||||
@@ -15,12 +16,12 @@ const ForwardServiceType = "forward"
|
||||
type ForwarderService struct {
|
||||
forwarder config.Forwarder
|
||||
shutdown chan struct{}
|
||||
logger logger.Service
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
// NewForwardService creates a new forwarder service
|
||||
func NewForwardService(f config.Forwarder, logger logger.Service) *ForwarderService {
|
||||
return &ForwarderService{forwarder: f, shutdown: make(chan struct{}, 1), logger: logger}
|
||||
func NewForwardService(f config.Forwarder, log *zerolog.Logger) *ForwarderService {
|
||||
return &ForwarderService{forwarder: f, shutdown: make(chan struct{}, 1), log: log}
|
||||
}
|
||||
|
||||
// Name is used to figure out this service is related to the others (normally the addr it binds to)
|
||||
@@ -46,5 +47,5 @@ func (s *ForwarderService) Shutdown() {
|
||||
|
||||
// Run is the run loop that is started by the overwatch service
|
||||
func (s *ForwarderService) Run() error {
|
||||
return access.StartForwarder(s.forwarder, s.shutdown, s.logger)
|
||||
return access.StartForwarder(s.forwarder, s.shutdown, s.log)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
)
|
||||
|
||||
// ResolverServiceType is used to identify what kind of overwatch service this is
|
||||
const ResolverServiceType = "resolver"
|
||||
const (
|
||||
// ResolverServiceType is used to identify what kind of overwatch service this is
|
||||
ResolverServiceType = "resolver"
|
||||
|
||||
LogFieldResolverAddress = "resolverAddress"
|
||||
LogFieldResolverPort = "resolverPort"
|
||||
LogFieldResolverMaxUpstreamConns = "resolverMaxUpstreamConns"
|
||||
)
|
||||
|
||||
// ResolverService is used to wrap the tunneldns package's DNS over HTTP
|
||||
// into a service model for the overwatch package.
|
||||
@@ -15,14 +22,14 @@ const ResolverServiceType = "resolver"
|
||||
type ResolverService struct {
|
||||
resolver config.DNSResolver
|
||||
shutdown chan struct{}
|
||||
logger logger.Service
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
// NewResolverService creates a new resolver service
|
||||
func NewResolverService(r config.DNSResolver, logger logger.Service) *ResolverService {
|
||||
func NewResolverService(r config.DNSResolver, log *zerolog.Logger) *ResolverService {
|
||||
return &ResolverService{resolver: r,
|
||||
shutdown: make(chan struct{}),
|
||||
logger: logger,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +58,7 @@ func (s *ResolverService) Shutdown() {
|
||||
func (s *ResolverService) Run() error {
|
||||
// create a listener
|
||||
l, err := tunneldns.CreateListener(s.resolver.AddressOrDefault(), s.resolver.PortOrDefault(),
|
||||
s.resolver.UpstreamsOrDefault(), s.resolver.BootstrapsOrDefault(), s.logger)
|
||||
s.resolver.UpstreamsOrDefault(), s.resolver.BootstrapsOrDefault(), s.resolver.MaxUpstreamConnectionsOrDefault(), s.log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -60,14 +67,21 @@ func (s *ResolverService) Run() error {
|
||||
readySignal := make(chan struct{})
|
||||
err = l.Start(readySignal)
|
||||
if err != nil {
|
||||
l.Stop()
|
||||
_ = l.Stop()
|
||||
return err
|
||||
}
|
||||
<-readySignal
|
||||
s.logger.Infof("start resolver on: %s:%d", s.resolver.AddressOrDefault(), s.resolver.PortOrDefault())
|
||||
|
||||
resolverLog := s.log.With().
|
||||
Str(LogFieldResolverAddress, s.resolver.AddressOrDefault()).
|
||||
Uint16(LogFieldResolverPort, s.resolver.PortOrDefault()).
|
||||
Int(LogFieldResolverMaxUpstreamConns, s.resolver.MaxUpstreamConnectionsOrDefault()).
|
||||
Logger()
|
||||
|
||||
resolverLog.Info().Msg("Starting resolver")
|
||||
|
||||
// wait for shutdown signal
|
||||
<-s.shutdown
|
||||
s.logger.Infof("shutdown on: %s:%d", s.resolver.AddressOrDefault(), s.resolver.PortOrDefault())
|
||||
resolverLog.Info().Msg("Shutting down resolver")
|
||||
return l.Stop()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/overwatch"
|
||||
)
|
||||
|
||||
@@ -13,17 +14,17 @@ type AppService struct {
|
||||
serviceManager overwatch.Manager
|
||||
shutdownC chan struct{}
|
||||
configUpdateChan chan config.Root
|
||||
logger logger.Service
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
// NewAppService creates a new AppService with needed supporting services
|
||||
func NewAppService(configManager config.Manager, serviceManager overwatch.Manager, shutdownC chan struct{}, logger logger.Service) *AppService {
|
||||
func NewAppService(configManager config.Manager, serviceManager overwatch.Manager, shutdownC chan struct{}, log *zerolog.Logger) *AppService {
|
||||
return &AppService{
|
||||
configManager: configManager,
|
||||
serviceManager: serviceManager,
|
||||
shutdownC: shutdownC,
|
||||
configUpdateChan: make(chan config.Root),
|
||||
logger: logger,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +38,7 @@ func (s *AppService) Run() error {
|
||||
func (s *AppService) Shutdown() error {
|
||||
s.configManager.Shutdown()
|
||||
s.shutdownC <- struct{}{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -66,14 +68,14 @@ func (s *AppService) handleConfigUpdate(c config.Root) {
|
||||
// handle the client forward listeners
|
||||
activeServices := map[string]struct{}{}
|
||||
for _, f := range c.Forwarders {
|
||||
service := NewForwardService(f, s.logger)
|
||||
service := NewForwardService(f, s.log)
|
||||
s.serviceManager.Add(service)
|
||||
activeServices[service.Name()] = struct{}{}
|
||||
}
|
||||
|
||||
// handle resolver changes
|
||||
if c.Resolver.Enabled {
|
||||
service := NewResolverService(c.Resolver, s.logger)
|
||||
service := NewResolverService(c.Resolver, s.log)
|
||||
s.serviceManager.Add(service)
|
||||
activeServices[service.Name()] = struct{}{}
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
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,49 @@
|
||||
package cliutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type BuildInfo struct {
|
||||
GoOS string `json:"go_os"`
|
||||
GoVersion string `json:"go_version"`
|
||||
GoArch string `json:"go_arch"`
|
||||
BuildType string `json:"build_type"`
|
||||
CloudflaredVersion string `json:"cloudflared_version"`
|
||||
}
|
||||
|
||||
func GetBuildInfo(buildType, version string) *BuildInfo {
|
||||
return &BuildInfo{
|
||||
GoOS: runtime.GOOS,
|
||||
GoVersion: runtime.Version(),
|
||||
GoArch: runtime.GOARCH,
|
||||
BuildType: buildType,
|
||||
CloudflaredVersion: version,
|
||||
}
|
||||
}
|
||||
|
||||
func (bi *BuildInfo) Log(log *zerolog.Logger) {
|
||||
log.Info().Msgf("Version %s", bi.CloudflaredVersion)
|
||||
if bi.BuildType != "" {
|
||||
log.Info().Msgf("Built%s", bi.GetBuildTypeMsg())
|
||||
}
|
||||
log.Info().Msgf("GOOS: %s, GOVersion: %s, GoArch: %s", bi.GoOS, bi.GoVersion, bi.GoArch)
|
||||
}
|
||||
|
||||
func (bi *BuildInfo) OSArch() string {
|
||||
return fmt.Sprintf("%s_%s", bi.GoOS, bi.GoArch)
|
||||
}
|
||||
|
||||
func (bi *BuildInfo) Version() string {
|
||||
return bi.CloudflaredVersion
|
||||
}
|
||||
|
||||
func (bi *BuildInfo) GetBuildTypeMsg() string {
|
||||
if bi.BuildType == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf(" with %s", bi.BuildType)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package cliutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func RemovedCommand(name string) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: name,
|
||||
Action: func(context *cli.Context) error {
|
||||
return cli.Exit(
|
||||
fmt.Sprintf("%s command is no longer supported by cloudflared. Consult Cloudflare Tunnel documentation for possible alternative solutions.", name),
|
||||
-1,
|
||||
)
|
||||
},
|
||||
Description: fmt.Sprintf("%s is deprecated", name),
|
||||
Hidden: true,
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,8 @@ package cliutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
type usageError string
|
||||
@@ -25,31 +22,17 @@ func UsageError(format string, args ...interface{}) error {
|
||||
}
|
||||
|
||||
// Ensures exit with error code if actionFunc returns an error
|
||||
func ErrorHandler(actionFunc cli.ActionFunc) cli.ActionFunc {
|
||||
func WithErrorHandler(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)
|
||||
err = cli.Exit(msg, -1)
|
||||
} else if _, ok := err.(cli.ExitCoder); !ok {
|
||||
err = cli.Exit(err.Error(), 1)
|
||||
}
|
||||
// os.Exits with error code if err is cli.ExitCoder or cli.MultiError
|
||||
cli.HandleExitCoder(err)
|
||||
err = cli.Exit(err.Error(), 1)
|
||||
}
|
||||
logger.SharedWriteManager.Shutdown()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// PrintLoggerSetupError returns an error to stdout to notify when a logger can't start
|
||||
func PrintLoggerSetupError(msg string, err error) error {
|
||||
l, le := logger.New()
|
||||
if le != nil {
|
||||
log.Printf("%s: %s", msg, err)
|
||||
} else {
|
||||
l.Errorf("%s: %s", msg, err)
|
||||
}
|
||||
|
||||
return errors.Wrap(err, msg)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package cliutil
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
func Action(actionFunc cli.ActionFunc) cli.ActionFunc {
|
||||
return WithErrorHandler(actionFunc)
|
||||
}
|
||||
|
||||
func ConfiguredAction(actionFunc cli.ActionFunc) cli.ActionFunc {
|
||||
// Adapt actionFunc to the type signature required by ConfiguredActionWithWarnings
|
||||
f := func(context *cli.Context, _ string) error {
|
||||
return actionFunc(context)
|
||||
}
|
||||
|
||||
return ConfiguredActionWithWarnings(f)
|
||||
}
|
||||
|
||||
// Just like ConfiguredAction, but accepts a second parameter with configuration warnings.
|
||||
func ConfiguredActionWithWarnings(actionFunc func(*cli.Context, string) error) cli.ActionFunc {
|
||||
return WithErrorHandler(func(c *cli.Context) error {
|
||||
warnings, err := setFlagsFromConfigFile(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return actionFunc(c, warnings)
|
||||
})
|
||||
}
|
||||
|
||||
func setFlagsFromConfigFile(c *cli.Context) (configWarnings string, err error) {
|
||||
const errorExitCode = 1
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
inputSource, warnings, err := config.ReadConfigFile(c, log)
|
||||
if err != nil {
|
||||
if err == config.ErrNoConfigFile {
|
||||
return "", nil
|
||||
}
|
||||
return "", cli.Exit(err, errorExitCode)
|
||||
}
|
||||
|
||||
if err := altsrc.ApplyInputSource(c, inputSource); err != nil {
|
||||
return "", cli.Exit(err, errorExitCode)
|
||||
}
|
||||
return warnings, nil
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
"gopkg.in/urfave/cli.v2/altsrc"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultConfigFiles is the file names from which we attempt to read configuration.
|
||||
DefaultConfigFiles = []string{"config.yml", "config.yaml"}
|
||||
|
||||
// DefaultUnixConfigLocation is the primary location to find a config file
|
||||
DefaultUnixConfigLocation = "/usr/local/etc/cloudflared"
|
||||
|
||||
// DefaultUnixLogLocation is the primary location to find log files
|
||||
DefaultUnixLogLocation = "/var/log/cloudflared"
|
||||
|
||||
// Launchd doesn't set root env variables, so there is default
|
||||
// Windows default config dir was ~/cloudflare-warp in documentation; let's keep it compatible
|
||||
DefaultConfigDirs = []string{"~/.cloudflared", "~/.cloudflare-warp", "~/cloudflare-warp", "/etc/cloudflared", DefaultUnixConfigLocation}
|
||||
)
|
||||
|
||||
const DefaultCredentialFile = "cert.pem"
|
||||
|
||||
// DefaultConfigDirectory returns the default directory of the config file
|
||||
func DefaultConfigDirectory() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
path := os.Getenv("CFDPATH")
|
||||
if path == "" {
|
||||
path = filepath.Join(os.Getenv("ProgramFiles(x86)"), "cloudflared")
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) { //doesn't exist, so return an empty failure string
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
return DefaultUnixConfigLocation
|
||||
}
|
||||
|
||||
// DefaultLogDirectory returns the default directory for log files
|
||||
func DefaultLogDirectory() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return DefaultConfigDirectory()
|
||||
}
|
||||
return DefaultUnixLogLocation
|
||||
}
|
||||
|
||||
// DefaultConfigPath returns the default location of a config file
|
||||
func DefaultConfigPath() string {
|
||||
dir := DefaultConfigDirectory()
|
||||
if dir == "" {
|
||||
return DefaultConfigFiles[0]
|
||||
}
|
||||
return filepath.Join(dir, DefaultConfigFiles[0])
|
||||
}
|
||||
|
||||
// FileExists checks to see if a file exist at the provided path.
|
||||
func FileExists(path string) (bool, error) {
|
||||
f, err := os.Open(path)
|
||||
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 DefaultConfigDirs 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 ""
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
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")
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build !windows && !darwin && !linux
|
||||
// +build !windows,!darwin,!linux
|
||||
|
||||
package main
|
||||
@@ -5,9 +6,9 @@ package main
|
||||
import (
|
||||
"os"
|
||||
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
cli "github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
func runApp(app *cli.App, graceShutdownC chan struct{}) {
|
||||
app.Run(os.Args)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package main
|
||||
@@ -7,26 +8,35 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/pkg/errors"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
func runApp(app *cli.App, graceShutdownC chan struct{}) {
|
||||
app.Commands = append(app.Commands, &cli.Command{
|
||||
Name: "service",
|
||||
Usage: "Manages the Argo Tunnel system service",
|
||||
Usage: "Manages the Cloudflare Tunnel system service",
|
||||
Subcommands: []*cli.Command{
|
||||
&cli.Command{
|
||||
{
|
||||
Name: "install",
|
||||
Usage: "Install Argo Tunnel as a system service",
|
||||
Action: installLinuxService,
|
||||
Usage: "Install Cloudflare Tunnel as a system service",
|
||||
Action: cliutil.ConfiguredAction(installLinuxService),
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "legacy",
|
||||
Usage: "Generate service file for non-named tunnels",
|
||||
},
|
||||
},
|
||||
},
|
||||
&cli.Command{
|
||||
{
|
||||
Name: "uninstall",
|
||||
Usage: "Uninstall the Argo Tunnel service",
|
||||
Action: uninstallLinuxService,
|
||||
Usage: "Uninstall the Cloudflare Tunnel service",
|
||||
Action: cliutil.ConfiguredAction(uninstallLinuxService),
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -39,19 +49,20 @@ const (
|
||||
serviceConfigDir = "/etc/cloudflared"
|
||||
serviceConfigFile = "config.yml"
|
||||
serviceCredentialFile = "cert.pem"
|
||||
serviceConfigPath = serviceConfigDir + "/" + serviceConfigFile
|
||||
)
|
||||
|
||||
var systemdTemplates = []ServiceTemplate{
|
||||
{
|
||||
Path: "/etc/systemd/system/cloudflared.service",
|
||||
Content: `[Unit]
|
||||
Description=Argo Tunnel
|
||||
Description=Cloudflare Tunnel
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
TimeoutStartSec=0
|
||||
Type=notify
|
||||
ExecStart={{ .Path }} --config /etc/cloudflared/config.yml --origincert /etc/cloudflared/cert.pem --no-autoupdate
|
||||
ExecStart={{ .Path }} --config /etc/cloudflared/config.yml --no-autoupdate{{ range .ExtraArgs }} {{ . }}{{ end }}
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
|
||||
@@ -62,17 +73,17 @@ WantedBy=multi-user.target
|
||||
{
|
||||
Path: "/etc/systemd/system/cloudflared-update.service",
|
||||
Content: `[Unit]
|
||||
Description=Update Argo Tunnel
|
||||
Description=Update Cloudflare 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'
|
||||
`,
|
||||
},
|
||||
{
|
||||
Path: "/etc/systemd/system/cloudflared-update.timer",
|
||||
Content: `[Unit]
|
||||
Description=Update Argo Tunnel
|
||||
Description=Update Cloudflare Tunnel
|
||||
|
||||
[Timer]
|
||||
OnCalendar=daily
|
||||
@@ -89,7 +100,7 @@ var sysvTemplate = ServiceTemplate{
|
||||
Content: `#!/bin/sh
|
||||
# For RedHat and cousins:
|
||||
# chkconfig: 2345 99 01
|
||||
# description: Argo Tunnel agent
|
||||
# description: Cloudflare Tunnel agent
|
||||
# processname: {{.Path}}
|
||||
### BEGIN INIT INFO
|
||||
# Provides: {{.Path}}
|
||||
@@ -97,11 +108,11 @@ var sysvTemplate = ServiceTemplate{
|
||||
# Required-Stop:
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: Argo Tunnel
|
||||
# Description: Argo Tunnel agent
|
||||
# Short-Description: Cloudflare Tunnel
|
||||
# Description: Cloudflare Tunnel agent
|
||||
### END INIT INFO
|
||||
name=$(basename $(readlink -f $0))
|
||||
cmd="{{.Path}} --config /etc/cloudflared/config.yml --origincert /etc/cloudflared/cert.pem --pidfile /var/run/$name.pid --autoupdate-freq 24h0m0s"
|
||||
cmd="{{.Path}} --config /etc/cloudflared/config.yml --pidfile /var/run/$name.pid --autoupdate-freq 24h0m0s{{ range .ExtraArgs }} {{ . }}{{ end }}"
|
||||
pid_file="/var/run/$name.pid"
|
||||
stdout_log="/var/log/$name.log"
|
||||
stderr_log="/var/log/$name.err"
|
||||
@@ -180,11 +191,7 @@ func isSystemd() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile string, logger logger.Service) error {
|
||||
if err := ensureConfigDirExists(serviceConfigDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile string, log *zerolog.Logger) error {
|
||||
srcCredentialPath := filepath.Join(userConfigDir, userCredentialFile)
|
||||
destCredentialPath := filepath.Join(serviceConfigDir, serviceCredentialFile)
|
||||
if srcCredentialPath != destCredentialPath {
|
||||
@@ -199,71 +206,108 @@ func copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile str
|
||||
if err := copyConfig(srcConfigPath, destConfigPath); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("Copied %s to %s", srcConfigPath, destConfigPath)
|
||||
log.Info().Msgf("Copied %s to %s", srcConfigPath, destConfigPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func installLinuxService(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
etPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error determining executable path: %v", err)
|
||||
}
|
||||
templateArgs := ServiceTemplateArgs{Path: etPath}
|
||||
templateArgs := ServiceTemplateArgs{
|
||||
Path: etPath,
|
||||
}
|
||||
|
||||
userConfigDir := filepath.Dir(c.String("config"))
|
||||
userConfigFile := filepath.Base(c.String("config"))
|
||||
userCredentialFile := config.DefaultCredentialFile
|
||||
if err = copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile, logger); err != nil {
|
||||
logger.Errorf("Failed to copy user configuration: %s. Before running the service, ensure that %s contains two files, %s and %s", err,
|
||||
serviceConfigDir, serviceCredentialFile, serviceConfigFile)
|
||||
if err := ensureConfigDirExists(serviceConfigDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.Bool("legacy") {
|
||||
userConfigDir := filepath.Dir(c.String("config"))
|
||||
userConfigFile := filepath.Base(c.String("config"))
|
||||
userCredentialFile := config.DefaultCredentialFile
|
||||
if err = copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile, log); err != nil {
|
||||
log.Err(err).Msgf("Failed to copy user configuration. Before running the service, ensure that %s contains two files, %s and %s",
|
||||
serviceConfigDir, serviceCredentialFile, serviceConfigFile)
|
||||
return err
|
||||
}
|
||||
templateArgs.ExtraArgs = []string{
|
||||
"--origincert", serviceConfigDir + "/" + serviceCredentialFile,
|
||||
}
|
||||
} else {
|
||||
src, _, err := config.ReadConfigFile(c, log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// can't use context because this command doesn't define "credentials-file" flag
|
||||
configPresent := func(s string) bool {
|
||||
val, err := src.String(s)
|
||||
return err == nil && val != ""
|
||||
}
|
||||
if src.TunnelID == "" || !configPresent(tunnel.CredFileFlag) {
|
||||
return fmt.Errorf(`Configuration file %s must contain entries for the tunnel to run and its associated credentials:
|
||||
tunnel: TUNNEL-UUID
|
||||
credentials-file: CREDENTIALS-FILE
|
||||
`, src.Source())
|
||||
}
|
||||
if src.Source() != serviceConfigPath {
|
||||
if exists, err := config.FileExists(serviceConfigPath); err != nil || exists {
|
||||
return fmt.Errorf("Possible conflicting configuration in %[1]s and %[2]s. Either remove %[2]s or run `cloudflared --config %[2]s service install`", src.Source(), serviceConfigPath)
|
||||
}
|
||||
|
||||
if err := copyFile(src.Source(), serviceConfigPath); err != nil {
|
||||
return fmt.Errorf("failed to copy %s to %s: %w", src.Source(), serviceConfigPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
templateArgs.ExtraArgs = []string{
|
||||
"tunnel", "run",
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case isSystemd():
|
||||
logger.Infof("Using Systemd")
|
||||
return installSystemd(&templateArgs, logger)
|
||||
log.Info().Msgf("Using Systemd")
|
||||
return installSystemd(&templateArgs, log)
|
||||
default:
|
||||
logger.Infof("Using Sysv")
|
||||
return installSysv(&templateArgs, logger)
|
||||
log.Info().Msgf("Using SysV")
|
||||
return installSysv(&templateArgs, log)
|
||||
}
|
||||
}
|
||||
|
||||
func installSystemd(templateArgs *ServiceTemplateArgs, logger logger.Service) error {
|
||||
func installSystemd(templateArgs *ServiceTemplateArgs, log *zerolog.Logger) error {
|
||||
for _, serviceTemplate := range systemdTemplates {
|
||||
err := serviceTemplate.Generate(templateArgs)
|
||||
if err != nil {
|
||||
logger.Errorf("error generating service template: %s", err)
|
||||
log.Err(err).Msg("error generating service template")
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := runCommand("systemctl", "enable", "cloudflared.service"); err != nil {
|
||||
logger.Errorf("systemctl enable cloudflared.service error: %s", err)
|
||||
log.Err(err).Msg("systemctl enable cloudflared.service error")
|
||||
return err
|
||||
}
|
||||
if err := runCommand("systemctl", "start", "cloudflared-update.timer"); err != nil {
|
||||
logger.Errorf("systemctl start cloudflared-update.timer error: %s", err)
|
||||
log.Err(err).Msg("systemctl start cloudflared-update.timer error")
|
||||
return err
|
||||
}
|
||||
logger.Infof("systemctl daemon-reload")
|
||||
log.Info().Msg("systemctl daemon-reload")
|
||||
return runCommand("systemctl", "daemon-reload")
|
||||
}
|
||||
|
||||
func installSysv(templateArgs *ServiceTemplateArgs, logger logger.Service) error {
|
||||
func installSysv(templateArgs *ServiceTemplateArgs, log *zerolog.Logger) error {
|
||||
confPath, err := sysvTemplate.ResolvePath()
|
||||
if err != nil {
|
||||
logger.Errorf("error resolving system path: %s", err)
|
||||
log.Err(err).Msg("error resolving system path")
|
||||
return err
|
||||
}
|
||||
if err := sysvTemplate.Generate(templateArgs); err != nil {
|
||||
logger.Errorf("error generating system template: %s", err)
|
||||
log.Err(err).Msg("error generating system template")
|
||||
return err
|
||||
}
|
||||
for _, i := range [...]string{"2", "3", "4", "5"} {
|
||||
@@ -280,43 +324,40 @@ func installSysv(templateArgs *ServiceTemplateArgs, logger logger.Service) error
|
||||
}
|
||||
|
||||
func uninstallLinuxService(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
switch {
|
||||
case isSystemd():
|
||||
logger.Infof("Using Systemd")
|
||||
return uninstallSystemd(logger)
|
||||
log.Info().Msg("Using Systemd")
|
||||
return uninstallSystemd(log)
|
||||
default:
|
||||
logger.Infof("Using Sysv")
|
||||
return uninstallSysv(logger)
|
||||
log.Info().Msg("Using SysV")
|
||||
return uninstallSysv(log)
|
||||
}
|
||||
}
|
||||
|
||||
func uninstallSystemd(logger logger.Service) error {
|
||||
func uninstallSystemd(log *zerolog.Logger) error {
|
||||
if err := runCommand("systemctl", "disable", "cloudflared.service"); err != nil {
|
||||
logger.Errorf("systemctl disable cloudflared.service error: %s", err)
|
||||
log.Err(err).Msg("systemctl disable cloudflared.service error")
|
||||
return err
|
||||
}
|
||||
if err := runCommand("systemctl", "stop", "cloudflared-update.timer"); err != nil {
|
||||
logger.Errorf("systemctl stop cloudflared-update.timer error: %s", err)
|
||||
log.Err(err).Msg("systemctl stop cloudflared-update.timer error")
|
||||
return err
|
||||
}
|
||||
for _, serviceTemplate := range systemdTemplates {
|
||||
if err := serviceTemplate.Remove(); err != nil {
|
||||
logger.Errorf("error removing service template: %s", err)
|
||||
log.Err(err).Msg("error removing service template")
|
||||
return err
|
||||
}
|
||||
}
|
||||
logger.Infof("Successfully uninstall cloudflared service")
|
||||
log.Info().Msgf("Successfully uninstalled cloudflared service from systemd")
|
||||
return nil
|
||||
}
|
||||
|
||||
func uninstallSysv(logger logger.Service) error {
|
||||
func uninstallSysv(log *zerolog.Logger) error {
|
||||
if err := sysvTemplate.Remove(); err != nil {
|
||||
logger.Errorf("error removing service template: %s", err)
|
||||
log.Err(err).Msg("error removing service template")
|
||||
return err
|
||||
}
|
||||
for _, i := range [...]string{"2", "3", "4", "5"} {
|
||||
@@ -329,6 +370,6 @@ func uninstallSysv(logger logger.Service) error {
|
||||
continue
|
||||
}
|
||||
}
|
||||
logger.Infof("Successfully uninstall cloudflared service")
|
||||
log.Info().Msgf("Successfully uninstalled cloudflared service from sysv")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package main
|
||||
@@ -6,34 +7,35 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
launchdIdentifier = "com.cloudflare.cloudflared"
|
||||
)
|
||||
|
||||
func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
func runApp(app *cli.App, graceShutdownC chan struct{}) {
|
||||
app.Commands = append(app.Commands, &cli.Command{
|
||||
Name: "service",
|
||||
Usage: "Manages the Argo Tunnel launch agent",
|
||||
Usage: "Manages the Cloudflare Tunnel launch agent",
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "install",
|
||||
Usage: "Install Argo Tunnel as an user launch agent",
|
||||
Action: installLaunchd,
|
||||
Usage: "Install Cloudflare Tunnel as an user launch agent",
|
||||
Action: cliutil.ConfiguredAction(installLaunchd),
|
||||
},
|
||||
{
|
||||
Name: "uninstall",
|
||||
Usage: "Uninstall the Argo Tunnel launch agent",
|
||||
Action: uninstallLaunchd,
|
||||
Usage: "Uninstall the Cloudflare Tunnel launch agent",
|
||||
Action: cliutil.ConfiguredAction(uninstallLaunchd),
|
||||
},
|
||||
},
|
||||
})
|
||||
app.Run(os.Args)
|
||||
_ = app.Run(os.Args)
|
||||
}
|
||||
|
||||
func newLaunchdTemplate(installPath, stdoutPath, stderrPath string) *ServiceTemplate {
|
||||
@@ -106,71 +108,61 @@ func stderrPath() (string, error) {
|
||||
}
|
||||
|
||||
func installLaunchd(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
if isRootUser() {
|
||||
logger.Infof("Installing Argo Tunnel client as a system launch daemon. " +
|
||||
"Argo Tunnel client will run at boot")
|
||||
log.Info().Msg("Installing Cloudflare Tunnel client as a system launch daemon. " +
|
||||
"Cloudflare Tunnel client will run at boot")
|
||||
} else {
|
||||
logger.Infof("Installing Argo Tunnel client as an user launch agent. " +
|
||||
"Note that Argo Tunnel client will only run when the user is logged in. " +
|
||||
"If you want to run Argo Tunnel client at boot, install with root permission. " +
|
||||
"For more information, visit https://developers.cloudflare.com/argo-tunnel/reference/service/")
|
||||
log.Info().Msg("Installing Cloudflare Tunnel client as an user launch agent. " +
|
||||
"Note that Cloudflare Tunnel client will only run when the user is logged in. " +
|
||||
"If you want to run Cloudflare Tunnel client at boot, install with root permission. " +
|
||||
"For more information, visit https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/run-as-service")
|
||||
}
|
||||
etPath, err := os.Executable()
|
||||
if err != nil {
|
||||
logger.Errorf("Error determining executable path: %s", err)
|
||||
log.Err(err).Msg("Error determining executable path")
|
||||
return fmt.Errorf("Error determining executable path: %v", err)
|
||||
}
|
||||
installPath, err := installPath()
|
||||
if err != nil {
|
||||
logger.Errorf("Error determining install path: %s", err)
|
||||
log.Err(err).Msg("Error determining install path")
|
||||
return errors.Wrap(err, "Error determining install path")
|
||||
}
|
||||
stdoutPath, err := stdoutPath()
|
||||
if err != nil {
|
||||
logger.Errorf("error determining stdout path: %s", err)
|
||||
log.Err(err).Msg("error determining stdout path")
|
||||
return errors.Wrap(err, "error determining stdout path")
|
||||
}
|
||||
stderrPath, err := stderrPath()
|
||||
if err != nil {
|
||||
logger.Errorf("error determining stderr path: %s", err)
|
||||
log.Err(err).Msg("error determining stderr path")
|
||||
return errors.Wrap(err, "error determining stderr path")
|
||||
}
|
||||
launchdTemplate := newLaunchdTemplate(installPath, stdoutPath, stderrPath)
|
||||
if err != nil {
|
||||
logger.Errorf("error creating launchd template: %s", err)
|
||||
return errors.Wrap(err, "error creating launchd template")
|
||||
}
|
||||
templateArgs := ServiceTemplateArgs{Path: etPath}
|
||||
err = launchdTemplate.Generate(&templateArgs)
|
||||
if err != nil {
|
||||
logger.Errorf("error generating launchd template: %s", err)
|
||||
log.Err(err).Msg("error generating launchd template")
|
||||
return err
|
||||
}
|
||||
plistPath, err := launchdTemplate.ResolvePath()
|
||||
if err != nil {
|
||||
logger.Errorf("error resolving launchd template path: %s", err)
|
||||
log.Err(err).Msg("error resolving launchd template path")
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Infof("Outputs are logged to %s and %s", stderrPath, stdoutPath)
|
||||
log.Info().Msgf("Outputs are logged to %s and %s", stderrPath, stdoutPath)
|
||||
return runCommand("launchctl", "load", plistPath)
|
||||
}
|
||||
|
||||
func uninstallLaunchd(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
if isRootUser() {
|
||||
logger.Infof("Uninstalling Argo Tunnel as a system launch daemon")
|
||||
log.Info().Msg("Uninstalling Cloudflare Tunnel as a system launch daemon")
|
||||
} else {
|
||||
logger.Infof("Uninstalling Argo Tunnel as an user launch agent")
|
||||
log.Info().Msg("Uninstalling Cloudflare Tunnel as an user launch agent")
|
||||
}
|
||||
installPath, err := installPath()
|
||||
if err != nil {
|
||||
@@ -185,20 +177,17 @@ func uninstallLaunchd(c *cli.Context) error {
|
||||
return errors.Wrap(err, "error determining stderr path")
|
||||
}
|
||||
launchdTemplate := newLaunchdTemplate(installPath, stdoutPath, stderrPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error creating launchd template")
|
||||
}
|
||||
plistPath, err := launchdTemplate.ResolvePath()
|
||||
if err != nil {
|
||||
logger.Errorf("error resolving launchd template path: %s", err)
|
||||
log.Err(err).Msg("error resolving launchd template path")
|
||||
return err
|
||||
}
|
||||
err = runCommand("launchctl", "unload", plistPath)
|
||||
if err != nil {
|
||||
logger.Errorf("error unloading: %s", err)
|
||||
log.Err(err).Msg("error unloading")
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Infof("Outputs are logged to %s and %s", stderrPath, stdoutPath)
|
||||
log.Info().Msgf("Outputs are logged to %s and %s", stderrPath, stdoutPath)
|
||||
return launchdTemplate.Remove()
|
||||
}
|
||||
|
||||
+69
-54
@@ -2,24 +2,26 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.uber.org/automaxprocs/maxprocs"
|
||||
|
||||
"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/proxydns"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||
log "github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/metrics"
|
||||
"github.com/cloudflare/cloudflared/overwatch"
|
||||
"github.com/cloudflare/cloudflared/watcher"
|
||||
|
||||
raven "github.com/getsentry/raven-go"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -29,6 +31,7 @@ const (
|
||||
var (
|
||||
Version = "DEV"
|
||||
BuildTime = "unknown"
|
||||
BuildType = ""
|
||||
// Mostly network errors that we don't want reported back to Sentry, this is done by substring match.
|
||||
ignoredErrors = []string{
|
||||
"connection reset by peer",
|
||||
@@ -43,13 +46,13 @@ var (
|
||||
)
|
||||
|
||||
func main() {
|
||||
metrics.RegisterBuildInfo(BuildTime, Version)
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
metrics.RegisterBuildInfo(BuildType, BuildTime, Version)
|
||||
raven.SetRelease(Version)
|
||||
maxprocs.Set()
|
||||
bInfo := cliutil.GetBuildInfo(BuildType, Version)
|
||||
|
||||
// Force shutdown channel used by the app. When closed, app must terminate.
|
||||
// Windows service manager closes this channel when it receives shutdown command.
|
||||
shutdownC := make(chan struct{})
|
||||
// Graceful shutdown channel used by the app. When closed, app must terminate.
|
||||
// Graceful shutdown channel used by the app. When closed, app must terminate gracefully.
|
||||
// Windows service manager closes this channel when it receives stop command.
|
||||
graceShutdownC := make(chan struct{})
|
||||
|
||||
@@ -66,36 +69,58 @@ func main() {
|
||||
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/),
|
||||
the terms of the Cloudflare License (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/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.Version = fmt.Sprintf("%s (built %s%s)", Version, BuildTime, bInfo.GetBuildTypeMsg())
|
||||
app.Description = `cloudflared connects your machine or user identity to Cloudflare's global network.
|
||||
You can use it to authenticate a session to reach an API behind Access, route web traffic to this machine,
|
||||
and configure access control.`
|
||||
and configure access control.
|
||||
|
||||
See https://developers.cloudflare.com/cloudflare-one/connections/connect-apps for more in-depth documentation.`
|
||||
app.Flags = flags()
|
||||
app.Action = action(Version, shutdownC, graceShutdownC)
|
||||
app.Before = tunnel.Before
|
||||
app.Action = action(graceShutdownC)
|
||||
app.Commands = commands(cli.ShowVersion)
|
||||
|
||||
tunnel.Init(Version, shutdownC, graceShutdownC) // we need this to support the tunnel sub command...
|
||||
access.Init(shutdownC, graceShutdownC)
|
||||
runApp(app, shutdownC, graceShutdownC)
|
||||
tunnel.Init(bInfo, graceShutdownC) // we need this to support the tunnel sub command...
|
||||
access.Init(graceShutdownC)
|
||||
updater.Init(Version)
|
||||
runApp(app, graceShutdownC)
|
||||
}
|
||||
|
||||
func commands(version func(c *cli.Context)) []*cli.Command {
|
||||
cmds := []*cli.Command{
|
||||
{
|
||||
Name: "update",
|
||||
Action: updater.Update,
|
||||
Usage: "Update the agent if a new version exists",
|
||||
ArgsUsage: " ",
|
||||
Name: "update",
|
||||
Action: cliutil.ConfiguredAction(updater.Update),
|
||||
Usage: "Update the agent if a new version exists",
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "beta",
|
||||
Usage: "specify if you wish to update to the latest beta version",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "force",
|
||||
Usage: "specify if you wish to force an upgrade to the latest version regardless of the current version",
|
||||
Hidden: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "staging",
|
||||
Usage: "specify if you wish to use the staging url for updating",
|
||||
Hidden: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "version",
|
||||
Usage: "specify a version you wish to upgrade or downgrade to",
|
||||
Hidden: false,
|
||||
},
|
||||
},
|
||||
Description: `Looks for a new version on the official download server.
|
||||
If a new version exists, updates the agent binary and quits.
|
||||
Otherwise, does nothing.
|
||||
|
||||
To determine if an update happened in a script, check for error code 64.`,
|
||||
To determine if an update happened in a script, check for error code 11.`,
|
||||
},
|
||||
{
|
||||
Name: "version",
|
||||
@@ -108,6 +133,7 @@ To determine if an update happened in a script, check for error code 64.`,
|
||||
},
|
||||
}
|
||||
cmds = append(cmds, tunnel.Commands()...)
|
||||
cmds = append(cmds, proxydns.Command(false))
|
||||
cmds = append(cmds, access.Commands()...)
|
||||
return cmds
|
||||
}
|
||||
@@ -121,24 +147,20 @@ func isEmptyInvocation(c *cli.Context) bool {
|
||||
return c.NArg() == 0 && c.NumFlags() == 0
|
||||
}
|
||||
|
||||
func action(version string, shutdownC, graceShutdownC chan struct{}) cli.ActionFunc {
|
||||
return func(c *cli.Context) (err error) {
|
||||
func action(graceShutdownC chan struct{}) cli.ActionFunc {
|
||||
return cliutil.ConfiguredAction(func(c *cli.Context) (err error) {
|
||||
if isEmptyInvocation(c) {
|
||||
return handleServiceMode(shutdownC)
|
||||
return handleServiceMode(c, graceShutdownC)
|
||||
}
|
||||
tags := make(map[string]string)
|
||||
tags["hostname"] = c.String("hostname")
|
||||
raven.SetTagsContext(tags)
|
||||
raven.CapturePanic(func() { err = tunnel.StartServer(c, version, shutdownC, graceShutdownC, nil) }, nil)
|
||||
exitCode := 0
|
||||
raven.CapturePanic(func() { err = tunnel.TunnelCommand(c) }, nil)
|
||||
if err != nil {
|
||||
handleError(err)
|
||||
exitCode = 1
|
||||
captureError(err)
|
||||
}
|
||||
// we already handle error printing, so we pass an empty string so we
|
||||
// don't have to print again.
|
||||
return cli.Exit("", exitCode)
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func userHomeDir() (string, error) {
|
||||
@@ -154,7 +176,7 @@ func userHomeDir() (string, error) {
|
||||
}
|
||||
|
||||
// In order to keep the amount of noise sent to Sentry low, typical network errors can be filtered out here by a substring match.
|
||||
func handleError(err error) {
|
||||
func captureError(err error) {
|
||||
errorMessage := err.Error()
|
||||
for _, ignoredErrorMessage := range ignoredErrors {
|
||||
if strings.Contains(errorMessage, ignoredErrorMessage) {
|
||||
@@ -165,41 +187,34 @@ func handleError(err error) {
|
||||
}
|
||||
|
||||
// cloudflared was started without any flags
|
||||
func handleServiceMode(shutdownC chan struct{}) error {
|
||||
defer log.SharedWriteManager.Shutdown()
|
||||
logDirectory, logLevel := config.FindLogSettings()
|
||||
|
||||
logger, err := log.New(log.DefaultFile(logDirectory), log.LogLevelString(logLevel))
|
||||
if err != nil {
|
||||
return cliutil.PrintLoggerSetupError("error setting up logger", err)
|
||||
}
|
||||
logger.Infof("logging to directory: %s", logDirectory)
|
||||
func handleServiceMode(c *cli.Context, shutdownC chan struct{}) error {
|
||||
log := logger.CreateLoggerFromContext(c, logger.DisableTerminalLog)
|
||||
|
||||
// start the main run loop that reads from the config file
|
||||
f, err := watcher.NewFile()
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot load config file: %s", err)
|
||||
log.Err(err).Msg("Cannot load config file")
|
||||
return err
|
||||
}
|
||||
|
||||
configPath := config.FindOrCreateConfigPath()
|
||||
configManager, err := config.NewFileManager(f, configPath, logger)
|
||||
configManager, err := config.NewFileManager(f, configPath, log)
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot setup config file for monitoring: %s", err)
|
||||
log.Err(err).Msg("Cannot setup config file for monitoring")
|
||||
return err
|
||||
}
|
||||
logger.Infof("monitoring config file at: %s", configPath)
|
||||
log.Info().Msgf("monitoring config file at: %s", configPath)
|
||||
|
||||
serviceCallback := func(t string, name string, err error) {
|
||||
if err != nil {
|
||||
logger.Errorf("%s service: %s encountered an error: %s", t, name, err)
|
||||
log.Err(err).Msgf("%s service: %s encountered an error", t, name)
|
||||
}
|
||||
}
|
||||
serviceManager := overwatch.NewAppManager(serviceCallback)
|
||||
|
||||
appService := NewAppService(configManager, serviceManager, shutdownC, logger)
|
||||
appService := NewAppService(configManager, serviceManager, shutdownC, log)
|
||||
if err := appService.Run(); err != nil {
|
||||
logger.Errorf("Failed to start app service: %s", err)
|
||||
log.Err(err).Msg("Failed to start app service")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
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.DefaultConfigDirs[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
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package proxydns
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/metrics"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
)
|
||||
|
||||
func Command(hidden bool) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "proxy-dns",
|
||||
Action: cliutil.ConfiguredAction(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"},
|
||||
},
|
||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||
&cli.IntFlag{
|
||||
Name: "port",
|
||||
Usage: "Listen on given port for the DNS over HTTPS proxy server.",
|
||||
Value: 53,
|
||||
EnvVars: []string{"TUNNEL_DNS_PORT"},
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "upstream",
|
||||
Usage: "Upstream endpoint URL, you can specify multiple endpoints for redundancy.",
|
||||
Value: cli.NewStringSlice("https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"),
|
||||
EnvVars: []string{"TUNNEL_DNS_UPSTREAM"},
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "bootstrap",
|
||||
Usage: "bootstrap endpoint URL, you can specify multiple endpoints for redundancy.",
|
||||
Value: cli.NewStringSlice("https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"),
|
||||
EnvVars: []string{"TUNNEL_DNS_BOOTSTRAP"},
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "max-upstream-conns",
|
||||
Usage: "Maximum concurrent connections to upstream. Setting to 0 means unlimited.",
|
||||
Value: tunneldns.MaxUpstreamConnsDefault,
|
||||
EnvVars: []string{"TUNNEL_DNS_MAX_UPSTREAM_CONNS"},
|
||||
},
|
||||
},
|
||||
ArgsUsage: " ", // can't be the empty string or we get the default output
|
||||
Hidden: hidden,
|
||||
}
|
||||
}
|
||||
|
||||
// Run implements a foreground runner
|
||||
func Run(c *cli.Context) error {
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
metricsListener, err := net.Listen("tcp", c.String("metrics"))
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to open the metrics listener")
|
||||
}
|
||||
|
||||
go metrics.ServeMetrics(metricsListener, nil, nil, "", log)
|
||||
|
||||
listener, err := tunneldns.CreateListener(
|
||||
c.String("address"),
|
||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||
uint16(c.Int("port")),
|
||||
c.StringSlice("upstream"),
|
||||
c.StringSlice("bootstrap"),
|
||||
c.Int("max-upstream-conns"),
|
||||
log,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to create the listeners")
|
||||
return err
|
||||
}
|
||||
|
||||
// Try to start the server
|
||||
readySignal := make(chan struct{})
|
||||
err = listener.Start(readySignal)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to start the listeners")
|
||||
return listener.Stop()
|
||||
}
|
||||
<-readySignal
|
||||
|
||||
// Wait for signal
|
||||
signals := make(chan os.Signal, 10)
|
||||
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
|
||||
defer signal.Stop(signals)
|
||||
<-signals
|
||||
|
||||
// Shut down server
|
||||
err = listener.Stop()
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to stop")
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -10,8 +10,9 @@ import (
|
||||
"os/exec"
|
||||
"text/template"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
)
|
||||
|
||||
type ServiceTemplate struct {
|
||||
@@ -21,7 +22,8 @@ type ServiceTemplate struct {
|
||||
}
|
||||
|
||||
type ServiceTemplateArgs struct {
|
||||
Path string
|
||||
Path string
|
||||
ExtraArgs []string
|
||||
}
|
||||
|
||||
func (st *ServiceTemplate) ResolvePath() (string, error) {
|
||||
@@ -80,7 +82,7 @@ func runCommand(command string, args ...string) error {
|
||||
return fmt.Errorf("error starting %s: %v", command, err)
|
||||
}
|
||||
|
||||
ioutil.ReadAll(stderr)
|
||||
_, _ = ioutil.ReadAll(stderr)
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s returned with error: %v", command, err)
|
||||
@@ -139,6 +141,33 @@ func copyCredential(srcCredentialPath, destCredentialPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFile(src, dest string) error {
|
||||
srcFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
destFile, err := os.Create(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ok := false
|
||||
defer func() {
|
||||
destFile.Close()
|
||||
if !ok {
|
||||
_ = os.Remove(dest)
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := io.Copy(destFile, srcFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ok = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyConfig(srcConfigPath, destConfigPath string) error {
|
||||
// Copy or create config
|
||||
destFile, exists, err := openFile(destConfigPath, true)
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
//+build darwin
|
||||
|
||||
package shell
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func getBrowserCmd(url string) *exec.Cmd {
|
||||
return exec.Command("open", url)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
//+build !windows,!darwin,!linux,!netbsd,!freebsd,!openbsd
|
||||
|
||||
package shell
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func getBrowserCmd(url string) *exec.Cmd {
|
||||
return nil
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
//+build linux freebsd openbsd netbsd
|
||||
|
||||
package shell
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func getBrowserCmd(url string) *exec.Cmd {
|
||||
return exec.Command("xdg-open", url)
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
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 a redirect URL to the access cli request if opening the browser
|
||||
func FetchTokenWithRedirect(appURL *url.URL, logger logger.Service) (string, error) {
|
||||
return getToken(appURL, true, logger)
|
||||
}
|
||||
|
||||
// FetchToken will either load a stored token or generate a new one
|
||||
// it doesn't append a redirect URL to the access cli request if opening the browser
|
||||
func FetchToken(appURL *url.URL, logger logger.Service) (string, error) {
|
||||
return getToken(appURL, false, logger)
|
||||
}
|
||||
|
||||
// getToken will either load a stored token or generate a new one
|
||||
func getToken(appURL *url.URL, shouldRedirect bool, logger logger.Service) (string, error) {
|
||||
if token, err := GetTokenIfExists(appURL); token != "" && err == nil {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
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, shouldRedirect, 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
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
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, shouldRedirect bool, logger logger.Service) ([]byte, error) {
|
||||
encrypterClient, err := encrypter.New("cloudflared_priv.pem", "cloudflared_pub.pem")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
requestURL, err := buildRequestURL(transferURL, key, value+encrypterClient.PublicKey(), shouldEncrypt, shouldRedirect)
|
||||
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, shouldRedirect bool) (string, error) {
|
||||
q := baseURL.Query()
|
||||
q.Set(key, value)
|
||||
baseURL.RawQuery = q.Encode()
|
||||
if !cli {
|
||||
return baseURL.String(), nil
|
||||
}
|
||||
|
||||
if shouldRedirect {
|
||||
q.Set("redirect_url", baseURL.String()) // we add the token as a query param on both the redirect_url and the main url
|
||||
}
|
||||
baseURL.RawQuery = q.Encode() // and this actual baseURL.
|
||||
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
|
||||
}
|
||||
+579
-652
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDedup(t *testing.T) {
|
||||
expected := []string{"a", "b"}
|
||||
actual := dedup([]string{"a", "b", "a"})
|
||||
require.ElementsMatch(t, expected, actual)
|
||||
}
|
||||
@@ -1,43 +1,49 @@
|
||||
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/google/uuid"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
const LogFieldOriginCertPath = "originCertPath"
|
||||
|
||||
var (
|
||||
developerPortal = "https://developers.cloudflare.com/argo-tunnel"
|
||||
quickStartUrl = developerPortal + "/quickstart/quickstart/"
|
||||
serviceUrl = developerPortal + "/reference/service/"
|
||||
argumentsUrl = developerPortal + "/reference/arguments/"
|
||||
|
||||
LogFieldHostname = "hostname"
|
||||
)
|
||||
|
||||
// returns the first path that contains a cert.pem file. If none of the DefaultConfigDirs
|
||||
// returns the first path that contains a cert.pem file. If none of the DefaultConfigSearchDirectories
|
||||
// contains a cert.pem file, return empty string
|
||||
func findDefaultOriginCertPath() string {
|
||||
for _, defaultConfigDir := range config.DefaultConfigDirs {
|
||||
for _, defaultConfigDir := range config.DefaultConfigSearchDirectories() {
|
||||
originCertPath, _ := homedir.Expand(filepath.Join(defaultConfigDir, config.DefaultCredentialFile))
|
||||
if ok, _ := config.FileExists(originCertPath); ok {
|
||||
return originCertPath
|
||||
@@ -46,30 +52,23 @@ func findDefaultOriginCertPath() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func generateRandomClientID(logger logger.Service) (string, error) {
|
||||
func generateRandomClientID(log *zerolog.Logger) (string, error) {
|
||||
u, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
logger.Errorf("couldn't create UUID for client ID %s", err)
|
||||
log.Error().Msgf("couldn't create UUID for client ID %s", err)
|
||||
return "", err
|
||||
}
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func logClientOptions(c *cli.Context, logger logger.Service) {
|
||||
func logClientOptions(c *cli.Context, log *zerolog.Logger) {
|
||||
flags := make(map[string]interface{})
|
||||
for _, flag := range c.LocalFlagNames() {
|
||||
for _, flag := range c.FlagNames() {
|
||||
flags[flag] = c.Generic(flag)
|
||||
}
|
||||
|
||||
sliceFlags := []string{"header", "tag", "proxy-dns-upstream", "upstream", "edge"}
|
||||
for _, sliceFlag := range sliceFlags {
|
||||
if len(c.StringSlice(sliceFlag)) > 0 {
|
||||
flags[sliceFlag] = strings.Join(c.StringSlice(sliceFlag), ", ")
|
||||
}
|
||||
}
|
||||
|
||||
if len(flags) > 0 {
|
||||
logger.Infof("Environment variables %v", flags)
|
||||
log.Info().Msgf("Settings: %v", flags)
|
||||
}
|
||||
|
||||
envs := make(map[string]string)
|
||||
@@ -84,40 +83,39 @@ func logClientOptions(c *cli.Context, logger logger.Service) {
|
||||
}
|
||||
}
|
||||
if len(envs) > 0 {
|
||||
logger.Infof("Environmental variables %v", envs)
|
||||
log.Info().Msgf("Environmental variables %v", envs)
|
||||
}
|
||||
}
|
||||
|
||||
func dnsProxyStandAlone(c *cli.Context) bool {
|
||||
return c.IsSet("proxy-dns") && (!c.IsSet("hostname") && !c.IsSet("tag") && !c.IsSet("hello-world"))
|
||||
func dnsProxyStandAlone(c *cli.Context, namedTunnel *connection.NamedTunnelConfig) bool {
|
||||
return c.IsSet("proxy-dns") && (!c.IsSet("hostname") && !c.IsSet("tag") && !c.IsSet("hello-world") && namedTunnel == nil)
|
||||
}
|
||||
|
||||
func findOriginCert(c *cli.Context, logger logger.Service) (string, error) {
|
||||
originCertPath := c.String("origincert")
|
||||
func findOriginCert(originCertPath string, log *zerolog.Logger) (string, error) {
|
||||
if originCertPath == "" {
|
||||
logger.Infof("Cannot determine default origin certificate path. No file %s in %v", config.DefaultCredentialFile, config.DefaultConfigDirs)
|
||||
log.Info().Msgf("Cannot determine default origin certificate path. No file %s in %v", config.DefaultCredentialFile, config.DefaultConfigSearchDirectories())
|
||||
if isRunningFromTerminal() {
|
||||
logger.Errorf("You need to specify the origin certificate path with --origincert option, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", argumentsUrl)
|
||||
return "", fmt.Errorf("Client didn't specify origincert path when running from terminal")
|
||||
log.Error().Msgf("You need to specify the origin certificate path with --origincert option, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", argumentsUrl)
|
||||
return "", fmt.Errorf("client didn't specify origincert path when running from terminal")
|
||||
} else {
|
||||
logger.Errorf("You need to specify the origin certificate path by specifying the origincert option in the configuration file, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", serviceUrl)
|
||||
return "", fmt.Errorf("Client didn't specify origincert path")
|
||||
log.Error().Msgf("You need to specify the origin certificate path by specifying the origincert option in the configuration file, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", serviceUrl)
|
||||
return "", fmt.Errorf("client didn't specify origincert path")
|
||||
}
|
||||
}
|
||||
var err error
|
||||
originCertPath, err = homedir.Expand(originCertPath)
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot resolve path %s: %s", originCertPath, err)
|
||||
return "", fmt.Errorf("Cannot resolve path %s", originCertPath)
|
||||
log.Err(err).Msgf("Cannot resolve origin certificate path")
|
||||
return "", fmt.Errorf("cannot resolve path %s", originCertPath)
|
||||
}
|
||||
// Check that the user has acquired a certificate using the login command
|
||||
ok, err := config.FileExists(originCertPath)
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot check if origin cert exists at path %s", originCertPath)
|
||||
return "", fmt.Errorf("Cannot check if origin cert exists at path %s", originCertPath)
|
||||
log.Error().Err(err).Msgf("Cannot check if origin cert exists at path %s", originCertPath)
|
||||
return "", fmt.Errorf("cannot check if origin cert exists at path %s", originCertPath)
|
||||
}
|
||||
if !ok {
|
||||
logger.Errorf(`Cannot find a valid certificate for your origin at the path:
|
||||
log.Error().Msgf(`Cannot find a valid certificate for your origin at the path:
|
||||
|
||||
%s
|
||||
|
||||
@@ -126,186 +124,218 @@ If you don't have a certificate signed by Cloudflare, run the command:
|
||||
|
||||
%s login
|
||||
`, originCertPath, os.Args[0])
|
||||
return "", fmt.Errorf("Cannot find a valid certificate at the path %s", originCertPath)
|
||||
return "", fmt.Errorf("cannot find a valid certificate at the path %s", originCertPath)
|
||||
}
|
||||
|
||||
return originCertPath, nil
|
||||
}
|
||||
|
||||
func readOriginCert(originCertPath string, logger logger.Service) ([]byte, error) {
|
||||
logger.Debugf("Reading origin cert from %s", originCertPath)
|
||||
|
||||
func readOriginCert(originCertPath string) ([]byte, error) {
|
||||
// Easier to send the certificate as []byte via RPC than decoding it at this point
|
||||
originCert, err := ioutil.ReadFile(originCertPath)
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot read %s to load origin certificate: %s", originCertPath, err)
|
||||
return nil, fmt.Errorf("Cannot read %s to load origin certificate", originCertPath)
|
||||
return nil, fmt.Errorf("cannot read %s to load origin certificate", originCertPath)
|
||||
}
|
||||
return originCert, nil
|
||||
}
|
||||
|
||||
func getOriginCert(c *cli.Context, logger logger.Service) ([]byte, error) {
|
||||
if originCertPath, err := findOriginCert(c, logger); err != nil {
|
||||
func getOriginCert(originCertPath string, log *zerolog.Logger) ([]byte, error) {
|
||||
if originCertPath, err := findOriginCert(originCertPath, log); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return readOriginCert(originCertPath, logger)
|
||||
return readOriginCert(originCertPath)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
info *cliutil.BuildInfo,
|
||||
log, logTransport *zerolog.Logger,
|
||||
observer *connection.Observer,
|
||||
namedTunnel *connection.NamedTunnelConfig,
|
||||
) (*origin.TunnelConfig, ingress.Ingress, error) {
|
||||
isNamedTunnel := namedTunnel != nil
|
||||
|
||||
hostname, err := validation.ValidateHostname(c.String("hostname"))
|
||||
configHostname := c.String("hostname")
|
||||
hostname, err := validation.ValidateHostname(configHostname)
|
||||
if err != nil {
|
||||
logger.Errorf("Invalid hostname: %s", err)
|
||||
return nil, errors.Wrap(err, "Invalid hostname")
|
||||
log.Err(err).Str(LogFieldHostname, configHostname).Msg("Invalid hostname")
|
||||
return nil, ingress.Ingress{}, errors.Wrap(err, "Invalid hostname")
|
||||
}
|
||||
isFreeTunnel := hostname == ""
|
||||
clientID := c.String("id")
|
||||
if !c.IsSet("id") {
|
||||
clientID, err = generateRandomClientID(logger)
|
||||
clientID, err = generateRandomClientID(log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, ingress.Ingress{}, err
|
||||
}
|
||||
}
|
||||
|
||||
tags, err := NewTagSliceFromCLI(c.StringSlice("tag"))
|
||||
if err != nil {
|
||||
logger.Errorf("Tag parse failure: %s", err)
|
||||
return nil, errors.Wrap(err, "Tag parse failure")
|
||||
log.Err(err).Msg("Tag parse failure")
|
||||
return nil, ingress.Ingress{}, errors.Wrap(err, "Tag parse failure")
|
||||
}
|
||||
|
||||
tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID})
|
||||
|
||||
originURL, err := config.ValidateUrl(c, compatibilityMode)
|
||||
if err != nil {
|
||||
logger.Errorf("Error validating origin URL: %s", err)
|
||||
return nil, errors.Wrap(err, "Error validating origin URL")
|
||||
}
|
||||
|
||||
var originCert []byte
|
||||
if !isFreeTunnel {
|
||||
originCert, err = getOriginCert(c, logger)
|
||||
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 {
|
||||
var (
|
||||
ingressRules ingress.Ingress
|
||||
classicTunnel *connection.ClassicTunnelConfig
|
||||
)
|
||||
cfg := config.GetConfiguration()
|
||||
if isNamedTunnel {
|
||||
clientUUID, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can't generate clientUUID")
|
||||
return nil, ingress.Ingress{}, errors.Wrap(err, "can't generate connector UUID")
|
||||
}
|
||||
log.Info().Msgf("Generated Connector ID: %s", clientUUID)
|
||||
features := append(c.StringSlice("features"), origin.FeatureSerializedHeaders)
|
||||
namedTunnel.Client = tunnelpogs.ClientInfo{
|
||||
ClientID: clientUUID[:],
|
||||
Features: []string{origin.FeatureSerializedHeaders},
|
||||
Version: version,
|
||||
Arch: fmt.Sprintf("%s_%s", buildInfo.GoOS, buildInfo.GoArch),
|
||||
Features: dedup(features),
|
||||
Version: info.Version(),
|
||||
Arch: info.OSArch(),
|
||||
}
|
||||
ingressRules, err = ingress.ParseIngress(cfg)
|
||||
if err != nil && err != ingress.ErrNoIngressRules {
|
||||
return nil, ingress.Ingress{}, err
|
||||
}
|
||||
if !ingressRules.IsEmpty() && c.IsSet("url") {
|
||||
return nil, ingress.Ingress{}, ingress.ErrURLIncompatibleWithIngress
|
||||
}
|
||||
} else {
|
||||
|
||||
originCertPath := c.String("origincert")
|
||||
originCertLog := log.With().
|
||||
Str(LogFieldOriginCertPath, originCertPath).
|
||||
Logger()
|
||||
|
||||
originCert, err := getOriginCert(originCertPath, &originCertLog)
|
||||
if err != nil {
|
||||
return nil, ingress.Ingress{}, errors.Wrap(err, "Error getting origin cert")
|
||||
}
|
||||
|
||||
classicTunnel = &connection.ClassicTunnelConfig{
|
||||
Hostname: hostname,
|
||||
OriginCert: originCert,
|
||||
// turn off use of reconnect token and auth refresh when using named tunnels
|
||||
UseReconnectToken: !isNamedTunnel && c.Bool("use-reconnect-token"),
|
||||
}
|
||||
}
|
||||
|
||||
// Convert single-origin configuration into multi-origin configuration.
|
||||
if ingressRules.IsEmpty() {
|
||||
ingressRules, err = ingress.NewSingleOrigin(c, !isNamedTunnel)
|
||||
if err != nil {
|
||||
return nil, ingress.Ingress{}, err
|
||||
}
|
||||
}
|
||||
|
||||
var warpRoutingService *ingress.WarpRoutingService
|
||||
warpRoutingEnabled := isWarpRoutingEnabled(cfg.WarpRouting, isNamedTunnel)
|
||||
if warpRoutingEnabled {
|
||||
warpRoutingService = ingress.NewWarpRoutingService()
|
||||
log.Info().Msgf("Warp-routing is enabled")
|
||||
}
|
||||
|
||||
protocolSelector, err := connection.NewProtocolSelector(c.String("protocol"), warpRoutingEnabled, namedTunnel, edgediscovery.ProtocolPercentage, origin.ResolveTTL, log)
|
||||
if err != nil {
|
||||
return nil, ingress.Ingress{}, err
|
||||
}
|
||||
log.Info().Msgf("Initial protocol %s", protocolSelector.Current())
|
||||
|
||||
edgeTLSConfigs := make(map[connection.Protocol]*tls.Config, len(connection.ProtocolList))
|
||||
for _, p := range connection.ProtocolList {
|
||||
tlsSettings := p.TLSSettings()
|
||||
if tlsSettings == nil {
|
||||
return nil, ingress.Ingress{}, fmt.Errorf("%s has unknown TLS settings", p)
|
||||
}
|
||||
edgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c, tlsSettings.ServerName)
|
||||
if err != nil {
|
||||
return nil, ingress.Ingress{}, errors.Wrap(err, "unable to create TLS config to connect with edge")
|
||||
}
|
||||
if len(tlsSettings.NextProtos) > 0 {
|
||||
edgeTLSConfig.NextProtos = tlsSettings.NextProtos
|
||||
}
|
||||
edgeTLSConfigs[p] = edgeTLSConfig
|
||||
}
|
||||
|
||||
originProxy := origin.NewOriginProxy(ingressRules, warpRoutingService, tags, log)
|
||||
gracePeriod, err := gracePeriod(c)
|
||||
if err != nil {
|
||||
return nil, ingress.Ingress{}, err
|
||||
}
|
||||
connectionConfig := &connection.Config{
|
||||
OriginProxy: originProxy,
|
||||
GracePeriod: gracePeriod,
|
||||
ReplaceExisting: c.Bool("force"),
|
||||
}
|
||||
muxerConfig := &connection.MuxerConfig{
|
||||
HeartbeatInterval: c.Duration("heartbeat-interval"),
|
||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||
MaxHeartbeats: uint64(c.Int("heartbeat-count")),
|
||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||
CompressionSetting: h2mux.CompressionSetting(uint64(c.Int("compression-quality"))),
|
||||
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
|
||||
}
|
||||
|
||||
return &origin.TunnelConfig{
|
||||
BuildInfo: buildInfo,
|
||||
ClientID: clientID,
|
||||
ClientTlsConfig: httpTransport.TLSClientConfig,
|
||||
CompressionQuality: c.Uint64("compression-quality"),
|
||||
EdgeAddrs: c.StringSlice("edge"),
|
||||
GracePeriod: c.Duration("grace-period"),
|
||||
HAConnections: c.Int("ha-connections"),
|
||||
HTTPTransport: httpTransport,
|
||||
HeartbeatInterval: c.Duration("heartbeat-interval"),
|
||||
Hostname: hostname,
|
||||
HTTPHostHeader: c.String("http-host-header"),
|
||||
IncidentLookup: origin.NewIncidentLookup(),
|
||||
IsAutoupdated: c.Bool("is-autoupdated"),
|
||||
IsFreeTunnel: isFreeTunnel,
|
||||
LBPool: c.String("lb-pool"),
|
||||
Logger: logger,
|
||||
TransportLogger: transportLogger,
|
||||
MaxHeartbeats: c.Uint64("heartbeat-count"),
|
||||
Metrics: tunnelMetrics,
|
||||
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
|
||||
NoChunkedEncoding: c.Bool("no-chunked-encoding"),
|
||||
OriginCert: originCert,
|
||||
OriginUrl: originURL,
|
||||
ReportedVersion: version,
|
||||
Retries: c.Uint("retries"),
|
||||
RunFromTerminal: isRunningFromTerminal(),
|
||||
Tags: tags,
|
||||
TlsConfig: toEdgeTLSConfig,
|
||||
NamedTunnel: namedTunnel,
|
||||
ReplaceExisting: c.Bool("force"),
|
||||
// turn off use of reconnect token and auth refresh when using named tunnels
|
||||
UseReconnectToken: compatibilityMode && c.Bool("use-reconnect-token"),
|
||||
}, nil
|
||||
ConnectionConfig: connectionConfig,
|
||||
OSArch: info.OSArch(),
|
||||
ClientID: clientID,
|
||||
EdgeAddrs: c.StringSlice("edge"),
|
||||
Region: c.String("region"),
|
||||
HAConnections: c.Int("ha-connections"),
|
||||
IncidentLookup: origin.NewIncidentLookup(),
|
||||
IsAutoupdated: c.Bool("is-autoupdated"),
|
||||
LBPool: c.String("lb-pool"),
|
||||
Tags: tags,
|
||||
Log: log,
|
||||
LogTransport: logTransport,
|
||||
Observer: observer,
|
||||
ReportedVersion: info.Version(),
|
||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||
Retries: uint(c.Int("retries")),
|
||||
RunFromTerminal: isRunningFromTerminal(),
|
||||
NamedTunnel: namedTunnel,
|
||||
ClassicTunnel: classicTunnel,
|
||||
MuxerConfig: muxerConfig,
|
||||
ProtocolSelector: protocolSelector,
|
||||
EdgeTLSConfigs: edgeTLSConfigs,
|
||||
}, ingressRules, nil
|
||||
}
|
||||
|
||||
func gracePeriod(c *cli.Context) (time.Duration, error) {
|
||||
period := c.Duration("grace-period")
|
||||
if period > connection.MaxGracePeriod {
|
||||
return time.Duration(0), fmt.Errorf("grace-period must be equal or less than %v", connection.MaxGracePeriod)
|
||||
}
|
||||
return period, nil
|
||||
}
|
||||
|
||||
func isWarpRoutingEnabled(warpConfig config.WarpRoutingConfig, isNamedTunnel bool) bool {
|
||||
return warpConfig.Enabled && isNamedTunnel
|
||||
}
|
||||
|
||||
func isRunningFromTerminal() bool {
|
||||
return terminal.IsTerminal(int(os.Stdout.Fd()))
|
||||
}
|
||||
|
||||
// Remove any duplicates from the slice
|
||||
func dedup(slice []string) []string {
|
||||
|
||||
// Convert the slice into a set
|
||||
set := make(map[string]bool, 0)
|
||||
for _, str := range slice {
|
||||
set[str] = true
|
||||
}
|
||||
|
||||
// Convert the set back into a slice
|
||||
keys := make([]string, len(set))
|
||||
i := 0
|
||||
for str := range set {
|
||||
keys[i] = str
|
||||
i++
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
// TODO: Remove the above build tag and include this test when we start compiling with Golang 1.10.0+
|
||||
|
||||
package tunnel
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// CredFinder can find the tunnel credentials file.
|
||||
type CredFinder interface {
|
||||
Path() (string, error)
|
||||
}
|
||||
|
||||
// Implements CredFinder and looks for the credentials file at the given
|
||||
// filepath.
|
||||
type staticPath struct {
|
||||
filePath string
|
||||
fs fileSystem
|
||||
}
|
||||
|
||||
func newStaticPath(filePath string, fs fileSystem) CredFinder {
|
||||
return staticPath{
|
||||
filePath: filePath,
|
||||
fs: fs,
|
||||
}
|
||||
}
|
||||
|
||||
func (a staticPath) Path() (string, error) {
|
||||
if a.filePath != "" && a.fs.validFilePath(a.filePath) {
|
||||
return a.filePath, nil
|
||||
}
|
||||
return "", fmt.Errorf("Tunnel credentials file '%s' doesn't exist or is not a file", a.filePath)
|
||||
}
|
||||
|
||||
// Implements CredFinder and looks for the credentials file in several directories
|
||||
// searching for a file named <id>.json
|
||||
type searchByID struct {
|
||||
id uuid.UUID
|
||||
c *cli.Context
|
||||
log *zerolog.Logger
|
||||
fs fileSystem
|
||||
}
|
||||
|
||||
func newSearchByID(id uuid.UUID, c *cli.Context, log *zerolog.Logger, fs fileSystem) CredFinder {
|
||||
return searchByID{
|
||||
id: id,
|
||||
c: c,
|
||||
log: log,
|
||||
fs: fs,
|
||||
}
|
||||
}
|
||||
|
||||
func (s searchByID) Path() (string, error) {
|
||||
originCertPath := s.c.String("origincert")
|
||||
originCertLog := s.log.With().
|
||||
Str(LogFieldOriginCertPath, originCertPath).
|
||||
Logger()
|
||||
|
||||
// Fallback to look for tunnel credentials in the origin cert directory
|
||||
if originCertPath, err := findOriginCert(originCertPath, &originCertLog); err == nil {
|
||||
originCertDir := filepath.Dir(originCertPath)
|
||||
if filePath, err := tunnelFilePath(s.id, originCertDir); err == nil {
|
||||
if s.fs.validFilePath(filePath) {
|
||||
return filePath, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort look under default config directories
|
||||
for _, configDir := range config.DefaultConfigSearchDirectories() {
|
||||
if filePath, err := tunnelFilePath(s.id, configDir); err == nil {
|
||||
if s.fs.validFilePath(filePath) {
|
||||
return filePath, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("tunnel credentials file not found")
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Abstract away details of reading files, so that SubcommandContext can read
|
||||
// from either the real filesystem, or a mock (when running unit tests).
|
||||
type fileSystem interface {
|
||||
readFile(filePath string) ([]byte, error)
|
||||
validFilePath(path string) bool
|
||||
}
|
||||
|
||||
type realFileSystem struct{}
|
||||
|
||||
func (fs realFileSystem) validFilePath(path string) bool {
|
||||
fileStat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !fileStat.IsDir()
|
||||
}
|
||||
|
||||
func (fs realFileSystem) readFile(filePath string) ([]byte, error) {
|
||||
return ioutil.ReadFile(filePath)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
)
|
||||
|
||||
type Info struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Connectors []*cfapi.ActiveClient `json:"conns"`
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func buildIngressSubcommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "ingress",
|
||||
Category: "Tunnel",
|
||||
Usage: "Validate and test cloudflared tunnel's ingress configuration",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] ingress COMMAND [arguments...]",
|
||||
Hidden: true,
|
||||
Description: ` Cloudflared lets you route traffic from the internet to multiple different addresses on your
|
||||
origin. Multiple-origin routing is configured by a set of rules. Each rule matches traffic
|
||||
by its hostname or path, and routes it to an address. These rules are configured under the
|
||||
'ingress' key of your config.yaml, for example:
|
||||
|
||||
ingress:
|
||||
- hostname: www.example.com
|
||||
service: https://localhost:8000
|
||||
- hostname: *.example.xyz
|
||||
path: /[a-zA-Z]+.html
|
||||
service: https://localhost:8001
|
||||
- hostname: *
|
||||
service: https://localhost:8002
|
||||
|
||||
To ensure cloudflared can route all incoming requests, the last rule must be a catch-all
|
||||
rule that matches all traffic. You can validate these rules with the 'ingress validate'
|
||||
command, and test which rule matches a particular URL with 'ingress rule <URL>'.
|
||||
|
||||
Multiple-origin routing is incompatible with the --url flag.`,
|
||||
Subcommands: []*cli.Command{buildValidateIngressCommand(), buildTestURLCommand()},
|
||||
}
|
||||
}
|
||||
|
||||
func buildValidateIngressCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "validate",
|
||||
Action: cliutil.ConfiguredActionWithWarnings(validateIngressCommand),
|
||||
Usage: "Validate the ingress configuration ",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] ingress validate",
|
||||
Description: "Validates the configuration file, ensuring your ingress rules are OK.",
|
||||
}
|
||||
}
|
||||
|
||||
func buildTestURLCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "rule",
|
||||
Action: cliutil.ConfiguredAction(testURLCommand),
|
||||
Usage: "Check which ingress rule matches a given request URL",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] ingress rule URL",
|
||||
ArgsUsage: "URL",
|
||||
Description: "Check which ingress rule matches a given request URL. " +
|
||||
"Ingress rules match a request's hostname and path. Hostname is " +
|
||||
"optional and is either a full hostname like `www.example.com` or a " +
|
||||
"hostname with a `*` for its subdomains, e.g. `*.example.com`. Path " +
|
||||
"is optional and matches a regular expression, like `/[a-zA-Z0-9_]+.html`",
|
||||
}
|
||||
}
|
||||
|
||||
// validateIngressCommand check the syntax of the ingress rules in the cloudflared config file
|
||||
func validateIngressCommand(c *cli.Context, warnings string) error {
|
||||
conf := config.GetConfiguration()
|
||||
if conf.Source() == "" {
|
||||
fmt.Println("No configuration file was found. Please create one, or use the --config flag to specify its filepath. You can use the help command to learn more about configuration files")
|
||||
return nil
|
||||
}
|
||||
fmt.Println("Validating rules from", conf.Source())
|
||||
if _, err := ingress.ParseIngress(conf); err != nil {
|
||||
return errors.Wrap(err, "Validation failed")
|
||||
}
|
||||
if c.IsSet("url") {
|
||||
return ingress.ErrURLIncompatibleWithIngress
|
||||
}
|
||||
if warnings != "" {
|
||||
fmt.Println("Warning: unused keys detected in your config file. Here is a list of unused keys:")
|
||||
fmt.Println(warnings)
|
||||
return nil
|
||||
}
|
||||
fmt.Println("OK")
|
||||
return nil
|
||||
}
|
||||
|
||||
// testURLCommand checks which ingress rule matches the given URL.
|
||||
func testURLCommand(c *cli.Context) error {
|
||||
requestArg := c.Args().First()
|
||||
if requestArg == "" {
|
||||
return errors.New("cloudflared tunnel rule expects a single argument, the URL to test")
|
||||
}
|
||||
|
||||
requestURL, err := url.Parse(requestArg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s is not a valid URL", requestArg)
|
||||
}
|
||||
if requestURL.Hostname() == "" && requestURL.Scheme == "" {
|
||||
return fmt.Errorf("%s doesn't have a hostname, consider adding a scheme", requestArg)
|
||||
}
|
||||
|
||||
conf := config.GetConfiguration()
|
||||
fmt.Println("Using rules from", conf.Source())
|
||||
ing, err := ingress.ParseIngress(conf)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Validation failed")
|
||||
}
|
||||
|
||||
_, i := ing.FindMatchingRule(requestURL.Hostname(), requestURL.Path)
|
||||
fmt.Printf("Matched rule #%d\n", i+1)
|
||||
fmt.Println(ing.Rules[i].MultiLineString())
|
||||
return nil
|
||||
}
|
||||
@@ -2,29 +2,39 @@ package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"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 "gopkg.in/urfave/cli.v2"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/token"
|
||||
)
|
||||
|
||||
const (
|
||||
baseLoginURL = "https://dash.cloudflare.com/argotunnel"
|
||||
callbackStoreURL = "https://login.argotunnel.com/"
|
||||
callbackStoreURL = "https://login.cloudflareaccess.org/"
|
||||
)
|
||||
|
||||
func login(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
func buildLoginSubcommand(hidden bool) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "login",
|
||||
Action: cliutil.ConfiguredAction(login),
|
||||
Usage: "Generate a configuration file with your login details",
|
||||
ArgsUsage: " ",
|
||||
Hidden: hidden,
|
||||
}
|
||||
}
|
||||
|
||||
func login(c *cli.Context) error {
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
path, ok, err := checkForExistingCert()
|
||||
if ok {
|
||||
@@ -40,18 +50,30 @@ func login(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = transfer.Run(loginURL, "cert", "callback", callbackStoreURL, path, false, true, logger)
|
||||
resourceData, err := token.RunTransfer(
|
||||
loginURL,
|
||||
"cert",
|
||||
"callback",
|
||||
callbackStoreURL,
|
||||
false,
|
||||
false,
|
||||
log,
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to write the certificate due to the following error:\n%v\n\nYour browser will download the certificate instead. You will have to manually\ncopy it to the following path:\n\n%s\n", err, path)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(path, resourceData, 0600); err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("error writing cert to %s", path))
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stdout, "You have successfully logged in.\nIf you wish to copy your credentials to a server, they have been saved to:\n%s\n", path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkForExistingCert() (string, bool, error) {
|
||||
configPath, err := homedir.Expand(config.DefaultConfigDirs[0])
|
||||
configPath, err := homedir.Expand(config.DefaultConfigSearchDirectories()[0])
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
)
|
||||
|
||||
const httpTimeout = 15 * time.Second
|
||||
|
||||
const disclaimer = "Thank you for trying Cloudflare Tunnel. Doing so, without a Cloudflare account, is a quick way to" +
|
||||
" experiment and try it out. However, be aware that these account-less Tunnels have no uptime guarantee. If you " +
|
||||
"intend to use Tunnels in production you should use a pre-created named tunnel by following: " +
|
||||
"https://developers.cloudflare.com/cloudflare-one/connections/connect-apps"
|
||||
|
||||
// RunQuickTunnel requests a tunnel from the specified service.
|
||||
// We use this to power quick tunnels on trycloudflare.com, but the
|
||||
// service is open-source and could be used by anyone.
|
||||
func RunQuickTunnel(sc *subcommandContext) error {
|
||||
sc.log.Info().Msg(disclaimer)
|
||||
sc.log.Info().Msg("Requesting new quick Tunnel on trycloudflare.com...")
|
||||
|
||||
client := http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSHandshakeTimeout: httpTimeout,
|
||||
ResponseHeaderTimeout: httpTimeout,
|
||||
},
|
||||
Timeout: httpTimeout,
|
||||
}
|
||||
|
||||
resp, err := client.Post(fmt.Sprintf("%s/tunnel", sc.c.String("quick-service")), "application/json", nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to request quick Tunnel")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var data QuickTunnelResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
return errors.Wrap(err, "failed to unmarshal quick Tunnel")
|
||||
}
|
||||
|
||||
tunnelID, err := uuid.Parse(data.Result.ID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse quick Tunnel ID")
|
||||
}
|
||||
|
||||
credentials := connection.Credentials{
|
||||
AccountTag: data.Result.AccountTag,
|
||||
TunnelSecret: data.Result.Secret,
|
||||
TunnelID: tunnelID,
|
||||
TunnelName: data.Result.Name,
|
||||
}
|
||||
|
||||
url := data.Result.Hostname
|
||||
if !strings.HasPrefix(url, "https://") {
|
||||
url = "https://" + url
|
||||
}
|
||||
|
||||
for _, line := range AsciiBox([]string{
|
||||
"Your quick Tunnel has been created! Visit it at (it may take some time to be reachable):",
|
||||
url,
|
||||
}, 2) {
|
||||
sc.log.Info().Msg(line)
|
||||
}
|
||||
|
||||
if !sc.c.IsSet("protocol") {
|
||||
sc.c.Set("protocol", "quic")
|
||||
}
|
||||
|
||||
return StartServer(
|
||||
sc.c,
|
||||
buildInfo,
|
||||
&connection.NamedTunnelConfig{Credentials: credentials, QuickTunnelUrl: data.Result.Hostname},
|
||||
sc.log,
|
||||
sc.isUIEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
type QuickTunnelResponse struct {
|
||||
Success bool
|
||||
Result QuickTunnel
|
||||
Errors []QuickTunnelError
|
||||
}
|
||||
|
||||
type QuickTunnelError struct {
|
||||
Code int
|
||||
Message string
|
||||
}
|
||||
|
||||
type QuickTunnel struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Hostname string `json:"hostname"`
|
||||
AccountTag string `json:"account_tag"`
|
||||
Secret []byte `json:"secret"`
|
||||
}
|
||||
|
||||
// Print out the given lines in a nice ASCII box.
|
||||
func AsciiBox(lines []string, padding int) (box []string) {
|
||||
maxLen := maxLen(lines)
|
||||
spacer := strings.Repeat(" ", padding)
|
||||
border := "+" + strings.Repeat("-", maxLen+(padding*2)) + "+"
|
||||
box = append(box, border)
|
||||
for _, line := range lines {
|
||||
box = append(box, "|"+spacer+line+strings.Repeat(" ", maxLen-len(line))+spacer+"|")
|
||||
}
|
||||
box = append(box, border)
|
||||
return
|
||||
}
|
||||
|
||||
func maxLen(lines []string) int {
|
||||
max := 0
|
||||
for _, line := range lines {
|
||||
if len(line) > max {
|
||||
max = len(line)
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
@@ -1,20 +1,25 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"fmt"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func runDNSProxyServer(c *cli.Context, dnsReadySignal, shutdownC chan struct{}, logger logger.Service) error {
|
||||
func runDNSProxyServer(c *cli.Context, dnsReadySignal chan struct{}, shutdownC <-chan struct{}, log *zerolog.Logger) error {
|
||||
port := c.Int("proxy-dns-port")
|
||||
if port <= 0 || port > 65535 {
|
||||
return errors.New("The 'proxy-dns-port' must be a valid port number in <1, 65535> range.")
|
||||
}
|
||||
listener, err := tunneldns.CreateListener(c.String("proxy-dns-address"), uint16(port), c.StringSlice("proxy-dns-upstream"), c.StringSlice("proxy-dns-bootstrap"), logger)
|
||||
maxUpstreamConnections := c.Int("proxy-dns-max-upstream-conns")
|
||||
if maxUpstreamConnections < 0 {
|
||||
return fmt.Errorf("'%s' must be 0 or higher", "proxy-dns-max-upstream-conns")
|
||||
}
|
||||
listener, err := tunneldns.CreateListener(c.String("proxy-dns-address"), uint16(port), c.StringSlice("proxy-dns-upstream"), c.StringSlice("proxy-dns-bootstrap"), maxUpstreamConnections, log)
|
||||
if err != nil {
|
||||
close(dnsReadySignal)
|
||||
listener.Stop()
|
||||
@@ -26,6 +31,7 @@ func runDNSProxyServer(c *cli.Context, dnsReadySignal, shutdownC chan struct{},
|
||||
return errors.Wrap(err, "Cannot start the DNS over HTTPS proxy server")
|
||||
}
|
||||
<-shutdownC
|
||||
listener.Stop()
|
||||
_ = listener.Stop()
|
||||
log.Info().Msg("DNS server stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,83 +4,20 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// waitForSignal notifies all routines to shutdownC immediately by closing the
|
||||
// shutdownC when one of the routines in main exits, or when this process receives
|
||||
// SIGTERM/SIGINT
|
||||
func waitForSignal(errC chan error, shutdownC chan struct{}, logger logger.Service) error {
|
||||
// waitForSignal closes graceShutdownC to indicate that we should start graceful shutdown sequence
|
||||
func waitForSignal(graceShutdownC chan struct{}, logger *zerolog.Logger) {
|
||||
signals := make(chan os.Signal, 10)
|
||||
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
|
||||
defer signal.Stop(signals)
|
||||
|
||||
select {
|
||||
case err := <-errC:
|
||||
logger.Infof("terminating due to error: %v", err)
|
||||
close(shutdownC)
|
||||
return err
|
||||
case s := <-signals:
|
||||
logger.Infof("terminating due to signal %s", s)
|
||||
close(shutdownC)
|
||||
case <-shutdownC:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// waitForSignalWithGraceShutdown notifies all routines to shutdown immediately
|
||||
// by closing the shutdownC when one of the routines in main exits.
|
||||
// When this process recieves SIGTERM/SIGINT, it closes the graceShutdownC to
|
||||
// notify certain routines to start graceful shutdown. When grace period is over,
|
||||
// or when some routine exits, it notifies the rest of the routines to shutdown
|
||||
// immediately by closing shutdownC.
|
||||
// In the case of handling commands from Windows Service Manager, closing graceShutdownC
|
||||
// initiate graceful shutdown.
|
||||
func waitForSignalWithGraceShutdown(errC chan error,
|
||||
shutdownC, graceShutdownC chan struct{},
|
||||
gracePeriod time.Duration,
|
||||
logger logger.Service,
|
||||
) error {
|
||||
signals := make(chan os.Signal, 10)
|
||||
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
|
||||
defer signal.Stop(signals)
|
||||
|
||||
select {
|
||||
case err := <-errC:
|
||||
logger.Infof("Initiating graceful shutdown due to %v ...", err)
|
||||
logger.Info().Msgf("Initiating graceful shutdown due to signal %s ...", s)
|
||||
close(graceShutdownC)
|
||||
close(shutdownC)
|
||||
return err
|
||||
case s := <-signals:
|
||||
logger.Infof("Initiating graceful shutdown due to signal %s ...", s)
|
||||
close(graceShutdownC)
|
||||
waitForGracePeriod(signals, errC, shutdownC, gracePeriod, logger)
|
||||
case <-graceShutdownC:
|
||||
waitForGracePeriod(signals, errC, shutdownC, gracePeriod, logger)
|
||||
case <-shutdownC:
|
||||
close(graceShutdownC)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func waitForGracePeriod(signals chan os.Signal,
|
||||
errC chan error,
|
||||
shutdownC chan struct{},
|
||||
gracePeriod time.Duration,
|
||||
logger logger.Service,
|
||||
) {
|
||||
// Unregister signal handler early, so the client can send a second SIGTERM/SIGINT
|
||||
// to force shutdown cloudflared
|
||||
signal.Stop(signals)
|
||||
graceTimerTick := time.Tick(gracePeriod)
|
||||
// send close signal via shutdownC when grace period expires or when an
|
||||
// error is encountered.
|
||||
select {
|
||||
case <-graceTimerTick:
|
||||
case <-errC:
|
||||
}
|
||||
close(shutdownC)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -18,140 +22,85 @@ var (
|
||||
graceShutdownErr = fmt.Errorf("receive grace shutdown")
|
||||
)
|
||||
|
||||
func testChannelClosed(t *testing.T, c chan struct{}) {
|
||||
func channelClosed(c chan struct{}) bool {
|
||||
select {
|
||||
case <-c:
|
||||
return
|
||||
return true
|
||||
default:
|
||||
t.Fatal("Channel should be closed")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForSignal(t *testing.T) {
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
// Test handling server error
|
||||
errC := make(chan error)
|
||||
shutdownC := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
errC <- serverErr
|
||||
}()
|
||||
|
||||
// received error, shutdownC should be closed
|
||||
err := waitForSignal(errC, shutdownC, logger)
|
||||
assert.Equal(t, serverErr, err)
|
||||
testChannelClosed(t, shutdownC)
|
||||
func TestSignalShutdown(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
|
||||
// Test handling SIGTERM & SIGINT
|
||||
for _, sig := range []syscall.Signal{syscall.SIGTERM, syscall.SIGINT} {
|
||||
errC = make(chan error)
|
||||
shutdownC = make(chan struct{})
|
||||
|
||||
go func(shutdownC chan struct{}) {
|
||||
<-shutdownC
|
||||
errC <- shutdownErr
|
||||
}(shutdownC)
|
||||
graceShutdownC := make(chan struct{})
|
||||
|
||||
go func(sig syscall.Signal) {
|
||||
// sleep for a tick to prevent sending signal before calling waitForSignal
|
||||
time.Sleep(tick)
|
||||
syscall.Kill(syscall.Getpid(), sig)
|
||||
_ = syscall.Kill(syscall.Getpid(), sig)
|
||||
}(sig)
|
||||
|
||||
err = waitForSignal(errC, shutdownC, logger)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, shutdownErr, <-errC)
|
||||
testChannelClosed(t, shutdownC)
|
||||
time.AfterFunc(time.Second, func() {
|
||||
select {
|
||||
case <-graceShutdownC:
|
||||
default:
|
||||
close(graceShutdownC)
|
||||
t.Fatal("waitForSignal timed out")
|
||||
}
|
||||
})
|
||||
|
||||
waitForSignal(graceShutdownC, &log)
|
||||
assert.True(t, channelClosed(graceShutdownC))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForSignalWithGraceShutdown(t *testing.T) {
|
||||
// Test server returning error
|
||||
errC := make(chan error)
|
||||
shutdownC := make(chan struct{})
|
||||
graceshutdownC := make(chan struct{})
|
||||
func TestWaitForShutdown(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
|
||||
errC := make(chan error)
|
||||
graceShutdownC := make(chan struct{})
|
||||
const gracePeriod = 5 * time.Second
|
||||
|
||||
contextCancelled := false
|
||||
cancel := func() {
|
||||
contextCancelled = true
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// on, error stop immediately
|
||||
contextCancelled = false
|
||||
startTime := time.Now()
|
||||
go func() {
|
||||
errC <- serverErr
|
||||
}()
|
||||
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
|
||||
// received error, both shutdownC and graceshutdownC should be closed
|
||||
err := waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
|
||||
err := waitToShutdown(&wg, cancel, errC, graceShutdownC, gracePeriod, &log)
|
||||
assert.Equal(t, serverErr, err)
|
||||
testChannelClosed(t, shutdownC)
|
||||
testChannelClosed(t, graceshutdownC)
|
||||
assert.True(t, contextCancelled)
|
||||
assert.False(t, channelClosed(graceShutdownC))
|
||||
assert.True(t, time.Now().Sub(startTime) < time.Second) // check that wait ended early
|
||||
|
||||
// shutdownC closed, graceshutdownC should also be closed and no error
|
||||
errC = make(chan error)
|
||||
shutdownC = make(chan struct{})
|
||||
graceshutdownC = make(chan struct{})
|
||||
close(shutdownC)
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
|
||||
assert.NoError(t, err)
|
||||
testChannelClosed(t, shutdownC)
|
||||
testChannelClosed(t, graceshutdownC)
|
||||
// on graceful shutdown, ignore error but stop as soon as an error arrives
|
||||
contextCancelled = false
|
||||
startTime = time.Now()
|
||||
go func() {
|
||||
close(graceShutdownC)
|
||||
time.Sleep(tick)
|
||||
errC <- serverErr
|
||||
}()
|
||||
err = waitToShutdown(&wg, cancel, errC, graceShutdownC, gracePeriod, &log)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, contextCancelled)
|
||||
assert.True(t, time.Now().Sub(startTime) < time.Second) // check that wait ended early
|
||||
|
||||
// graceshutdownC closed, shutdownC should also be closed and no error
|
||||
errC = make(chan error)
|
||||
shutdownC = make(chan struct{})
|
||||
graceshutdownC = make(chan struct{})
|
||||
close(graceshutdownC)
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
|
||||
assert.NoError(t, err)
|
||||
testChannelClosed(t, shutdownC)
|
||||
testChannelClosed(t, graceshutdownC)
|
||||
|
||||
// Test handling SIGTERM & SIGINT
|
||||
for _, sig := range []syscall.Signal{syscall.SIGTERM, syscall.SIGINT} {
|
||||
errC := make(chan error)
|
||||
shutdownC = make(chan struct{})
|
||||
graceshutdownC = make(chan struct{})
|
||||
|
||||
go func(shutdownC, graceshutdownC chan struct{}) {
|
||||
<-graceshutdownC
|
||||
<-shutdownC
|
||||
errC <- graceShutdownErr
|
||||
}(shutdownC, graceshutdownC)
|
||||
|
||||
go func(sig syscall.Signal) {
|
||||
// sleep for a tick to prevent sending signal before calling waitForSignalWithGraceShutdown
|
||||
time.Sleep(tick)
|
||||
syscall.Kill(syscall.Getpid(), sig)
|
||||
}(sig)
|
||||
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, graceShutdownErr, <-errC)
|
||||
testChannelClosed(t, shutdownC)
|
||||
testChannelClosed(t, graceshutdownC)
|
||||
}
|
||||
|
||||
// Test handling SIGTERM & SIGINT, server send error before end of grace period
|
||||
for _, sig := range []syscall.Signal{syscall.SIGTERM, syscall.SIGINT} {
|
||||
errC := make(chan error)
|
||||
shutdownC = make(chan struct{})
|
||||
graceshutdownC = make(chan struct{})
|
||||
|
||||
go func(shutdownC, graceshutdownC chan struct{}) {
|
||||
<-graceshutdownC
|
||||
errC <- graceShutdownErr
|
||||
<-shutdownC
|
||||
errC <- shutdownErr
|
||||
}(shutdownC, graceshutdownC)
|
||||
|
||||
go func(sig syscall.Signal) {
|
||||
// sleep for a tick to prevent sending signal before calling waitForSignalWithGraceShutdown
|
||||
time.Sleep(tick)
|
||||
syscall.Kill(syscall.Getpid(), sig)
|
||||
}(sig)
|
||||
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, shutdownErr, <-errC)
|
||||
testChannelClosed(t, shutdownC)
|
||||
testChannelClosed(t, graceshutdownC)
|
||||
}
|
||||
// with graceShutdownC closed stop right away without grace period
|
||||
contextCancelled = false
|
||||
startTime = time.Now()
|
||||
err = waitToShutdown(&wg, cancel, errC, graceShutdownC, 0, &log)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, contextCancelled)
|
||||
assert.True(t, time.Now().Sub(startTime) < time.Second) // check that wait ended early
|
||||
}
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/certutil"
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
type errInvalidJSONCredential struct {
|
||||
err error
|
||||
path string
|
||||
}
|
||||
|
||||
func (e errInvalidJSONCredential) Error() string {
|
||||
return "Invalid JSON when parsing tunnel credentials file"
|
||||
}
|
||||
|
||||
// subcommandContext carries structs shared between subcommands, to reduce number of arguments needed to
|
||||
// pass between subcommands, and make sure they are only initialized once
|
||||
type subcommandContext struct {
|
||||
c *cli.Context
|
||||
log *zerolog.Logger
|
||||
isUIEnabled bool
|
||||
fs fileSystem
|
||||
|
||||
// These fields should be accessed using their respective Getter
|
||||
tunnelstoreClient cfapi.Client
|
||||
userCredential *userCredential
|
||||
}
|
||||
|
||||
func newSubcommandContext(c *cli.Context) (*subcommandContext, error) {
|
||||
isUIEnabled := c.IsSet(uiFlag) && c.String("name") != ""
|
||||
|
||||
// If UI is enabled, terminal log output should be disabled -- log should be written into a UI log window instead
|
||||
log := logger.CreateLoggerFromContext(c, isUIEnabled)
|
||||
|
||||
return &subcommandContext{
|
||||
c: c,
|
||||
log: log,
|
||||
isUIEnabled: isUIEnabled,
|
||||
fs: realFileSystem{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Returns something that can find the given tunnel's credentials file.
|
||||
func (sc *subcommandContext) credentialFinder(tunnelID uuid.UUID) CredFinder {
|
||||
if path := sc.c.String(CredFileFlag); path != "" {
|
||||
return newStaticPath(path, sc.fs)
|
||||
}
|
||||
return newSearchByID(tunnelID, sc.c, sc.log, sc.fs)
|
||||
}
|
||||
|
||||
type userCredential struct {
|
||||
cert *certutil.OriginCert
|
||||
certPath string
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) client() (cfapi.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", buildInfo.Version())
|
||||
client, err := cfapi.NewRESTClient(
|
||||
sc.c.String("api-url"),
|
||||
credential.cert.AccountID,
|
||||
credential.cert.ZoneID,
|
||||
credential.cert.ServiceKey,
|
||||
userAgent,
|
||||
sc.log,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sc.tunnelstoreClient = client
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) credential() (*userCredential, error) {
|
||||
if sc.userCredential == nil {
|
||||
originCertPath := sc.c.String("origincert")
|
||||
originCertLog := sc.log.With().
|
||||
Str(LogFieldOriginCertPath, originCertPath).
|
||||
Logger()
|
||||
|
||||
originCertPath, err := findOriginCert(originCertPath, &originCertLog)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error locating origin cert")
|
||||
}
|
||||
blocks, err := readOriginCert(originCertPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Can't read origin cert from %s", originCertPath)
|
||||
}
|
||||
|
||||
cert, err := certutil.DecodeOriginCert(blocks)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error decoding origin cert")
|
||||
}
|
||||
|
||||
if cert.AccountID == "" {
|
||||
return nil, errors.Errorf(`Origin certificate needs to be refreshed before creating new tunnels.\nDelete %s and run "cloudflared login" to obtain a new cert.`, originCertPath)
|
||||
}
|
||||
|
||||
sc.userCredential = &userCredential{
|
||||
cert: cert,
|
||||
certPath: originCertPath,
|
||||
}
|
||||
}
|
||||
return sc.userCredential, nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) readTunnelCredentials(credFinder CredFinder) (connection.Credentials, error) {
|
||||
filePath, err := credFinder.Path()
|
||||
if err != nil {
|
||||
return connection.Credentials{}, err
|
||||
}
|
||||
body, err := sc.fs.readFile(filePath)
|
||||
if err != nil {
|
||||
return connection.Credentials{}, errors.Wrapf(err, "couldn't read tunnel credentials from %v", filePath)
|
||||
}
|
||||
|
||||
var credentials connection.Credentials
|
||||
if err = json.Unmarshal(body, &credentials); err != nil {
|
||||
if strings.HasSuffix(filePath, ".pem") {
|
||||
return connection.Credentials{}, fmt.Errorf("The tunnel credentials file should be .json but you gave a .pem. " +
|
||||
"The tunnel credentials file was originally created by `cloudflared tunnel create`. " +
|
||||
"You may have accidentally used the filepath to cert.pem, which is generated by `cloudflared tunnel " +
|
||||
"login`.")
|
||||
}
|
||||
return connection.Credentials{}, errInvalidJSONCredential{path: filePath, err: err}
|
||||
}
|
||||
return credentials, nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) create(name string, credentialsFilePath string, secret string) (*cfapi.Tunnel, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "couldn't create client to talk to Cloudflare Tunnel backend")
|
||||
}
|
||||
|
||||
var tunnelSecret []byte
|
||||
if secret == "" {
|
||||
tunnelSecret, err = generateTunnelSecret()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "couldn't generate the secret for your new tunnel")
|
||||
}
|
||||
} else {
|
||||
decodedSecret, err := base64.StdEncoding.DecodeString(secret)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Couldn't decode tunnel secret from base64")
|
||||
}
|
||||
tunnelSecret = []byte(decodedSecret)
|
||||
if len(tunnelSecret) < 32 {
|
||||
return nil, errors.New("Decoded tunnel secret must be at least 32 bytes long")
|
||||
}
|
||||
}
|
||||
|
||||
tunnel, err := client.CreateTunnel(name, tunnelSecret)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Create Tunnel API call failed")
|
||||
}
|
||||
|
||||
credential, err := sc.credential()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tunnelCredentials := connection.Credentials{
|
||||
AccountTag: credential.cert.AccountID,
|
||||
TunnelSecret: tunnelSecret,
|
||||
TunnelID: tunnel.ID,
|
||||
TunnelName: name,
|
||||
}
|
||||
usedCertPath := false
|
||||
if credentialsFilePath == "" {
|
||||
originCertDir := filepath.Dir(credential.certPath)
|
||||
credentialsFilePath, err = tunnelFilePath(tunnelCredentials.TunnelID, originCertDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
usedCertPath = true
|
||||
}
|
||||
writeFileErr := writeTunnelCredentials(credentialsFilePath, &tunnelCredentials)
|
||||
if writeFileErr != nil {
|
||||
var errorLines []string
|
||||
errorLines = append(errorLines, fmt.Sprintf("Your tunnel '%v' was created with ID %v. However, cloudflared couldn't write tunnel credentials to %s.", tunnel.Name, tunnel.ID, credentialsFilePath))
|
||||
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 credentials file"))
|
||||
}
|
||||
errorMsg := strings.Join(errorLines, "\n")
|
||||
return nil, errors.New(errorMsg)
|
||||
}
|
||||
|
||||
if outputFormat := sc.c.String(outputFormatFlag.Name); outputFormat != "" {
|
||||
return nil, renderOutput(outputFormat, &tunnel)
|
||||
}
|
||||
|
||||
fmt.Printf("Tunnel credentials written to %v.", credentialsFilePath)
|
||||
if usedCertPath {
|
||||
fmt.Print(" cloudflared chose this file based on where your origin certificate was found.")
|
||||
}
|
||||
fmt.Println(" Keep this file secret. To revoke these credentials, delete the tunnel.")
|
||||
fmt.Printf("\nCreated tunnel %s with id %s\n", tunnel.Name, tunnel.ID)
|
||||
return tunnel, nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) list(filter *cfapi.TunnelFilter) ([]*cfapi.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", 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)
|
||||
}
|
||||
if forceFlagSet {
|
||||
if err := client.CleanupConnections(tunnel.ID, cfapi.NewCleanupParams()); err != nil {
|
||||
return errors.Wrapf(err, "Error cleaning up connections for tunnel %s", tunnel.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if err := client.DeleteTunnel(tunnel.ID); err != nil {
|
||||
return errors.Wrapf(err, "Error deleting tunnel %s", tunnel.ID)
|
||||
}
|
||||
|
||||
credFinder := sc.credentialFinder(id)
|
||||
if tunnelCredentialsPath, err := credFinder.Path(); err == nil {
|
||||
if err = os.Remove(tunnelCredentialsPath); err != nil {
|
||||
sc.log.Info().Msgf("Tunnel %v was deleted, but we could not remove its credentials file %s: %s. Consider deleting this file manually.", id, tunnelCredentialsPath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// findCredentials will choose the right way to find the credentials file, find it,
|
||||
// and add the TunnelID into any old credentials (generated before TUN-3581 added the `TunnelID`
|
||||
// field to credentials files)
|
||||
func (sc *subcommandContext) findCredentials(tunnelID uuid.UUID) (connection.Credentials, error) {
|
||||
var credentials connection.Credentials
|
||||
var err error
|
||||
if credentialsContents := sc.c.String(CredContentsFlag); credentialsContents != "" {
|
||||
if err = json.Unmarshal([]byte(credentialsContents), &credentials); err != nil {
|
||||
err = errInvalidJSONCredential{path: "TUNNEL_CRED_CONTENTS", err: err}
|
||||
}
|
||||
} else {
|
||||
credFinder := sc.credentialFinder(tunnelID)
|
||||
credentials, err = sc.readTunnelCredentials(credFinder)
|
||||
}
|
||||
// This line ensures backwards compatibility with credentials files generated before
|
||||
// TUN-3581. Those old credentials files don't have a TunnelID field, so we enrich the struct
|
||||
// with the ID, which we have already resolved from the user input.
|
||||
credentials.TunnelID = tunnelID
|
||||
return credentials, err
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) run(tunnelID uuid.UUID) error {
|
||||
credentials, err := sc.findCredentials(tunnelID)
|
||||
if err != nil {
|
||||
if e, ok := err.(errInvalidJSONCredential); ok {
|
||||
sc.log.Error().Msgf("The credentials file at %s contained invalid JSON. This is probably caused by passing the wrong filepath. Reminder: the credentials file is a .json file created via `cloudflared tunnel create`.", e.path)
|
||||
sc.log.Error().Msgf("Invalid JSON when parsing credentials file: %s", e.err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return StartServer(
|
||||
sc.c,
|
||||
buildInfo,
|
||||
&connection.NamedTunnelConfig{Credentials: credentials},
|
||||
sc.log,
|
||||
sc.isUIEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) cleanupConnections(tunnelIDs []uuid.UUID) error {
|
||||
params := cfapi.NewCleanupParams()
|
||||
extraLog := ""
|
||||
if connector := sc.c.String("connector-id"); connector != "" {
|
||||
connectorID, err := uuid.Parse(connector)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "%s is not a valid client ID (must be a UUID)", connector)
|
||||
}
|
||||
params.ForClient(connectorID)
|
||||
extraLog = fmt.Sprintf(" for connector-id %s", connectorID.String())
|
||||
}
|
||||
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, tunnelID := range tunnelIDs {
|
||||
sc.log.Info().Msgf("Cleanup connection for tunnel %s%s", tunnelID, extraLog)
|
||||
if err := client.CleanupConnections(tunnelID, params); err != nil {
|
||||
sc.log.Error().Msgf("Error cleaning up connections for tunnel %v, error :%v", tunnelID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) route(tunnelID uuid.UUID, r cfapi.HostnameRoute) (cfapi.HostnameRouteResult, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.RouteTunnel(tunnelID, r)
|
||||
}
|
||||
|
||||
// Query Tunnelstore to find the active tunnel with the given name.
|
||||
func (sc *subcommandContext) tunnelActive(name string) (*cfapi.Tunnel, bool, error) {
|
||||
filter := cfapi.NewTunnelFilter()
|
||||
filter.NoDeleted()
|
||||
filter.ByName(name)
|
||||
tunnels, err := sc.list(filter)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if len(tunnels) == 0 {
|
||||
return nil, false, nil
|
||||
}
|
||||
// There should only be 1 active tunnel for a given name
|
||||
return tunnels[0], true, nil
|
||||
}
|
||||
|
||||
// findID parses the input. If it's a UUID, return the UUID.
|
||||
// Otherwise, assume it's a name, and look up the ID of that tunnel.
|
||||
func (sc *subcommandContext) findID(input string) (uuid.UUID, error) {
|
||||
if u, err := uuid.Parse(input); err == nil {
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// Look up name in the credentials file.
|
||||
credFinder := newStaticPath(sc.c.String(CredFileFlag), sc.fs)
|
||||
if credentials, err := sc.readTunnelCredentials(credFinder); err == nil {
|
||||
if credentials.TunnelID != uuid.Nil && input == credentials.TunnelName {
|
||||
return credentials.TunnelID, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to querying Tunnelstore.
|
||||
if tunnel, found, err := sc.tunnelActive(input); err != nil {
|
||||
return uuid.Nil, err
|
||||
} else if found {
|
||||
return tunnel.ID, nil
|
||||
}
|
||||
|
||||
return uuid.Nil, fmt.Errorf("%s is neither the ID nor the name of any of your tunnels", input)
|
||||
}
|
||||
|
||||
// findIDs is just like mapping `findID` over a slice, but it only uses
|
||||
// one Tunnelstore API call per non-UUID input provided.
|
||||
func (sc *subcommandContext) findIDs(inputs []string) ([]uuid.UUID, error) {
|
||||
uuids, names := splitUuids(inputs)
|
||||
|
||||
for _, name := range names {
|
||||
filter := cfapi.NewTunnelFilter()
|
||||
filter.NoDeleted()
|
||||
filter.ByName(name)
|
||||
|
||||
tunnels, err := sc.list(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(tunnels) != 1 {
|
||||
return nil, fmt.Errorf("there should only be 1 non-deleted Tunnel named %s", name)
|
||||
}
|
||||
|
||||
uuids = append(uuids, tunnels[0].ID)
|
||||
}
|
||||
|
||||
return uuids, nil
|
||||
}
|
||||
|
||||
func splitUuids(inputs []string) ([]uuid.UUID, []string) {
|
||||
uuids := make([]uuid.UUID, 0)
|
||||
names := make([]string, 0)
|
||||
|
||||
for _, input := range inputs {
|
||||
id, err := uuid.Parse(input)
|
||||
if err != nil {
|
||||
names = append(names, input)
|
||||
} else {
|
||||
uuids = append(uuids, id)
|
||||
}
|
||||
}
|
||||
|
||||
return uuids, names
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
)
|
||||
|
||||
const noClientMsg = "error while creating backend client"
|
||||
|
||||
func (sc *subcommandContext) listRoutes(filter *cfapi.IpRouteFilter) ([]*cfapi.DetailedRoute, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, noClientMsg)
|
||||
}
|
||||
return client.ListRoutes(filter)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) addRoute(newRoute cfapi.NewRoute) (cfapi.Route, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return cfapi.Route{}, errors.Wrap(err, noClientMsg)
|
||||
}
|
||||
return client.AddRoute(newRoute)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) deleteRoute(params cfapi.DeleteRouteParams) error {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, noClientMsg)
|
||||
}
|
||||
return client.DeleteRoute(params)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) getRouteByIP(params cfapi.GetRouteByIpParams) (cfapi.DetailedRoute, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return cfapi.DetailedRoute{}, errors.Wrap(err, noClientMsg)
|
||||
}
|
||||
return client.GetByIP(params)
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"flag"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
)
|
||||
|
||||
type mockFileSystem struct {
|
||||
rf func(string) ([]byte, error)
|
||||
vfp func(string) bool
|
||||
}
|
||||
|
||||
func (fs mockFileSystem) validFilePath(path string) bool {
|
||||
return fs.vfp(path)
|
||||
}
|
||||
|
||||
func (fs mockFileSystem) readFile(filePath string) ([]byte, error) {
|
||||
return fs.rf(filePath)
|
||||
}
|
||||
|
||||
func Test_subcommandContext_findCredentials(t *testing.T) {
|
||||
type fields struct {
|
||||
c *cli.Context
|
||||
log *zerolog.Logger
|
||||
isUIEnabled bool
|
||||
fs fileSystem
|
||||
tunnelstoreClient cfapi.Client
|
||||
userCredential *userCredential
|
||||
}
|
||||
type args struct {
|
||||
tunnelID uuid.UUID
|
||||
}
|
||||
oldCertPath := "old_cert.json"
|
||||
newCertPath := "new_cert.json"
|
||||
accountTag := "0000d4d14e84bd4ae5a6a02e0000ac63"
|
||||
secret := []byte{211, 79, 177, 245, 179, 194, 152, 127, 140, 71, 18, 46, 183, 209, 10, 24, 192, 150, 55, 249, 211, 16, 167, 30, 113, 51, 152, 168, 72, 100, 205, 144}
|
||||
secretB64 := base64.StdEncoding.EncodeToString(secret)
|
||||
tunnelID := uuid.MustParse("df5ed608-b8b4-4109-89f3-9f2cf199df64")
|
||||
name := "mytunnel"
|
||||
|
||||
fs := mockFileSystem{
|
||||
rf: func(filePath string) ([]byte, error) {
|
||||
if filePath == oldCertPath {
|
||||
// An old credentials file created before TUN-3581 added the new fields
|
||||
return []byte(fmt.Sprintf(`{"AccountTag":"%s","TunnelSecret":"%s"}`, accountTag, secretB64)), nil
|
||||
}
|
||||
if filePath == newCertPath {
|
||||
// A new credentials file created after TUN-3581 with its new fields.
|
||||
return []byte(fmt.Sprintf(`{"AccountTag":"%s","TunnelSecret":"%s","TunnelID":"%s","TunnelName":"%s"}`, accountTag, secretB64, tunnelID, name)), nil
|
||||
}
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
vfp: func(string) bool { return true },
|
||||
}
|
||||
log := zerolog.Nop()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want connection.Credentials
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Filepath given leads to old credentials file",
|
||||
fields: fields{
|
||||
log: &log,
|
||||
fs: fs,
|
||||
c: func() *cli.Context {
|
||||
flagSet := flag.NewFlagSet("test0", flag.PanicOnError)
|
||||
flagSet.String(CredFileFlag, oldCertPath, "")
|
||||
c := cli.NewContext(cli.NewApp(), flagSet, nil)
|
||||
_ = c.Set(CredFileFlag, oldCertPath)
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
args: args{
|
||||
tunnelID: tunnelID,
|
||||
},
|
||||
want: connection.Credentials{
|
||||
AccountTag: accountTag,
|
||||
TunnelID: tunnelID,
|
||||
TunnelSecret: secret,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Filepath given leads to new credentials file",
|
||||
fields: fields{
|
||||
log: &log,
|
||||
fs: fs,
|
||||
c: func() *cli.Context {
|
||||
flagSet := flag.NewFlagSet("test0", flag.PanicOnError)
|
||||
flagSet.String(CredFileFlag, newCertPath, "")
|
||||
c := cli.NewContext(cli.NewApp(), flagSet, nil)
|
||||
_ = c.Set(CredFileFlag, newCertPath)
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
args: args{
|
||||
tunnelID: tunnelID,
|
||||
},
|
||||
want: connection.Credentials{
|
||||
AccountTag: accountTag,
|
||||
TunnelID: tunnelID,
|
||||
TunnelSecret: secret,
|
||||
TunnelName: name,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TUNNEL_CRED_CONTENTS given contains old credentials contents",
|
||||
fields: fields{
|
||||
log: &log,
|
||||
fs: fs,
|
||||
c: func() *cli.Context {
|
||||
flagSet := flag.NewFlagSet("test0", flag.PanicOnError)
|
||||
flagSet.String(CredContentsFlag, "", "")
|
||||
c := cli.NewContext(cli.NewApp(), flagSet, nil)
|
||||
_ = c.Set(CredContentsFlag, fmt.Sprintf(`{"AccountTag":"%s","TunnelSecret":"%s"}`, accountTag, secretB64))
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
args: args{
|
||||
tunnelID: tunnelID,
|
||||
},
|
||||
want: connection.Credentials{
|
||||
AccountTag: accountTag,
|
||||
TunnelID: tunnelID,
|
||||
TunnelSecret: secret,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TUNNEL_CRED_CONTENTS given contains new credentials contents",
|
||||
fields: fields{
|
||||
log: &log,
|
||||
fs: fs,
|
||||
c: func() *cli.Context {
|
||||
flagSet := flag.NewFlagSet("test0", flag.PanicOnError)
|
||||
flagSet.String(CredContentsFlag, "", "")
|
||||
c := cli.NewContext(cli.NewApp(), flagSet, nil)
|
||||
_ = c.Set(CredContentsFlag, fmt.Sprintf(`{"AccountTag":"%s","TunnelSecret":"%s","TunnelID":"%s","TunnelName":"%s"}`, accountTag, secretB64, tunnelID, name))
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
args: args{
|
||||
tunnelID: tunnelID,
|
||||
},
|
||||
want: connection.Credentials{
|
||||
AccountTag: accountTag,
|
||||
TunnelID: tunnelID,
|
||||
TunnelSecret: secret,
|
||||
TunnelName: name,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sc := &subcommandContext{
|
||||
c: tt.fields.c,
|
||||
log: tt.fields.log,
|
||||
isUIEnabled: tt.fields.isUIEnabled,
|
||||
fs: tt.fields.fs,
|
||||
tunnelstoreClient: tt.fields.tunnelstoreClient,
|
||||
userCredential: tt.fields.userCredential,
|
||||
}
|
||||
got, err := sc.findCredentials(tt.args.tunnelID)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("subcommandContext.findCredentials() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("subcommandContext.findCredentials() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type deleteMockTunnelStore struct {
|
||||
cfapi.Client
|
||||
mockTunnels map[uuid.UUID]mockTunnelBehaviour
|
||||
deletedTunnelIDs []uuid.UUID
|
||||
}
|
||||
|
||||
type mockTunnelBehaviour struct {
|
||||
tunnel cfapi.Tunnel
|
||||
deleteErr error
|
||||
cleanupErr error
|
||||
}
|
||||
|
||||
func newDeleteMockTunnelStore(tunnels ...mockTunnelBehaviour) *deleteMockTunnelStore {
|
||||
mockTunnels := make(map[uuid.UUID]mockTunnelBehaviour)
|
||||
for _, tunnel := range tunnels {
|
||||
mockTunnels[tunnel.tunnel.ID] = tunnel
|
||||
}
|
||||
return &deleteMockTunnelStore{
|
||||
mockTunnels: mockTunnels,
|
||||
deletedTunnelIDs: make([]uuid.UUID, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *deleteMockTunnelStore) GetTunnel(tunnelID uuid.UUID) (*cfapi.Tunnel, error) {
|
||||
tunnel, ok := d.mockTunnels[tunnelID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
|
||||
}
|
||||
return &tunnel.tunnel, nil
|
||||
}
|
||||
|
||||
func (d *deleteMockTunnelStore) DeleteTunnel(tunnelID uuid.UUID) error {
|
||||
tunnel, ok := d.mockTunnels[tunnelID]
|
||||
if !ok {
|
||||
return fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
|
||||
}
|
||||
|
||||
if tunnel.deleteErr != nil {
|
||||
return tunnel.deleteErr
|
||||
}
|
||||
|
||||
d.deletedTunnelIDs = append(d.deletedTunnelIDs, tunnelID)
|
||||
tunnel.tunnel.DeletedAt = time.Now()
|
||||
delete(d.mockTunnels, tunnelID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *deleteMockTunnelStore) CleanupConnections(tunnelID uuid.UUID, _ *cfapi.CleanupParams) error {
|
||||
tunnel, ok := d.mockTunnels[tunnelID]
|
||||
if !ok {
|
||||
return fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
|
||||
}
|
||||
return tunnel.cleanupErr
|
||||
}
|
||||
|
||||
func Test_subcommandContext_Delete(t *testing.T) {
|
||||
type fields struct {
|
||||
c *cli.Context
|
||||
log *zerolog.Logger
|
||||
isUIEnabled bool
|
||||
fs fileSystem
|
||||
tunnelstoreClient *deleteMockTunnelStore
|
||||
userCredential *userCredential
|
||||
}
|
||||
type args struct {
|
||||
tunnelIDs []uuid.UUID
|
||||
}
|
||||
newCertPath := "new_cert.json"
|
||||
tunnelID1 := uuid.MustParse("df5ed608-b8b4-4109-89f3-9f2cf199df64")
|
||||
tunnelID2 := uuid.MustParse("af5ed608-b8b4-4109-89f3-9f2cf199df64")
|
||||
log := zerolog.Nop()
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want []uuid.UUID
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "clean up continues if credentials are not found",
|
||||
fields: fields{
|
||||
log: &log,
|
||||
fs: mockFileSystem{
|
||||
rf: func(filePath string) ([]byte, error) {
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
vfp: func(string) bool { return true },
|
||||
},
|
||||
c: func() *cli.Context {
|
||||
flagSet := flag.NewFlagSet("test0", flag.PanicOnError)
|
||||
flagSet.String(CredFileFlag, newCertPath, "")
|
||||
c := cli.NewContext(cli.NewApp(), flagSet, nil)
|
||||
_ = c.Set(CredFileFlag, newCertPath)
|
||||
return c
|
||||
}(),
|
||||
tunnelstoreClient: newDeleteMockTunnelStore(
|
||||
mockTunnelBehaviour{
|
||||
tunnel: cfapi.Tunnel{ID: tunnelID1},
|
||||
},
|
||||
mockTunnelBehaviour{
|
||||
tunnel: cfapi.Tunnel{ID: tunnelID2},
|
||||
},
|
||||
),
|
||||
},
|
||||
|
||||
args: args{
|
||||
tunnelIDs: []uuid.UUID{tunnelID1, tunnelID2},
|
||||
},
|
||||
want: []uuid.UUID{tunnelID1, tunnelID2},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sc := &subcommandContext{
|
||||
c: tt.fields.c,
|
||||
log: tt.fields.log,
|
||||
isUIEnabled: tt.fields.isUIEnabled,
|
||||
fs: tt.fields.fs,
|
||||
tunnelstoreClient: tt.fields.tunnelstoreClient,
|
||||
userCredential: tt.fields.userCredential,
|
||||
}
|
||||
err := sc.delete(tt.args.tunnelIDs)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("subcommandContext.findCredentials() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
got := tt.fields.tunnelstoreClient.deletedTunnelIDs
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("subcommandContext.findCredentials() = %v, want %v", got, tt.want)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
)
|
||||
|
||||
func (sc *subcommandContext) addVirtualNetwork(newVnet cfapi.NewVirtualNetwork) (cfapi.VirtualNetwork, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return cfapi.VirtualNetwork{}, errors.Wrap(err, noClientMsg)
|
||||
}
|
||||
return client.CreateVirtualNetwork(newVnet)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) listVirtualNetworks(filter *cfapi.VnetFilter) ([]*cfapi.VirtualNetwork, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, noClientMsg)
|
||||
}
|
||||
return client.ListVirtualNetworks(filter)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) deleteVirtualNetwork(vnetId uuid.UUID) error {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, noClientMsg)
|
||||
}
|
||||
return client.DeleteVirtualNetwork(vnetId)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) updateVirtualNetwork(vnetId uuid.UUID, updates cfapi.UpdateVirtualNetwork) error {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, noClientMsg)
|
||||
}
|
||||
return client.UpdateVirtualNetwork(vnetId, updates)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,20 +1,19 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
|
||||
"github.com/google/uuid"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
)
|
||||
|
||||
func Test_fmtConnections(t *testing.T) {
|
||||
type args struct {
|
||||
connections []tunnelstore.Connection
|
||||
connections []cfapi.Connection
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -24,14 +23,14 @@ func Test_fmtConnections(t *testing.T) {
|
||||
{
|
||||
name: "empty",
|
||||
args: args{
|
||||
connections: []tunnelstore.Connection{},
|
||||
connections: []cfapi.Connection{},
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "trivial",
|
||||
args: args{
|
||||
connections: []tunnelstore.Connection{
|
||||
connections: []cfapi.Connection{
|
||||
{
|
||||
ColoName: "DFW",
|
||||
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
|
||||
@@ -40,10 +39,23 @@ func Test_fmtConnections(t *testing.T) {
|
||||
},
|
||||
want: "1xDFW",
|
||||
},
|
||||
{
|
||||
name: "with a pending reconnect",
|
||||
args: args{
|
||||
connections: []cfapi.Connection{
|
||||
{
|
||||
ColoName: "DFW",
|
||||
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
|
||||
IsPendingReconnect: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "many colos",
|
||||
args: args{
|
||||
connections: []tunnelstore.Connection{
|
||||
connections: []cfapi.Connection{
|
||||
{
|
||||
ColoName: "YRV",
|
||||
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
|
||||
@@ -67,7 +79,7 @@ func Test_fmtConnections(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := fmtConnections(tt.args.connections); got != tt.want {
|
||||
if got := fmtConnections(tt.args.connections, false); got != tt.want {
|
||||
t.Errorf("fmtConnections() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
@@ -75,11 +87,93 @@ func Test_fmtConnections(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTunnelfilePath(t *testing.T) {
|
||||
tunnelID, err := uuid.Parse("f48d8918-bc23-4647-9d48-082c5b76de65")
|
||||
assert.NoError(t, err)
|
||||
originCertDir := filepath.Dir("~/.cloudflared/cert.pem")
|
||||
actual, err := tunnelFilePath("tunnel", originCertDir)
|
||||
actual, err := tunnelFilePath(tunnelID, originCertDir)
|
||||
assert.NoError(t, err)
|
||||
homeDir, err := homedir.Dir()
|
||||
assert.NoError(t, err)
|
||||
expected := fmt.Sprintf("%s/.cloudflared/tunnel.json", homeDir)
|
||||
expected := filepath.Join(homeDir, ".cloudflared", tunnelID.String()+".json")
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestValidateName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want bool
|
||||
}{
|
||||
{name: "", want: false},
|
||||
{name: "-", want: false},
|
||||
{name: ".", want: false},
|
||||
{name: "a b", want: false},
|
||||
{name: "a+b", want: false},
|
||||
{name: "-ab", want: false},
|
||||
|
||||
{name: "ab", want: true},
|
||||
{name: "ab-c", want: true},
|
||||
{name: "abc.def", want: true},
|
||||
{name: "_ab_c.-d-ef", want: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := validateName(tt.name, false); got != tt.want {
|
||||
t.Errorf("validateName() = %v, want %v", got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_validateHostname(t *testing.T) {
|
||||
type args struct {
|
||||
s string
|
||||
allowWildcardSubdomain bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "Normal",
|
||||
args: args{
|
||||
s: "example.com",
|
||||
allowWildcardSubdomain: true,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "wildcard subdomain for TUN-358",
|
||||
args: args{
|
||||
s: "*.ehrig.io",
|
||||
allowWildcardSubdomain: true,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Misplaced wildcard",
|
||||
args: args{
|
||||
s: "subdomain.*.ehrig.io",
|
||||
allowWildcardSubdomain: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Invalid domain",
|
||||
args: args{
|
||||
s: "..",
|
||||
allowWildcardSubdomain: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Invalid domain",
|
||||
args: args{
|
||||
s: "..",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := validateHostname(tt.args.s, tt.args.allowWildcardSubdomain); got != tt.want {
|
||||
t.Errorf("validateHostname() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||
)
|
||||
|
||||
var (
|
||||
vnetFlag = &cli.StringFlag{
|
||||
Name: "vnet",
|
||||
Aliases: []string{"vn"},
|
||||
Usage: "The ID or name of the virtual network to which the route is associated to.",
|
||||
}
|
||||
)
|
||||
|
||||
func buildRouteIPSubcommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "ip",
|
||||
Usage: "Configure and query Cloudflare WARP routing to private IP networks made available through Cloudflare Tunnels.",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] route COMMAND [arguments...]",
|
||||
Description: `cloudflared can provision routes for any IP space in your corporate network. Users enrolled in
|
||||
your Cloudflare for Teams organization can reach those IPs through the Cloudflare WARP
|
||||
client. You can then configure L7/L4 filtering on https://dash.teams.cloudflare.com to
|
||||
determine who can reach certain routes.
|
||||
By default IP routes all exist within a single virtual network. If you use the same IP
|
||||
space(s) in different physical private networks, all meant to be reachable via IP routes,
|
||||
then you have to manage the ambiguous IP routes by associating them to virtual networks.
|
||||
See "cloudflared tunnel vnet --help" for more information.`,
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "add",
|
||||
Action: cliutil.ConfiguredAction(addRouteCommand),
|
||||
Usage: "Add a new network to the routing table reachable via a Tunnel",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] route ip add [flags] [CIDR] [TUNNEL] [COMMENT?]",
|
||||
Description: `Adds a network IP route space (represented as a CIDR) to your routing table.
|
||||
That network IP space becomes reachable for requests egressing from a user's machine
|
||||
as long as it is using Cloudflare WARP client and is enrolled in the same account
|
||||
that is running the Tunnel chosen here. Further, those requests will be proxied to
|
||||
the specified Tunnel, and reach an IP in the given CIDR, as long as that IP is
|
||||
reachable from cloudflared.
|
||||
If the CIDR exists in more than one private network, to be connected with Cloudflare
|
||||
Tunnels, then you have to manage those IP routes with virtual networks (see
|
||||
"cloudflared tunnel vnet --help)". In those cases, you then have to tell
|
||||
which virtual network's routing table you want to add the route to with:
|
||||
"cloudflared tunnel route ip add --vnet [ID/name] [CIDR] [TUNNEL]".`,
|
||||
Flags: []cli.Flag{vnetFlag},
|
||||
},
|
||||
{
|
||||
Name: "show",
|
||||
Aliases: []string{"list"},
|
||||
Action: cliutil.ConfiguredAction(showRoutesCommand),
|
||||
Usage: "Show the routing table",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] route ip show [flags]",
|
||||
Description: `Shows your organization private routing table. You can use flags to filter the results.`,
|
||||
Flags: showRoutesFlags(),
|
||||
},
|
||||
{
|
||||
Name: "delete",
|
||||
Action: cliutil.ConfiguredAction(deleteRouteCommand),
|
||||
Usage: "Delete a row from your organization's private routing table",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] route ip delete [flags] [CIDR]",
|
||||
Description: `Deletes the row for a given CIDR from your routing table. That portion of your network
|
||||
will no longer be reachable by the WARP clients. Note that if you use virtual
|
||||
networks, then you have to tell which virtual network whose routing table you
|
||||
have a row deleted from.`,
|
||||
Flags: []cli.Flag{vnetFlag},
|
||||
},
|
||||
{
|
||||
Name: "get",
|
||||
Action: cliutil.ConfiguredAction(getRouteByIPCommand),
|
||||
Usage: "Check which row of the routing table matches a given IP.",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] route ip get [flags] [IP]",
|
||||
Description: `Checks which row of the routing table will be used to proxy a given IP. This helps check
|
||||
and validate your config. Note that if you use virtual networks, then you have
|
||||
to tell which virtual network whose routing table you want to use.`,
|
||||
Flags: []cli.Flag{vnetFlag},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func showRoutesFlags() []cli.Flag {
|
||||
flags := make([]cli.Flag, 0)
|
||||
flags = append(flags, cfapi.IpRouteFilterFlags...)
|
||||
flags = append(flags, outputFormatFlag)
|
||||
return flags
|
||||
}
|
||||
|
||||
func showRoutesCommand(c *cli.Context) error {
|
||||
sc, err := newSubcommandContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filter, err := cfapi.NewIpRouteFilterFromCLI(c)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid config for routing filters")
|
||||
}
|
||||
|
||||
warningChecker := updater.StartWarningCheck(c)
|
||||
defer warningChecker.LogWarningIfAny(sc.log)
|
||||
|
||||
routes, err := sc.listRoutes(filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" {
|
||||
return renderOutput(outputFormat, routes)
|
||||
}
|
||||
|
||||
if len(routes) > 0 {
|
||||
formatAndPrintRouteList(routes)
|
||||
} else {
|
||||
fmt.Println("No routes were found for the given filter flags. You can use 'cloudflared tunnel route ip add' to add a route.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addRouteCommand(c *cli.Context) error {
|
||||
sc, err := newSubcommandContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.NArg() < 2 {
|
||||
return errors.New("You must supply at least 2 arguments, first the network you wish to route (in CIDR form e.g. 1.2.3.4/32) and then the tunnel ID to proxy with")
|
||||
}
|
||||
|
||||
args := c.Args()
|
||||
|
||||
_, network, err := net.ParseCIDR(args.Get(0))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Invalid network CIDR")
|
||||
}
|
||||
if network == nil {
|
||||
return errors.New("Invalid network CIDR")
|
||||
}
|
||||
|
||||
tunnelRef := args.Get(1)
|
||||
tunnelID, err := sc.findID(tunnelRef)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Invalid tunnel")
|
||||
}
|
||||
|
||||
comment := ""
|
||||
if c.NArg() >= 3 {
|
||||
comment = args.Get(2)
|
||||
}
|
||||
|
||||
var vnetId *uuid.UUID
|
||||
if c.IsSet(vnetFlag.Name) {
|
||||
id, err := getVnetId(sc, c.String(vnetFlag.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vnetId = &id
|
||||
}
|
||||
|
||||
_, err = sc.addRoute(cfapi.NewRoute{
|
||||
Comment: comment,
|
||||
Network: *network,
|
||||
TunnelID: tunnelID,
|
||||
VNetID: vnetId,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "API error")
|
||||
}
|
||||
fmt.Printf("Successfully added route for %s over tunnel %s\n", network, tunnelID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteRouteCommand(c *cli.Context) error {
|
||||
sc, err := newSubcommandContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.NArg() != 1 {
|
||||
return errors.New("You must supply exactly one argument, the network whose route you want to delete (in CIDR form e.g. 1.2.3.4/32)")
|
||||
}
|
||||
|
||||
_, network, err := net.ParseCIDR(c.Args().First())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Invalid network CIDR")
|
||||
}
|
||||
if network == nil {
|
||||
return errors.New("Invalid network CIDR")
|
||||
}
|
||||
|
||||
params := cfapi.DeleteRouteParams{
|
||||
Network: *network,
|
||||
}
|
||||
|
||||
if c.IsSet(vnetFlag.Name) {
|
||||
vnetId, err := getVnetId(sc, c.String(vnetFlag.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
params.VNetID = &vnetId
|
||||
}
|
||||
|
||||
if err := sc.deleteRoute(params); err != nil {
|
||||
return errors.Wrap(err, "API error")
|
||||
}
|
||||
fmt.Printf("Successfully deleted route for %s\n", network)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRouteByIPCommand(c *cli.Context) error {
|
||||
sc, err := newSubcommandContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.NArg() != 1 {
|
||||
return errors.New("You must supply exactly one argument, an IP whose route will be queried (e.g. 1.2.3.4 or 2001:0db8:::7334)")
|
||||
}
|
||||
|
||||
ipInput := c.Args().First()
|
||||
ip := net.ParseIP(ipInput)
|
||||
if ip == nil {
|
||||
return fmt.Errorf("Invalid IP %s", ipInput)
|
||||
}
|
||||
|
||||
params := cfapi.GetRouteByIpParams{
|
||||
Ip: ip,
|
||||
}
|
||||
|
||||
if c.IsSet(vnetFlag.Name) {
|
||||
vnetId, err := getVnetId(sc, c.String(vnetFlag.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
params.VNetID = &vnetId
|
||||
}
|
||||
|
||||
route, err := sc.getRouteByIP(params)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "API error")
|
||||
}
|
||||
if route.IsZero() {
|
||||
fmt.Printf("No route matches the IP %s\n", ip)
|
||||
} else {
|
||||
formatAndPrintRouteList([]*cfapi.DetailedRoute{&route})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatAndPrintRouteList(routes []*cfapi.DetailedRoute) {
|
||||
const (
|
||||
minWidth = 0
|
||||
tabWidth = 8
|
||||
padding = 1
|
||||
padChar = ' '
|
||||
flags = 0
|
||||
)
|
||||
|
||||
writer := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, padChar, flags)
|
||||
defer writer.Flush()
|
||||
|
||||
// Print column headers with tabbed columns
|
||||
_, _ = fmt.Fprintln(writer, "NETWORK\tVIRTUAL NET ID\tCOMMENT\tTUNNEL ID\tTUNNEL NAME\tCREATED\tDELETED\t")
|
||||
|
||||
// Loop through routes, create formatted string for each, and print using tabwriter
|
||||
for _, route := range routes {
|
||||
formattedStr := route.TableString()
|
||||
_, _ = fmt.Fprintln(writer, formattedStr)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||
)
|
||||
|
||||
var (
|
||||
makeDefaultFlag = &cli.BoolFlag{
|
||||
Name: "default",
|
||||
Aliases: []string{"d"},
|
||||
Usage: "The virtual network becomes the default one for the account. This means that all operations that " +
|
||||
"omit a virtual network will now implicitly be using this virtual network (i.e., the default one) such " +
|
||||
"as new IP routes that are created. When this flag is not set, the virtual network will not become the " +
|
||||
"default one in the account.",
|
||||
}
|
||||
newNameFlag = &cli.StringFlag{
|
||||
Name: "name",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "The new name for the virtual network.",
|
||||
}
|
||||
newCommentFlag = &cli.StringFlag{
|
||||
Name: "comment",
|
||||
Aliases: []string{"c"},
|
||||
Usage: "A new comment describing the purpose of the virtual network.",
|
||||
}
|
||||
)
|
||||
|
||||
func buildVirtualNetworkSubcommand(hidden bool) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "vnet",
|
||||
Usage: "Configure and query virtual networks to manage private IP routes with overlapping IPs.",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] network COMMAND [arguments...]",
|
||||
Description: `cloudflared allows to manage IP routes that expose origins in your private network space via their IP directly
|
||||
to clients outside (e.g. using WARP client) --- those are configurable via "cloudflared tunnel route ip" commands.
|
||||
By default, all those IP routes live in the same virtual network. Managing virtual networks (e.g. by creating a
|
||||
new one) becomes relevant when you have different private networks that have overlapping IPs. E.g.: if you have
|
||||
a private network A running Tunnel 1, and private network B running Tunnel 2, it is possible that both Tunnels
|
||||
expose the same IP space (say 10.0.0.0/8); to handle that, you have to add each IP Route (one that points to
|
||||
Tunnel 1 and another that points to Tunnel 2) in different Virtual Networks. That way, if your clients are on
|
||||
Virtual Network X, they will see Tunnel 1 (via Route A) and not see Tunnel 2 (since its Route B is associated
|
||||
to another Virtual Network Y).`,
|
||||
Hidden: hidden,
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "add",
|
||||
Action: cliutil.ConfiguredAction(addVirtualNetworkCommand),
|
||||
Usage: "Add a new virtual network to which IP routes can be attached",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] network add [flags] NAME [\"comment\"]",
|
||||
Description: `Adds a new virtual network. You can then attach IP routes to this virtual network with "cloudflared tunnel route ip"
|
||||
commands. By doing so, such route(s) become segregated from route(s) in another virtual networks. Note that all
|
||||
routes exist within some virtual network. If you do not specify any, then the system pre-creates a default virtual
|
||||
network to which all routes belong. That is fine if you do not have overlapping IPs within different physical
|
||||
private networks in your infrastructure exposed via Cloudflare Tunnel. Note: if a virtual network is added as
|
||||
the new default, then the previous existing default virtual network will be automatically modified to no longer
|
||||
be the current default.`,
|
||||
Flags: []cli.Flag{makeDefaultFlag},
|
||||
Hidden: hidden,
|
||||
},
|
||||
{
|
||||
Name: "list",
|
||||
Action: cliutil.ConfiguredAction(listVirtualNetworksCommand),
|
||||
Usage: "Lists the virtual networks",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] network list [flags]",
|
||||
Description: "Lists the virtual networks based on the given filter flags.",
|
||||
Flags: listVirtualNetworksFlags(),
|
||||
Hidden: hidden,
|
||||
},
|
||||
{
|
||||
Name: "delete",
|
||||
Action: cliutil.ConfiguredAction(deleteVirtualNetworkCommand),
|
||||
Usage: "Delete a virtual network",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] network delete VIRTUAL_NETWORK",
|
||||
Description: `Deletes the virtual network (given its ID or name). This is only possible if that virtual network is unused.
|
||||
A virtual network may be used by IP routes or by WARP devices.`,
|
||||
Hidden: hidden,
|
||||
},
|
||||
{
|
||||
Name: "update",
|
||||
Action: cliutil.ConfiguredAction(updateVirtualNetworkCommand),
|
||||
Usage: "Update a virtual network",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] network update [flags] VIRTUAL_NETWORK",
|
||||
Description: `Updates the virtual network (given its ID or name). If this virtual network is updated to become the new
|
||||
default, then the previously existing default virtual network will also be modified to no longer be the default.
|
||||
You cannot update a virtual network to not be the default anymore directly. Instead, you should create a new
|
||||
default or update an existing one to become the default.`,
|
||||
Flags: []cli.Flag{newNameFlag, newCommentFlag, makeDefaultFlag},
|
||||
Hidden: hidden,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func listVirtualNetworksFlags() []cli.Flag {
|
||||
flags := make([]cli.Flag, 0)
|
||||
flags = append(flags, cfapi.VnetFilterFlags...)
|
||||
flags = append(flags, outputFormatFlag)
|
||||
return flags
|
||||
}
|
||||
|
||||
func addVirtualNetworkCommand(c *cli.Context) error {
|
||||
sc, err := newSubcommandContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.NArg() < 1 {
|
||||
return errors.New("You must supply at least 1 argument, the name of the virtual network you wish to add.")
|
||||
}
|
||||
|
||||
warningChecker := updater.StartWarningCheck(c)
|
||||
defer warningChecker.LogWarningIfAny(sc.log)
|
||||
|
||||
args := c.Args()
|
||||
|
||||
name := args.Get(0)
|
||||
|
||||
comment := ""
|
||||
if c.NArg() >= 2 {
|
||||
comment = args.Get(1)
|
||||
}
|
||||
|
||||
newVnet := cfapi.NewVirtualNetwork{
|
||||
Name: name,
|
||||
Comment: comment,
|
||||
IsDefault: c.Bool(makeDefaultFlag.Name),
|
||||
}
|
||||
createdVnet, err := sc.addVirtualNetwork(newVnet)
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Could not add virtual network")
|
||||
}
|
||||
|
||||
extraMsg := ""
|
||||
if createdVnet.IsDefault {
|
||||
extraMsg = " (as the new default for this account) "
|
||||
}
|
||||
fmt.Printf(
|
||||
"Successfully added virtual 'network' %s with ID: %s%s\n"+
|
||||
"You can now add IP routes attached to this virtual network. See `cloudflared tunnel route ip add -help`\n",
|
||||
name, createdVnet.ID, extraMsg,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func listVirtualNetworksCommand(c *cli.Context) error {
|
||||
sc, err := newSubcommandContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
warningChecker := updater.StartWarningCheck(c)
|
||||
defer warningChecker.LogWarningIfAny(sc.log)
|
||||
|
||||
filter, err := cfapi.NewFromCLI(c)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid flags for filtering virtual networks")
|
||||
}
|
||||
|
||||
vnets, err := sc.listVirtualNetworks(filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" {
|
||||
return renderOutput(outputFormat, vnets)
|
||||
}
|
||||
|
||||
if len(vnets) > 0 {
|
||||
formatAndPrintVnetsList(vnets)
|
||||
} else {
|
||||
fmt.Println("No virtual networks were found for the given filter flags. You can use 'cloudflared tunnel vnet add' to add a virtual network.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteVirtualNetworkCommand(c *cli.Context) error {
|
||||
sc, err := newSubcommandContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.NArg() != 1 {
|
||||
return errors.New("You must supply exactly one argument, either the ID or name of the virtual network to delete")
|
||||
}
|
||||
|
||||
input := c.Args().Get(0)
|
||||
vnetId, err := getVnetId(sc, input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := sc.deleteVirtualNetwork(vnetId); err != nil {
|
||||
return errors.Wrap(err, "API error")
|
||||
}
|
||||
fmt.Printf("Successfully deleted virtual network '%s'\n", input)
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateVirtualNetworkCommand(c *cli.Context) error {
|
||||
sc, err := newSubcommandContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.NArg() != 1 {
|
||||
return errors.New(" You must supply exactly one argument, either the ID or (current) name of the virtual network to update")
|
||||
}
|
||||
|
||||
input := c.Args().Get(0)
|
||||
vnetId, err := getVnetId(sc, input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updates := cfapi.UpdateVirtualNetwork{}
|
||||
|
||||
if c.IsSet(newNameFlag.Name) {
|
||||
newName := c.String(newNameFlag.Name)
|
||||
updates.Name = &newName
|
||||
}
|
||||
if c.IsSet(newCommentFlag.Name) {
|
||||
newComment := c.String(newCommentFlag.Name)
|
||||
updates.Comment = &newComment
|
||||
}
|
||||
if c.IsSet(makeDefaultFlag.Name) {
|
||||
isDefault := c.Bool(makeDefaultFlag.Name)
|
||||
updates.IsDefault = &isDefault
|
||||
}
|
||||
|
||||
if err := sc.updateVirtualNetwork(vnetId, updates); err != nil {
|
||||
return errors.Wrap(err, "API error")
|
||||
}
|
||||
fmt.Printf("Successfully updated virtual network '%s'\n", input)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getVnetId(sc *subcommandContext, input string) (uuid.UUID, error) {
|
||||
val, err := uuid.Parse(input)
|
||||
if err == nil {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
filter := cfapi.NewVnetFilter()
|
||||
filter.WithDeleted(false)
|
||||
filter.ByName(input)
|
||||
|
||||
vnets, err := sc.listVirtualNetworks(filter)
|
||||
if err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
|
||||
if len(vnets) != 1 {
|
||||
return uuid.Nil, fmt.Errorf("there should only be 1 non-deleted virtual network named %s", input)
|
||||
}
|
||||
|
||||
return vnets[0].ID, nil
|
||||
}
|
||||
|
||||
func formatAndPrintVnetsList(vnets []*cfapi.VirtualNetwork) {
|
||||
const (
|
||||
minWidth = 0
|
||||
tabWidth = 8
|
||||
padding = 1
|
||||
padChar = ' '
|
||||
flags = 0
|
||||
)
|
||||
|
||||
writer := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, padChar, flags)
|
||||
defer writer.Flush()
|
||||
|
||||
_, _ = fmt.Fprintln(writer, "ID\tNAME\tIS DEFAULT\tCOMMENT\tCREATED\tDELETED\t")
|
||||
|
||||
for _, virtualNetwork := range vnets {
|
||||
formattedStr := virtualNetwork.TableString()
|
||||
_, _ = fmt.Fprintln(writer, formattedStr)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
)
|
||||
|
||||
type connState struct {
|
||||
location string
|
||||
}
|
||||
|
||||
type uiModel struct {
|
||||
version string
|
||||
edgeURL string
|
||||
metricsURL string
|
||||
localServices []string
|
||||
connections []connState
|
||||
}
|
||||
|
||||
type palette struct {
|
||||
url string
|
||||
connected string
|
||||
defaultText string
|
||||
disconnected string
|
||||
reconnecting string
|
||||
unregistered string
|
||||
}
|
||||
|
||||
func NewUIModel(version, hostname, metricsURL string, ing *ingress.Ingress, haConnections int) *uiModel {
|
||||
localServices := make([]string, len(ing.Rules))
|
||||
for i, rule := range ing.Rules {
|
||||
localServices[i] = rule.Service.String()
|
||||
}
|
||||
return &uiModel{
|
||||
version: version,
|
||||
edgeURL: hostname,
|
||||
metricsURL: metricsURL,
|
||||
localServices: localServices,
|
||||
connections: make([]connState, haConnections),
|
||||
}
|
||||
}
|
||||
|
||||
func (data *uiModel) Launch(
|
||||
ctx context.Context,
|
||||
log, transportLog *zerolog.Logger,
|
||||
) connection.EventSink {
|
||||
// Configure the logger to stream logs into the textview
|
||||
|
||||
// Add TextView as a group to write output to
|
||||
logTextView := NewDynamicColorTextView()
|
||||
// TODO: Format log for UI
|
||||
//log.Add(logTextView, logger.NewUIFormatter(time.RFC3339), logLevels...)
|
||||
//transportLog.Add(logTextView, logger.NewUIFormatter(time.RFC3339), logLevels...)
|
||||
|
||||
// Construct the UI
|
||||
palette := palette{
|
||||
url: "lightblue",
|
||||
connected: "lime",
|
||||
defaultText: "white",
|
||||
disconnected: "red",
|
||||
reconnecting: "orange",
|
||||
unregistered: "orange",
|
||||
}
|
||||
|
||||
app := tview.NewApplication()
|
||||
|
||||
grid := tview.NewGrid().SetGap(1, 0)
|
||||
frame := tview.NewFrame(grid)
|
||||
header := fmt.Sprintf("cloudflared [::b]%s", data.version)
|
||||
|
||||
frame.AddText(header, true, tview.AlignLeft, tcell.ColorWhite)
|
||||
|
||||
// Create table to store connection info and status
|
||||
connTable := tview.NewTable()
|
||||
// SetColumns takes a value for each column, representing the size of the column
|
||||
// Numbers <= 0 represent proportional widths and positive numbers represent absolute widths
|
||||
grid.SetColumns(20, 0)
|
||||
|
||||
// SetRows takes a value for each row, representing the size of the row
|
||||
grid.SetRows(1, 1, len(data.connections), 1, 0)
|
||||
|
||||
// AddItem takes a primitive tview type, row, column, rowSpan, columnSpan, minGridHeight, minGridWidth, and focus
|
||||
grid.AddItem(tview.NewTextView().SetText("Tunnel:"), 0, 0, 1, 1, 0, 0, false)
|
||||
grid.AddItem(tview.NewTextView().SetText("Status:"), 1, 0, 1, 1, 0, 0, false)
|
||||
grid.AddItem(tview.NewTextView().SetText("Connections:"), 2, 0, 1, 1, 0, 0, false)
|
||||
|
||||
grid.AddItem(tview.NewTextView().SetText("Metrics:"), 3, 0, 1, 1, 0, 0, false)
|
||||
|
||||
tunnelHostText := tview.NewTextView().SetText(data.edgeURL)
|
||||
|
||||
grid.AddItem(tunnelHostText, 0, 1, 1, 1, 0, 0, false)
|
||||
status := fmt.Sprintf("[%s]\u2022[%s] Proxying to [%s::b]%s", palette.connected, palette.defaultText, palette.url, strings.Join(data.localServices, ", "))
|
||||
grid.AddItem(NewDynamicColorTextView().SetText(status), 1, 1, 1, 1, 0, 0, false)
|
||||
|
||||
grid.AddItem(connTable, 2, 1, 1, 1, 0, 0, false)
|
||||
|
||||
grid.AddItem(NewDynamicColorTextView().SetText(fmt.Sprintf("Metrics at [%s::b]http://%s/metrics", palette.url, data.metricsURL)), 3, 1, 1, 1, 0, 0, false)
|
||||
|
||||
// Add TextView to stream logs
|
||||
// Logs are displayed in a new grid so a border can be set around them
|
||||
logGrid := tview.NewGrid().SetBorders(true).AddItem(logTextView.SetChangedFunc(handleNewText(app, logTextView)), 0, 0, 5, 2, 0, 0, false)
|
||||
// LogFrame holds the Logs header as well as the grid with the textView for streamed logs
|
||||
logFrame := tview.NewFrame(logGrid).AddText("[::b]Logs:[::-]", true, tview.AlignLeft, tcell.ColorWhite).SetBorders(0, 0, 0, 0, 0, 0)
|
||||
// Footer for log frame
|
||||
logFrame.AddText("[::d]Use Ctrl+C to exit[::-]", false, tview.AlignRight, tcell.ColorWhite)
|
||||
grid.AddItem(logFrame, 4, 0, 5, 2, 0, 0, false)
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
app.Stop()
|
||||
return
|
||||
}()
|
||||
|
||||
go func() {
|
||||
if err := app.SetRoot(frame, true).Run(); err != nil {
|
||||
log.Error().Msgf("Error launching UI: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return connection.EventSinkFunc(func(event connection.Event) {
|
||||
switch event.EventType {
|
||||
case connection.Connected:
|
||||
data.setConnTableCell(event, connTable, palette)
|
||||
case connection.Disconnected, connection.Reconnecting, connection.Unregistering:
|
||||
data.changeConnStatus(event, connTable, log, palette)
|
||||
case connection.SetURL:
|
||||
tunnelHostText.SetText(event.URL)
|
||||
data.edgeURL = event.URL
|
||||
case connection.RegisteringTunnel:
|
||||
if data.edgeURL == "" {
|
||||
tunnelHostText.SetText(fmt.Sprintf("Registering tunnel connection %d...", event.Index))
|
||||
}
|
||||
}
|
||||
app.Draw()
|
||||
})
|
||||
}
|
||||
|
||||
func NewDynamicColorTextView() *tview.TextView {
|
||||
return tview.NewTextView().SetDynamicColors(true)
|
||||
}
|
||||
|
||||
// Re-draws application when new logs are streamed to UI
|
||||
func handleNewText(app *tview.Application, logTextView *tview.TextView) func() {
|
||||
return func() {
|
||||
app.Draw()
|
||||
// SetFocus to enable scrolling in textview
|
||||
app.SetFocus(logTextView)
|
||||
}
|
||||
}
|
||||
|
||||
func (data *uiModel) changeConnStatus(event connection.Event, table *tview.Table, log *zerolog.Logger, palette palette) {
|
||||
index := int(event.Index)
|
||||
// Get connection location and state
|
||||
connState := data.getConnState(index)
|
||||
// Check if connection is already displayed in UI
|
||||
if connState == nil {
|
||||
log.Info().Msg("Connection is not in the UI table")
|
||||
return
|
||||
}
|
||||
|
||||
locationState := event.Location
|
||||
|
||||
if event.EventType == connection.Reconnecting {
|
||||
locationState = "Reconnecting..."
|
||||
}
|
||||
|
||||
connectionNum := index + 1
|
||||
// Get table cell
|
||||
cell := table.GetCell(index, 0)
|
||||
// Change dot color in front of text as well as location state
|
||||
text := newCellText(palette, connectionNum, locationState, event.EventType)
|
||||
cell.SetText(text)
|
||||
}
|
||||
|
||||
// Return connection location and row in UI table
|
||||
func (data *uiModel) getConnState(connID int) *connState {
|
||||
if connID < len(data.connections) {
|
||||
return &data.connections[connID]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (data *uiModel) setConnTableCell(event connection.Event, table *tview.Table, palette palette) {
|
||||
index := int(event.Index)
|
||||
connectionNum := index + 1
|
||||
|
||||
// Update slice to keep track of connection location and state in UI table
|
||||
data.connections[index].location = event.Location
|
||||
|
||||
// Update text in table cell to show disconnected state
|
||||
text := newCellText(palette, connectionNum, event.Location, event.EventType)
|
||||
cell := tview.NewTableCell(text)
|
||||
table.SetCell(index, 0, cell)
|
||||
}
|
||||
|
||||
func newCellText(palette palette, connectionNum int, location string, connectedStatus connection.Status) string {
|
||||
// HA connection indicator formatted as: "• #<CONNECTION_INDEX>: <COLO>",
|
||||
// where the left middle dot's color depends on the status of the connection
|
||||
const connFmtString = "[%s]\u2022[%s] #%d: %s"
|
||||
|
||||
var dotColor string
|
||||
switch connectedStatus {
|
||||
case connection.Connected:
|
||||
dotColor = palette.connected
|
||||
case connection.Disconnected:
|
||||
dotColor = palette.disconnected
|
||||
case connection.Reconnecting:
|
||||
dotColor = palette.reconnecting
|
||||
case connection.Unregistering:
|
||||
dotColor = palette.unregistered
|
||||
}
|
||||
|
||||
return fmt.Sprintf(connFmtString, dotColor, palette.defaultText, connectionNum, location)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
type VersionWarningChecker struct {
|
||||
warningChan chan string
|
||||
}
|
||||
|
||||
func StartWarningCheck(c *cli.Context) VersionWarningChecker {
|
||||
checker := VersionWarningChecker{
|
||||
warningChan: make(chan string),
|
||||
}
|
||||
|
||||
go func() {
|
||||
options := updateOptions{
|
||||
updateDisabled: true,
|
||||
isBeta: c.Bool("beta"),
|
||||
isStaging: c.Bool("staging"),
|
||||
isForced: false,
|
||||
intendedVersion: "",
|
||||
}
|
||||
checkResult, err := CheckForUpdate(options)
|
||||
if err == nil {
|
||||
checker.warningChan <- checkResult.UserMessage()
|
||||
}
|
||||
close(checker.warningChan)
|
||||
}()
|
||||
|
||||
return checker
|
||||
}
|
||||
|
||||
func (checker VersionWarningChecker) getWarning() string {
|
||||
select {
|
||||
case message := <-checker.warningChan:
|
||||
return message
|
||||
default:
|
||||
// No feedback on time, we don't wait for it, since this is best-effort.
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (checker VersionWarningChecker) LogWarningIfAny(log *zerolog.Logger) {
|
||||
if warning := checker.getWarning(); warning != "" {
|
||||
log.Warn().Msg(warning)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package updater
|
||||
|
||||
// CheckResult is the behaviour resulting from checking in with the Update Service
|
||||
type CheckResult interface {
|
||||
Apply() error
|
||||
Version() string
|
||||
UserMessage() string
|
||||
}
|
||||
|
||||
// Service is the functions to get check for new updates
|
||||
type Service interface {
|
||||
Check() (CheckResult, error)
|
||||
}
|
||||
|
||||
const (
|
||||
// OSKeyName is the url parameter key to send to the checkin API for the operating system of the local cloudflared (e.g. windows, darwin, linux)
|
||||
OSKeyName = "os"
|
||||
|
||||
// ArchitectureKeyName is the url parameter key to send to the checkin API for the architecture of the local cloudflared (e.g. amd64, x86)
|
||||
ArchitectureKeyName = "arch"
|
||||
|
||||
// BetaKeyName is the url parameter key to send to the checkin API to signal if the update should be a beta version or not
|
||||
BetaKeyName = "beta"
|
||||
|
||||
// VersionKeyName is the url parameter key to send to the checkin API to specific what version to upgrade or downgrade to
|
||||
VersionKeyName = "version"
|
||||
|
||||
// ClientVersionName is the url parameter key to send the version that this cloudflared is currently running with
|
||||
ClientVersionName = "clientVersion"
|
||||
)
|
||||
@@ -8,37 +8,33 @@ import (
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/equinox-io/equinox"
|
||||
"github.com/facebookgo/grace/gracenet"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
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/"
|
||||
noUpdateInShellMessage = "cloudflared will not automatically update when run from the shell. To enable auto-updates, run cloudflared as a service: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/run-as-service"
|
||||
noUpdateOnWindowsMessage = "cloudflared will not automatically update on Windows systems."
|
||||
noUpdateManagedPackageMessage = "cloudflared will not automatically update if installed by a package manager."
|
||||
isManagedInstallFile = ".installedFromPackageManager"
|
||||
UpdateURL = "https://update.argotunnel.com"
|
||||
StagingUpdateURL = "https://staging-update.argotunnel.com"
|
||||
|
||||
LogFieldVersion = "version"
|
||||
)
|
||||
|
||||
var (
|
||||
publicKey = []byte(`
|
||||
-----BEGIN ECDSA PUBLIC KEY-----
|
||||
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4OWZocTVZ8Do/L6ScLdkV+9A0IYMHoOf
|
||||
dsCmJ/QZ6aw0w9qkkwEpne1Lmo6+0pGexZzFZOH6w5amShn+RXt7qkSid9iWlzGq
|
||||
EKx0BZogHSor9Wy5VztdFaAaVbsJiCbO
|
||||
-----END ECDSA PUBLIC KEY-----
|
||||
`)
|
||||
version string
|
||||
)
|
||||
|
||||
// BinaryUpdated implements ExitCoder interface, the app will exit with status code 11
|
||||
// https://pkg.go.dev/gopkg.in/urfave/cli.v2?tab=doc#ExitCoder
|
||||
// https://pkg.go.dev/github.com/urfave/cli/v2?tab=doc#ExitCoder
|
||||
type statusSuccess struct {
|
||||
newVersion string
|
||||
}
|
||||
@@ -64,56 +60,96 @@ func (e *statusErr) ExitCode() int {
|
||||
return 10
|
||||
}
|
||||
|
||||
type updateOptions struct {
|
||||
updateDisabled bool
|
||||
isBeta bool
|
||||
isStaging bool
|
||||
isForced bool
|
||||
intendedVersion string
|
||||
}
|
||||
|
||||
type UpdateOutcome struct {
|
||||
Updated bool
|
||||
Version string
|
||||
Error error
|
||||
Updated bool
|
||||
Version string
|
||||
UserMessage 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 Init(v string) {
|
||||
version = v
|
||||
}
|
||||
|
||||
func Update(_ *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
func CheckForUpdate(options updateOptions) (CheckResult, error) {
|
||||
cfdPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
url := UpdateURL
|
||||
if options.isStaging {
|
||||
url = StagingUpdateURL
|
||||
}
|
||||
|
||||
s := NewWorkersService(version, url, cfdPath, Options{IsBeta: options.isBeta,
|
||||
IsForced: options.isForced, RequestedVersion: options.intendedVersion})
|
||||
|
||||
return s.Check()
|
||||
}
|
||||
|
||||
func applyUpdate(options updateOptions, update CheckResult) UpdateOutcome {
|
||||
if update.Version() == "" || options.updateDisabled {
|
||||
return UpdateOutcome{UserMessage: update.UserMessage()}
|
||||
}
|
||||
|
||||
err := update.Apply()
|
||||
if err != nil {
|
||||
return UpdateOutcome{Error: err}
|
||||
}
|
||||
|
||||
return UpdateOutcome{Updated: true, Version: update.Version(), UserMessage: update.UserMessage()}
|
||||
}
|
||||
|
||||
// Update is the handler for the update command from the command line
|
||||
func Update(c *cli.Context) error {
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
if wasInstalledFromPackageManager() {
|
||||
logger.Error("cloudflared was installed by a package manager. Please update using the same method.")
|
||||
log.Error().Msg("cloudflared was installed by a package manager. Please update using the same method.")
|
||||
return nil
|
||||
}
|
||||
|
||||
updateOutcome := loggedUpdate(logger)
|
||||
isBeta := c.Bool("beta")
|
||||
if isBeta {
|
||||
log.Info().Msg("cloudflared is set to update to the latest beta version")
|
||||
}
|
||||
|
||||
isStaging := c.Bool("staging")
|
||||
if isStaging {
|
||||
log.Info().Msg("cloudflared is set to update from staging")
|
||||
}
|
||||
|
||||
isForced := c.Bool("force")
|
||||
if isForced {
|
||||
log.Info().Msg("cloudflared is set to upgrade to the latest publish version regardless of the current version")
|
||||
}
|
||||
|
||||
updateOutcome := loggedUpdate(log, updateOptions{
|
||||
updateDisabled: false,
|
||||
isBeta: isBeta,
|
||||
isStaging: isStaging,
|
||||
isForced: isForced,
|
||||
intendedVersion: c.String("version"),
|
||||
})
|
||||
if updateOutcome.Error != nil {
|
||||
return &statusErr{updateOutcome.Error}
|
||||
}
|
||||
|
||||
if updateOutcome.noUpdate() {
|
||||
logger.Infof("cloudflared is up to date (%s)", updateOutcome.Version)
|
||||
log.Info().Str(LogFieldVersion, updateOutcome.Version).Msg("cloudflared is up to date")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -121,13 +157,19 @@ func Update(_ *cli.Context) error {
|
||||
}
|
||||
|
||||
// Checks for an update and applies it if one is available
|
||||
func loggedUpdate(logger logger.Service) UpdateOutcome {
|
||||
updateOutcome := checkForUpdateAndApply()
|
||||
func loggedUpdate(log *zerolog.Logger, options updateOptions) UpdateOutcome {
|
||||
checkResult, err := CheckForUpdate(options)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("update check failed")
|
||||
return UpdateOutcome{Error: err}
|
||||
}
|
||||
|
||||
updateOutcome := applyUpdate(options, checkResult)
|
||||
if updateOutcome.Updated {
|
||||
logger.Infof("cloudflared has been updated to version %s", updateOutcome.Version)
|
||||
log.Info().Str(LogFieldVersion, updateOutcome.Version).Msg("cloudflared has been updated")
|
||||
}
|
||||
if updateOutcome.Error != nil {
|
||||
logger.Errorf("update check failed: %s", updateOutcome.Error)
|
||||
log.Err(updateOutcome.Error).Msg("update failed to apply")
|
||||
}
|
||||
|
||||
return updateOutcome
|
||||
@@ -138,7 +180,7 @@ type AutoUpdater struct {
|
||||
configurable *configurable
|
||||
listeners *gracenet.Net
|
||||
updateConfigChan chan *configurable
|
||||
logger logger.Service
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
// AutoUpdaterConfigurable is the attributes of AutoUpdater that can be reconfigured during runtime
|
||||
@@ -147,45 +189,53 @@ type configurable struct {
|
||||
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
|
||||
}
|
||||
func NewAutoUpdater(updateDisabled bool, freq time.Duration, listeners *gracenet.Net, log *zerolog.Logger) *AutoUpdater {
|
||||
return &AutoUpdater{
|
||||
configurable: updaterConfigurable,
|
||||
configurable: createUpdateConfig(updateDisabled, freq, log),
|
||||
listeners: listeners,
|
||||
updateConfigChan: make(chan *configurable),
|
||||
logger: logger,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func createUpdateConfig(updateDisabled bool, freq time.Duration, log *zerolog.Logger) *configurable {
|
||||
if isAutoupdateEnabled(log, updateDisabled, freq) {
|
||||
log.Info().Dur("autoupdateFreq", freq).Msg("Autoupdate frequency is set")
|
||||
return &configurable{
|
||||
enabled: true,
|
||||
freq: freq,
|
||||
}
|
||||
} else {
|
||||
return &configurable{
|
||||
enabled: false,
|
||||
freq: DefaultCheckUpdateFreq,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
updateOutcome := loggedUpdate(a.log, updateOptions{updateDisabled: !a.configurable.enabled})
|
||||
if updateOutcome.Updated {
|
||||
Init(updateOutcome.Version)
|
||||
if IsSysV() {
|
||||
// SysV doesn't have a mechanism to keep service alive, we have to restart the process
|
||||
a.log.Info().Msg("Restarting service managed by SysV...")
|
||||
pid, err := a.listeners.StartProcess()
|
||||
if err != nil {
|
||||
a.log.Err(err).Msg("Unable to restart server automatically")
|
||||
return &statusErr{err: err}
|
||||
}
|
||||
return &statusSuccess{newVersion: updateOutcome.Version}
|
||||
// stop old process after autoupdate. Otherwise we create a new process
|
||||
// after each update
|
||||
a.log.Info().Msgf("PID of the new process is %d", pid)
|
||||
}
|
||||
return &statusSuccess{newVersion: updateOutcome.Version}
|
||||
} else if updateOutcome.UserMessage != "" {
|
||||
a.log.Warn().Msg(updateOutcome.UserMessage)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
@@ -200,39 +250,30 @@ func (a *AutoUpdater) Run(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// 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 (a *AutoUpdater) Update(updateDisabled bool, newFreq time.Duration) {
|
||||
a.updateConfigChan <- createUpdateConfig(updateDisabled, newFreq, a.log)
|
||||
}
|
||||
|
||||
func IsAutoupdateEnabled(c *cli.Context, l logger.Service) bool {
|
||||
if !SupportAutoUpdate(l) {
|
||||
func isAutoupdateEnabled(log *zerolog.Logger, updateDisabled bool, updateFreq time.Duration) bool {
|
||||
if !supportAutoUpdate(log) {
|
||||
return false
|
||||
}
|
||||
return !c.Bool("no-autoupdate") && c.Duration("autoupdate-freq") != 0
|
||||
return !updateDisabled && updateFreq != 0
|
||||
}
|
||||
|
||||
func SupportAutoUpdate(logger logger.Service) bool {
|
||||
func supportAutoUpdate(log *zerolog.Logger) bool {
|
||||
if runtime.GOOS == "windows" {
|
||||
logger.Info(noUpdateOnWindowsMessage)
|
||||
log.Info().Msg(noUpdateOnWindowsMessage)
|
||||
return false
|
||||
}
|
||||
|
||||
if wasInstalledFromPackageManager() {
|
||||
logger.Info(noUpdateManagedPackageMessage)
|
||||
log.Info().Msg(noUpdateManagedPackageMessage)
|
||||
return false
|
||||
}
|
||||
|
||||
if isRunningFromTerminal() {
|
||||
logger.Info(noUpdateInShellMessage)
|
||||
log.Info().Msg(noUpdateInShellMessage)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -2,17 +2,19 @@ package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/facebookgo/grace/gracenet"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func TestDisabledAutoUpdater(t *testing.T) {
|
||||
listeners := &gracenet.Net{}
|
||||
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
|
||||
autoupdater := NewAutoUpdater(0, listeners, logger)
|
||||
log := zerolog.Nop()
|
||||
autoupdater := NewAutoUpdater(false, 0, listeners, &log)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
errC := make(chan error)
|
||||
go func() {
|
||||
@@ -26,3 +28,13 @@ func TestDisabledAutoUpdater(t *testing.T) {
|
||||
// Make sure that autoupdater terminates after canceling the context
|
||||
assert.Equal(t, context.Canceled, <-errC)
|
||||
}
|
||||
|
||||
func TestCheckInWithUpdater(t *testing.T) {
|
||||
flagSet := flag.NewFlagSet(t.Name(), flag.PanicOnError)
|
||||
cliCtx := cli.NewContext(cli.NewApp(), flagSet, nil)
|
||||
|
||||
warningChecker := StartWarningCheck(cliCtx)
|
||||
warning := warningChecker.getWarning()
|
||||
// Assuming this runs either on a release or development version, then the Worker will never have anything to tell us.
|
||||
assert.Empty(t, warning)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// Options are the update options supported by the
|
||||
type Options struct {
|
||||
// IsBeta is for beta updates to be installed if available
|
||||
IsBeta bool
|
||||
|
||||
// IsForced is to forcibly download the latest version regardless of the current version
|
||||
IsForced bool
|
||||
|
||||
// RequestedVersion is the specific version to upgrade or downgrade to
|
||||
RequestedVersion string
|
||||
}
|
||||
|
||||
// VersionResponse is the JSON response from the Workers API endpoint
|
||||
type VersionResponse struct {
|
||||
URL string `json:"url"`
|
||||
Version string `json:"version"`
|
||||
Checksum string `json:"checksum"`
|
||||
IsCompressed bool `json:"compressed"`
|
||||
UserMessage string `json:"userMessage"`
|
||||
ShouldUpdate bool `json:"shouldUpdate"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// WorkersService implements Service.
|
||||
// It contains everything needed to check in with the WorkersAPI and download and apply the updates
|
||||
type WorkersService struct {
|
||||
currentVersion string
|
||||
url string
|
||||
targetPath string
|
||||
opts Options
|
||||
}
|
||||
|
||||
// NewWorkersService creates a new updater Service object.
|
||||
func NewWorkersService(currentVersion, url, targetPath string, opts Options) Service {
|
||||
return &WorkersService{
|
||||
currentVersion: currentVersion,
|
||||
url: url,
|
||||
targetPath: targetPath,
|
||||
opts: opts,
|
||||
}
|
||||
}
|
||||
|
||||
// Check does a check in with the Workers API to get a new version update
|
||||
func (s *WorkersService) Check() (CheckResult, error) {
|
||||
client := &http.Client{
|
||||
Timeout: clientTimeout,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, s.url, nil)
|
||||
q := req.URL.Query()
|
||||
q.Add(OSKeyName, runtime.GOOS)
|
||||
q.Add(ArchitectureKeyName, runtime.GOARCH)
|
||||
q.Add(ClientVersionName, s.currentVersion)
|
||||
|
||||
if s.opts.IsBeta {
|
||||
q.Add(BetaKeyName, "true")
|
||||
}
|
||||
|
||||
if s.opts.RequestedVersion != "" {
|
||||
q.Add(VersionKeyName, s.opts.RequestedVersion)
|
||||
}
|
||||
|
||||
req.URL.RawQuery = q.Encode()
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var v VersionResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v.Error != "" {
|
||||
return nil, errors.New(v.Error)
|
||||
}
|
||||
|
||||
versionToUpdate := ""
|
||||
if v.ShouldUpdate {
|
||||
versionToUpdate = v.Version
|
||||
}
|
||||
|
||||
return NewWorkersVersion(v.URL, versionToUpdate, v.Checksum, s.targetPath, v.UserMessage, v.IsCompressed), nil
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package updater
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var testFilePath = filepath.Join(os.TempDir(), "test")
|
||||
|
||||
func respondWithJSON(w http.ResponseWriter, v interface{}, status int) {
|
||||
data, _ := json.Marshal(v)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
func respondWithData(w http.ResponseWriter, b []byte, status int) {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.WriteHeader(status)
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
const mostRecentVersion = "2021.2.5"
|
||||
const mostRecentBetaVersion = "2021.3.0"
|
||||
const knownBuggyVersion = "2020.12.0"
|
||||
const expectedUserMsg = "This message is expected when running a known buggy version"
|
||||
|
||||
func updateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
version := mostRecentVersion
|
||||
host := fmt.Sprintf("http://%s", r.Host)
|
||||
url := host + "/download"
|
||||
|
||||
query := r.URL.Query()
|
||||
|
||||
if query.Get(BetaKeyName) == "true" {
|
||||
version = mostRecentBetaVersion
|
||||
url = host + "/beta"
|
||||
}
|
||||
|
||||
requestedVersion := query.Get(VersionKeyName)
|
||||
if requestedVersion != "" {
|
||||
version = requestedVersion
|
||||
url = fmt.Sprintf("%s?version=%s", url, requestedVersion)
|
||||
}
|
||||
|
||||
if query.Get(ArchitectureKeyName) != runtime.GOARCH || query.Get(OSKeyName) != runtime.GOOS {
|
||||
respondWithJSON(w, VersionResponse{Error: "unsupported os and architecture"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
fmt.Fprint(h, version)
|
||||
checksum := fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
var userMessage = ""
|
||||
if query.Get(ClientVersionName) == knownBuggyVersion {
|
||||
userMessage = expectedUserMsg
|
||||
}
|
||||
shouldUpdate := requestedVersion != "" || IsNewerVersion(query.Get(ClientVersionName), version)
|
||||
|
||||
v := VersionResponse{
|
||||
URL: url, Version: version, Checksum: checksum, UserMessage: userMessage, ShouldUpdate: shouldUpdate,
|
||||
}
|
||||
respondWithJSON(w, v, http.StatusOK)
|
||||
}
|
||||
|
||||
func gzipUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log.Println("got a request!")
|
||||
version := "2020.09.02"
|
||||
h := sha256.New()
|
||||
fmt.Fprint(h, version)
|
||||
checksum := fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
url := fmt.Sprintf("http://%s/gzip-download.tgz", r.Host)
|
||||
v := VersionResponse{URL: url, Version: version, Checksum: checksum, ShouldUpdate: true}
|
||||
respondWithJSON(w, v, http.StatusOK)
|
||||
}
|
||||
|
||||
func compressedDownloadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
version := "2020.09.02"
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
gw := gzip.NewWriter(buf)
|
||||
tw := tar.NewWriter(gw)
|
||||
|
||||
header := &tar.Header{
|
||||
Size: int64(len(version)),
|
||||
Name: "download",
|
||||
}
|
||||
tw.WriteHeader(header)
|
||||
tw.Write([]byte(version))
|
||||
|
||||
tw.Close()
|
||||
gw.Close()
|
||||
|
||||
respondWithData(w, buf.Bytes(), http.StatusOK)
|
||||
}
|
||||
|
||||
func downloadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
version := mostRecentVersion
|
||||
requestedVersion := r.URL.Query().Get(VersionKeyName)
|
||||
if requestedVersion != "" {
|
||||
version = requestedVersion
|
||||
}
|
||||
respondWithData(w, []byte(version), http.StatusOK)
|
||||
}
|
||||
|
||||
func betaHandler(w http.ResponseWriter, r *http.Request) {
|
||||
respondWithData(w, []byte(mostRecentBetaVersion), http.StatusOK)
|
||||
}
|
||||
|
||||
func failureHandler(w http.ResponseWriter, r *http.Request) {
|
||||
respondWithJSON(w, VersionResponse{Error: "unsupported os and architecture"}, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func IsNewerVersion(current string, check string) bool {
|
||||
if current == "" || check == "" {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(strings.ToLower(current), "dev") {
|
||||
return false // dev builds shouldn't update
|
||||
}
|
||||
|
||||
cMajor, cMinor, cPatch, err := SemanticParts(current)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
nMajor, nMinor, nPatch, err := SemanticParts(check)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if nMajor > cMajor {
|
||||
return true
|
||||
}
|
||||
|
||||
if nMajor == cMajor && nMinor > cMinor {
|
||||
return true
|
||||
}
|
||||
|
||||
if nMajor == cMajor && nMinor == cMinor && nPatch > cPatch {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func SemanticParts(version string) (major int, minor int, patch int, err error) {
|
||||
major = 0
|
||||
minor = 0
|
||||
patch = 0
|
||||
parts := strings.Split(version, ".")
|
||||
if len(parts) != 3 {
|
||||
err = errors.New("invalid version")
|
||||
return
|
||||
}
|
||||
major, err = strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
minor, err = strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
patch, err = strconv.Atoi(parts[2])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func createServer() *httptest.Server {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/updater", updateHandler)
|
||||
mux.HandleFunc("/download", downloadHandler)
|
||||
mux.HandleFunc("/beta", betaHandler)
|
||||
mux.HandleFunc("/fail", failureHandler)
|
||||
mux.HandleFunc("/compressed", gzipUpdateHandler)
|
||||
mux.HandleFunc("/gzip-download.tgz", compressedDownloadHandler)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func createTestFile(t *testing.T, path string) {
|
||||
f, err := os.Create(path)
|
||||
require.NoError(t, err)
|
||||
fmt.Fprint(f, "2020.08.04")
|
||||
f.Close()
|
||||
}
|
||||
|
||||
func TestUpdateService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
log.Println("server url: ", ts.URL)
|
||||
|
||||
s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{})
|
||||
v, err := s.Check()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, v.Version(), mostRecentVersion)
|
||||
|
||||
require.NoError(t, v.Apply())
|
||||
dat, err := ioutil.ReadFile(testFilePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, string(dat), mostRecentVersion)
|
||||
}
|
||||
|
||||
func TestBetaUpdateService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
|
||||
s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{IsBeta: true})
|
||||
v, err := s.Check()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, v.Version(), mostRecentBetaVersion)
|
||||
|
||||
require.NoError(t, v.Apply())
|
||||
dat, err := ioutil.ReadFile(testFilePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, string(dat), mostRecentBetaVersion)
|
||||
}
|
||||
|
||||
func TestFailUpdateService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
|
||||
s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/fail", ts.URL), testFilePath, Options{})
|
||||
v, err := s.Check()
|
||||
require.Error(t, err)
|
||||
require.Nil(t, v)
|
||||
}
|
||||
|
||||
func TestNoUpdateService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
|
||||
s := NewWorkersService(mostRecentVersion, fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{})
|
||||
v, err := s.Check()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, v)
|
||||
require.Empty(t, v.Version())
|
||||
}
|
||||
|
||||
func TestForcedUpdateService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
|
||||
s := NewWorkersService("2020.8.5", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{IsForced: true})
|
||||
v, err := s.Check()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, v.Version(), mostRecentVersion)
|
||||
|
||||
require.NoError(t, v.Apply())
|
||||
dat, err := ioutil.ReadFile(testFilePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, string(dat), mostRecentVersion)
|
||||
}
|
||||
|
||||
func TestUpdateSpecificVersionService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
reqVersion := "2020.9.1"
|
||||
|
||||
s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{RequestedVersion: reqVersion})
|
||||
v, err := s.Check()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, reqVersion, v.Version())
|
||||
|
||||
require.NoError(t, v.Apply())
|
||||
dat, err := ioutil.ReadFile(testFilePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, reqVersion, string(dat))
|
||||
}
|
||||
|
||||
func TestCompressedUpdateService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
|
||||
s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/compressed", ts.URL), testFilePath, Options{})
|
||||
v, err := s.Check()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "2020.09.02", v.Version())
|
||||
|
||||
require.NoError(t, v.Apply())
|
||||
dat, err := ioutil.ReadFile(testFilePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "2020.09.02", string(dat))
|
||||
}
|
||||
|
||||
func TestUpdateWhenRunningKnownBuggyVersion(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
|
||||
s := NewWorkersService(knownBuggyVersion, fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{})
|
||||
v, err := s.Check()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, v.Version(), mostRecentVersion)
|
||||
require.Equal(t, v.UserMessage(), expectedUserMsg)
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
clientTimeout = time.Second * 60
|
||||
// stop the service
|
||||
// rename cloudflared.exe to cloudflared.exe.old
|
||||
// rename cloudflared.exe.new to cloudflared.exe
|
||||
// delete cloudflared.exe.old
|
||||
// start the service
|
||||
// delete the batch file
|
||||
windowsUpdateCommandTemplate = `@echo off
|
||||
sc stop cloudflared >nul 2>&1
|
||||
rename "{{.TargetPath}}" {{.OldName}}
|
||||
rename "{{.NewPath}}" {{.BinaryName}}
|
||||
del "{{.OldPath}}"
|
||||
sc start cloudflared >nul 2>&1
|
||||
del {{.BatchName}}`
|
||||
batchFileName = "cfd_update.bat"
|
||||
)
|
||||
|
||||
// Prepare some data to insert into the template.
|
||||
type batchData struct {
|
||||
TargetPath string
|
||||
OldName string
|
||||
NewPath string
|
||||
OldPath string
|
||||
BinaryName string
|
||||
BatchName string
|
||||
}
|
||||
|
||||
// WorkersVersion implements the Version interface.
|
||||
// It contains everything needed to perform a version upgrade
|
||||
type WorkersVersion struct {
|
||||
downloadURL string
|
||||
checksum string
|
||||
version string
|
||||
targetPath string
|
||||
isCompressed bool
|
||||
userMessage string
|
||||
}
|
||||
|
||||
// NewWorkersVersion creates a new Version object. This is normally created by a WorkersService JSON checkin response
|
||||
// url is where to download the file
|
||||
// version is the version of this update
|
||||
// checksum is the expected checksum of the downloaded file
|
||||
// target path is where the file should be replace. Normally this the running cloudflared's path
|
||||
// userMessage is a possible message to convey back to the user after having checked in with the Updater Service
|
||||
// isCompressed tells whether the asset to update cloudflared is compressed or not
|
||||
func NewWorkersVersion(url, version, checksum, targetPath, userMessage string, isCompressed bool) CheckResult {
|
||||
return &WorkersVersion{
|
||||
downloadURL: url,
|
||||
version: version,
|
||||
checksum: checksum,
|
||||
targetPath: targetPath,
|
||||
isCompressed: isCompressed,
|
||||
userMessage: userMessage,
|
||||
}
|
||||
}
|
||||
|
||||
// Apply does the actual verification and update logic.
|
||||
// This includes signature and checksum validation,
|
||||
// replacing the binary, etc
|
||||
func (v *WorkersVersion) Apply() error {
|
||||
newFilePath := fmt.Sprintf("%s.new", v.targetPath)
|
||||
os.Remove(newFilePath) //remove any failed updates before download
|
||||
|
||||
// download the file
|
||||
if err := download(v.downloadURL, newFilePath, v.isCompressed); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check that the file is what is expected
|
||||
if err := isValidChecksum(v.checksum, newFilePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oldFilePath := fmt.Sprintf("%s.old", v.targetPath)
|
||||
// Windows requires more effort to self update, especially when it is running as a service:
|
||||
// you have to stop the service (if running as one) in order to move/rename the binary
|
||||
// but now the binary isn't running though, so an external process
|
||||
// has to move the old binary out and the new one in then start the service
|
||||
// the easiest way to do this is with a batch file (or with a DLL, but that gets ugly for a cross compiled binary like cloudflared)
|
||||
// a batch file isn't ideal, but it is the simplest path forward for the constraints Windows creates
|
||||
if runtime.GOOS == "windows" {
|
||||
if err := writeBatchFile(v.targetPath, newFilePath, oldFilePath); err != nil {
|
||||
return err
|
||||
}
|
||||
rootDir := filepath.Dir(v.targetPath)
|
||||
batchPath := filepath.Join(rootDir, batchFileName)
|
||||
return runWindowsBatch(batchPath)
|
||||
}
|
||||
|
||||
// now move the current file out, move the new file in and delete the old file
|
||||
if err := os.Rename(v.targetPath, oldFilePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Rename(newFilePath, v.targetPath); err != nil {
|
||||
//attempt rollback
|
||||
os.Rename(oldFilePath, v.targetPath)
|
||||
return err
|
||||
}
|
||||
os.Remove(oldFilePath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns the version number of this update/release (e.g. 2020.08.05)
|
||||
func (v *WorkersVersion) Version() string {
|
||||
return v.version
|
||||
}
|
||||
|
||||
// String returns a possible message to convey back to user after having checked in with the Updater Service. E.g.
|
||||
// it can warn about the need to update the version currently running.
|
||||
func (v *WorkersVersion) UserMessage() string {
|
||||
return v.userMessage
|
||||
}
|
||||
|
||||
// download the file from the link in the json
|
||||
func download(url, filepath string, isCompressed bool) error {
|
||||
client := &http.Client{
|
||||
Timeout: clientTimeout,
|
||||
}
|
||||
resp, err := client.Get(url)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var r io.Reader
|
||||
r = resp.Body
|
||||
|
||||
// compressed macos binary, need to decompress
|
||||
if isCompressed || isCompressedFile(url) {
|
||||
// first the gzip reader
|
||||
gr, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer gr.Close()
|
||||
|
||||
// now the tar
|
||||
tr := tar.NewReader(gr)
|
||||
|
||||
// advance the reader pass the header, which will be the single binary file
|
||||
tr.Next()
|
||||
|
||||
r = tr
|
||||
}
|
||||
|
||||
out, err := os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, r)
|
||||
return err
|
||||
}
|
||||
|
||||
// isCompressedFile is a really simple file extension check to see if this is a macos tar and gzipped
|
||||
func isCompressedFile(urlstring string) bool {
|
||||
if strings.HasSuffix(urlstring, ".tgz") {
|
||||
return true
|
||||
}
|
||||
|
||||
u, err := url.Parse(urlstring)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.HasSuffix(u.Path, ".tgz")
|
||||
}
|
||||
|
||||
// checks if the checksum in the json response matches the checksum of the file download
|
||||
func isValidChecksum(checksum, filePath string) error {
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hash := fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
if checksum != hash {
|
||||
return errors.New("checksum validation failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeBatchFile writes a batch file out to disk
|
||||
// see the dicussion on why it has to be done this way
|
||||
func writeBatchFile(targetPath string, newPath string, oldPath string) error {
|
||||
os.Remove(batchFileName) //remove any failed updates before download
|
||||
f, err := os.Create(batchFileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
cfdName := filepath.Base(targetPath)
|
||||
oldName := filepath.Base(oldPath)
|
||||
|
||||
data := batchData{
|
||||
TargetPath: targetPath,
|
||||
OldName: oldName,
|
||||
NewPath: newPath,
|
||||
OldPath: oldPath,
|
||||
BinaryName: cfdName,
|
||||
BatchName: batchFileName,
|
||||
}
|
||||
|
||||
t, err := template.New("batch").Parse(windowsUpdateCommandTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return t.Execute(f, data)
|
||||
}
|
||||
|
||||
// run each OS command for windows
|
||||
func runWindowsBatch(batchFile string) error {
|
||||
cmd := exec.Command("cmd", "/c", batchFile)
|
||||
return cmd.Start()
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
@@ -12,20 +13,21 @@ import (
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/pkg/errors"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"golang.org/x/sys/windows/svc/eventlog"
|
||||
"golang.org/x/sys/windows/svc/mgr"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
windowsServiceName = "Cloudflared"
|
||||
windowsServiceDescription = "Argo Tunnel agent"
|
||||
windowsServiceUrl = "https://developers.cloudflare.com/argo-tunnel/reference/service/"
|
||||
windowsServiceDescription = "Cloudflare Tunnel agent"
|
||||
windowsServiceUrl = "https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/run-as-service#windows"
|
||||
|
||||
recoverActionDelay = time.Second * 20
|
||||
failureCountResetPeriod = time.Hour * 24
|
||||
@@ -37,22 +39,24 @@ const (
|
||||
// ERROR_FAILED_SERVICE_CONTROLLER_CONNECT
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/debug/system-error-codes--1000-1299-
|
||||
serviceControllerConnectionFailure = 1063
|
||||
|
||||
LogFieldWindowsServiceName = "windowsServiceName"
|
||||
)
|
||||
|
||||
func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
func runApp(app *cli.App, graceShutdownC chan struct{}) {
|
||||
app.Commands = append(app.Commands, &cli.Command{
|
||||
Name: "service",
|
||||
Usage: "Manages the Argo Tunnel Windows service",
|
||||
Usage: "Manages the Cloudflare Tunnel Windows service",
|
||||
Subcommands: []*cli.Command{
|
||||
&cli.Command{
|
||||
{
|
||||
Name: "install",
|
||||
Usage: "Install Argo Tunnel as a Windows service",
|
||||
Action: installWindowsService,
|
||||
Usage: "Install Cloudflare Tunnel as a Windows service",
|
||||
Action: cliutil.ConfiguredAction(installWindowsService),
|
||||
},
|
||||
&cli.Command{
|
||||
{
|
||||
Name: "uninstall",
|
||||
Usage: "Uninstall the Argo Tunnel service",
|
||||
Action: uninstallWindowsService,
|
||||
Usage: "Uninstall the Cloudflare Tunnel service",
|
||||
Action: cliutil.ConfiguredAction(uninstallWindowsService),
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -67,15 +71,11 @@ func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
// 2. get ERROR_FAILED_SERVICE_CONTROLLER_CONNECT
|
||||
// This involves actually trying to start the service.
|
||||
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
log := logger.Create(nil)
|
||||
|
||||
isIntSess, err := svc.IsAnInteractiveSession()
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to determine if we are running in an interactive session: %v", err)
|
||||
log.Fatal().Err(err).Msg("failed to determine if we are running in an interactive session")
|
||||
}
|
||||
if isIntSess {
|
||||
app.Run(os.Args)
|
||||
@@ -85,7 +85,7 @@ func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
// Run executes service name by calling windowsService which is a Handler
|
||||
// interface that implements Execute method.
|
||||
// It will set service status to stop after Execute returns
|
||||
err = svc.Run(windowsServiceName, &windowsService{app: app, shutdownC: shutdownC, graceShutdownC: graceShutdownC})
|
||||
err = svc.Run(windowsServiceName, &windowsService{app: app, graceShutdownC: graceShutdownC})
|
||||
if err != nil {
|
||||
if errno, ok := err.(syscall.Errno); ok && int(errno) == serviceControllerConnectionFailure {
|
||||
// Hack: assume this is a false negative from the IsAnInteractiveSession() check above.
|
||||
@@ -93,27 +93,22 @@ func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
app.Run(os.Args)
|
||||
return
|
||||
}
|
||||
logger.Fatalf("%s service failed: %v", windowsServiceName, err)
|
||||
log.Fatal().Err(err).Msgf("%s service failed", windowsServiceName)
|
||||
}
|
||||
}
|
||||
|
||||
type windowsService struct {
|
||||
app *cli.App
|
||||
shutdownC chan struct{}
|
||||
graceShutdownC chan struct{}
|
||||
}
|
||||
|
||||
// called by the package code at the start of the service
|
||||
// Execute is called by the service manager when service starts, the state
|
||||
// of the service will be set to Stopped when this function returns.
|
||||
func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeRequest, statusChan chan<- svc.Status) (ssec bool, errno uint32) {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
log := logger.Create(nil)
|
||||
elog, err := eventlog.Open(windowsServiceName)
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot open event log for %s with error: %s", windowsServiceName, err)
|
||||
log.Err(err).Msgf("Cannot open event log for %s", windowsServiceName)
|
||||
return
|
||||
}
|
||||
defer elog.Close()
|
||||
@@ -136,13 +131,12 @@ func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeReques
|
||||
}
|
||||
elog.Info(1, fmt.Sprintf("%s service arguments: %v", windowsServiceName, args))
|
||||
|
||||
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
|
||||
statusChan <- svc.Status{State: svc.StartPending}
|
||||
errC := make(chan error)
|
||||
go func() {
|
||||
errC <- s.app.Run(args)
|
||||
}()
|
||||
statusChan <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
||||
statusChan <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown}
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -150,19 +144,26 @@ func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeReques
|
||||
switch c.Cmd {
|
||||
case svc.Interrogate:
|
||||
statusChan <- c.CurrentStatus
|
||||
case svc.Stop:
|
||||
close(s.graceShutdownC)
|
||||
statusChan <- svc.Status{State: svc.StopPending}
|
||||
case svc.Shutdown:
|
||||
close(s.shutdownC)
|
||||
case svc.Stop, svc.Shutdown:
|
||||
if s.graceShutdownC != nil {
|
||||
// start graceful shutdown
|
||||
elog.Info(1, "cloudflared starting graceful shutdown")
|
||||
close(s.graceShutdownC)
|
||||
s.graceShutdownC = nil
|
||||
statusChan <- svc.Status{State: svc.StopPending}
|
||||
continue
|
||||
}
|
||||
// repeated attempts at graceful shutdown forces immediate stop
|
||||
elog.Info(1, "cloudflared terminating immediately")
|
||||
statusChan <- svc.Status{State: svc.StopPending}
|
||||
return false, 0
|
||||
default:
|
||||
elog.Error(1, fmt.Sprintf("unexpected control request #%d", c))
|
||||
}
|
||||
case err := <-errC:
|
||||
ssec = true
|
||||
if err != nil {
|
||||
elog.Error(1, fmt.Sprintf("cloudflared terminated with error %v", err))
|
||||
ssec = true
|
||||
errno = 1
|
||||
} else {
|
||||
elog.Info(1, "cloudflared terminated without error")
|
||||
@@ -174,80 +175,68 @@ func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeReques
|
||||
}
|
||||
|
||||
func installWindowsService(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
zeroLogger := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
logger.Infof("Installing Argo Tunnel Windows service")
|
||||
zeroLogger.Info().Msg("Installing Cloudflare Tunnel Windows service")
|
||||
exepath, err := os.Executable()
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot find path name that start the process")
|
||||
return err
|
||||
return errors.Wrap(err, "Cannot find path name that start the process")
|
||||
}
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot establish a connection to the service control manager: %s", err)
|
||||
return err
|
||||
return errors.Wrap(err, "Cannot establish a connection to the service control manager")
|
||||
}
|
||||
defer m.Disconnect()
|
||||
s, err := m.OpenService(windowsServiceName)
|
||||
log := zeroLogger.With().Str(LogFieldWindowsServiceName, windowsServiceName).Logger()
|
||||
if err == nil {
|
||||
s.Close()
|
||||
logger.Errorf("service %s already exists", windowsServiceName)
|
||||
return fmt.Errorf("service %s already exists", windowsServiceName)
|
||||
return fmt.Errorf("Service %s already exists", windowsServiceName)
|
||||
}
|
||||
config := mgr.Config{StartType: mgr.StartAutomatic, DisplayName: windowsServiceDescription}
|
||||
s, err = m.CreateService(windowsServiceName, exepath, config)
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot install service %s", windowsServiceName)
|
||||
return err
|
||||
return errors.Wrap(err, "Cannot install service")
|
||||
}
|
||||
defer s.Close()
|
||||
logger.Infof("Argo Tunnel agent service is installed")
|
||||
log.Info().Msg("Cloudflare Tunnel agent service is installed")
|
||||
err = eventlog.InstallAsEventCreate(windowsServiceName, eventlog.Error|eventlog.Warning|eventlog.Info)
|
||||
if err != nil {
|
||||
s.Delete()
|
||||
logger.Errorf("Cannot install event logger: %s", err)
|
||||
return fmt.Errorf("SetupEventLogSource() failed: %s", err)
|
||||
return errors.Wrap(err, "Cannot install event logger")
|
||||
}
|
||||
err = configRecoveryOption(s.Handle)
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot set service recovery actions: %s", err)
|
||||
logger.Infof("See %s to manually configure service recovery actions", windowsServiceUrl)
|
||||
log.Err(err).Msg("Cannot set service recovery actions")
|
||||
log.Info().Msgf("See %s to manually configure service recovery actions", windowsServiceUrl)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func uninstallWindowsService(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog).
|
||||
With().
|
||||
Str(LogFieldWindowsServiceName, windowsServiceName).Logger()
|
||||
|
||||
logger.Infof("Uninstalling Argo Tunnel Windows Service")
|
||||
log.Info().Msg("Uninstalling Cloudflare Tunnel Windows Service")
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot establish a connection to the service control manager")
|
||||
return err
|
||||
return errors.Wrap(err, "Cannot establish a connection to the service control manager")
|
||||
}
|
||||
defer m.Disconnect()
|
||||
s, err := m.OpenService(windowsServiceName)
|
||||
if err != nil {
|
||||
logger.Errorf("service %s is not installed", windowsServiceName)
|
||||
return fmt.Errorf("service %s is not installed", windowsServiceName)
|
||||
return fmt.Errorf("Service %s is not installed", windowsServiceName)
|
||||
}
|
||||
defer s.Close()
|
||||
err = s.Delete()
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot delete service %s", windowsServiceName)
|
||||
return err
|
||||
return errors.Wrap(err, "Cannot delete service")
|
||||
}
|
||||
logger.Infof("Argo Tunnel agent service is uninstalled")
|
||||
log.Info().Msg("Cloudflare Tunnel agent service is uninstalled")
|
||||
err = eventlog.Remove(windowsServiceName)
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot remove event logger")
|
||||
return fmt.Errorf("RemoveEventLogSource() failed: %s", err)
|
||||
return errors.Wrap(err, "Cannot remove event logger")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
__pycache__
|
||||
.pytest_cache
|
||||
@@ -0,0 +1,52 @@
|
||||
# Requirements
|
||||
1. Python 3.7 or later with packages in the given `requirements.txt`
|
||||
- E.g. with conda:
|
||||
- `conda create -n component-tests python=3.7`
|
||||
- `conda activate component-tests`
|
||||
- `pip3 install -r requirements.txt`
|
||||
|
||||
2. Create a config yaml file, for example:
|
||||
```
|
||||
cloudflared_binary: "cloudflared"
|
||||
tunnel: "3d539f97-cd3a-4d8e-c33b-65e9099c7a8d"
|
||||
credentials_file: "/Users/tunnel/.cloudflared/3d539f97-cd3a-4d8e-c33b-65e9099c7a8d.json"
|
||||
classic_hostname: "classic-tunnel-component-tests.example.com"
|
||||
origincert: "/Users/tunnel/.cloudflared/cert.pem"
|
||||
ingress:
|
||||
- hostname: named-tunnel-component-tests.example.com
|
||||
service: http_status:200
|
||||
- service: http_status:404
|
||||
```
|
||||
|
||||
3. Route hostname to the tunnel. For the example config above, we can do that via
|
||||
```
|
||||
cloudflared tunnel route dns 3d539f97-cd3a-4d8e-c33b-65e9099c7a8d named-tunnel-component-tests.example.com
|
||||
```
|
||||
|
||||
4. Turn on linter
|
||||
If you are using Visual Studio, follow https://code.visualstudio.com/docs/python/linting to turn on linter.
|
||||
|
||||
5. Turn on formatter
|
||||
If you are using Visual Studio, follow https://code.visualstudio.com/docs/python/editing#_formatting
|
||||
to turn on formatter and https://marketplace.visualstudio.com/items?itemName=cbrevik.toggle-format-on-save
|
||||
to turn on format on save.
|
||||
|
||||
6. If you have cloudflared running as a service on your machine, you can either stop the service or ignore the service tests
|
||||
via `--ignore test_service.py`
|
||||
|
||||
# How to run
|
||||
Specify path to config file via env var `COMPONENT_TESTS_CONFIG`. This is required.
|
||||
## All tests
|
||||
Run `pytest` inside this(component-tests) folder
|
||||
|
||||
## Specific files
|
||||
Run `pytest <file 1 name>.py <file 2 name>.py`
|
||||
|
||||
## Specific tests
|
||||
Run `pytest file.py -k <test 1 name> -k <test 2 name>`
|
||||
|
||||
## Live Logging
|
||||
Running with `-o log_cli=true` outputs logging to CLI as the tests are. By default the log level is WARN.
|
||||
`--log-cli-level` control logging level.
|
||||
For example, to log at info level, run `pytest -o log_cli=true --log-cli-level=INFO`.
|
||||
See https://docs.pytest.org/en/latest/logging.html#live-logs for more documentation on logging.
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python
|
||||
import copy
|
||||
|
||||
from dataclasses import dataclass, InitVar
|
||||
|
||||
from constants import METRICS_PORT, PROXY_DNS_PORT
|
||||
|
||||
# frozen=True raises exception when assigning to fields. This emulates immutability
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BaseConfig:
|
||||
cloudflared_binary: str
|
||||
no_autoupdate: bool = True
|
||||
metrics: str = f'localhost:{METRICS_PORT}'
|
||||
|
||||
def merge_config(self, additional):
|
||||
config = copy.copy(additional)
|
||||
config['no-autoupdate'] = self.no_autoupdate
|
||||
config['metrics'] = self.metrics
|
||||
return config
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NamedTunnelBaseConfig(BaseConfig):
|
||||
# The attributes of the parent class are ordered before attributes in this class,
|
||||
# so we have to use default values here and check if they are set in __post_init__
|
||||
tunnel: str = None
|
||||
credentials_file: str = None
|
||||
ingress: list = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.tunnel is None:
|
||||
raise TypeError("Field tunnel is not set")
|
||||
if self.credentials_file is None:
|
||||
raise TypeError("Field credentials_file is not set")
|
||||
if self.ingress is None:
|
||||
raise TypeError("Field ingress is not set")
|
||||
|
||||
def merge_config(self, additional):
|
||||
config = super(NamedTunnelBaseConfig, self).merge_config(additional)
|
||||
config['tunnel'] = self.tunnel
|
||||
config['credentials-file'] = self.credentials_file
|
||||
# In some cases we want to override default ingress, such as in config tests
|
||||
if 'ingress' not in config:
|
||||
config['ingress'] = self.ingress
|
||||
return config
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NamedTunnelConfig(NamedTunnelBaseConfig):
|
||||
full_config: dict = None
|
||||
additional_config: InitVar[dict] = {}
|
||||
|
||||
def __post_init__(self, additional_config):
|
||||
# Cannot call set self.full_config because the class is frozen, instead, we can use __setattr__
|
||||
# https://docs.python.org/3/library/dataclasses.html#frozen-instances
|
||||
object.__setattr__(self, 'full_config',
|
||||
self.merge_config(additional_config))
|
||||
|
||||
def get_url(self):
|
||||
return "https://" + self.ingress[0]['hostname']
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ClassicTunnelBaseConfig(BaseConfig):
|
||||
hostname: str = None
|
||||
origincert: str = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.hostname is None:
|
||||
raise TypeError("Field tunnel is not set")
|
||||
if self.origincert is None:
|
||||
raise TypeError("Field credentials_file is not set")
|
||||
|
||||
def merge_config(self, additional):
|
||||
config = super(ClassicTunnelBaseConfig, self).merge_config(additional)
|
||||
config['hostname'] = self.hostname
|
||||
config['origincert'] = self.origincert
|
||||
return config
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ClassicTunnelConfig(ClassicTunnelBaseConfig):
|
||||
full_config: dict = None
|
||||
additional_config: InitVar[dict] = {}
|
||||
|
||||
def __post_init__(self, additional_config):
|
||||
# Cannot call set self.full_config because the class is frozen, instead, we can use __setattr__
|
||||
# https://docs.python.org/3/library/dataclasses.html#frozen-instances
|
||||
object.__setattr__(self, 'full_config',
|
||||
self.merge_config(additional_config))
|
||||
|
||||
def get_url(self):
|
||||
return "https://" + self.hostname
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProxyDnsConfig(BaseConfig):
|
||||
full_config = {
|
||||
"port": PROXY_DNS_PORT,
|
||||
"no-autoupdate": True,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import os
|
||||
from enum import Enum, auto
|
||||
from time import sleep
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from config import NamedTunnelConfig, ClassicTunnelConfig, ProxyDnsConfig
|
||||
from constants import BACKOFF_SECS, PROXY_DNS_PORT
|
||||
from util import LOGGER
|
||||
|
||||
|
||||
class CfdModes(Enum):
|
||||
NAMED = auto()
|
||||
CLASSIC = auto()
|
||||
PROXY_DNS = auto()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def component_tests_config():
|
||||
config_file = os.getenv("COMPONENT_TESTS_CONFIG")
|
||||
if config_file is None:
|
||||
raise Exception(
|
||||
"Need to provide path to config file in COMPONENT_TESTS_CONFIG")
|
||||
with open(config_file, 'r') as stream:
|
||||
config = yaml.safe_load(stream)
|
||||
LOGGER.info(f"component tests base config {config}")
|
||||
|
||||
def _component_tests_config(additional_config={}, cfd_mode=CfdModes.NAMED, run_proxy_dns=True):
|
||||
if run_proxy_dns:
|
||||
# Regression test for TUN-4177, running with proxy-dns should not prevent tunnels from running.
|
||||
# So we run all tests with it.
|
||||
additional_config["proxy-dns"] = True
|
||||
additional_config["proxy-dns-port"] = PROXY_DNS_PORT
|
||||
else:
|
||||
additional_config.pop("proxy-dns", None)
|
||||
additional_config.pop("proxy-dns-port", None)
|
||||
|
||||
if cfd_mode is CfdModes.NAMED:
|
||||
return NamedTunnelConfig(additional_config=additional_config,
|
||||
cloudflared_binary=config['cloudflared_binary'],
|
||||
tunnel=config['tunnel'],
|
||||
credentials_file=config['credentials_file'],
|
||||
ingress=config['ingress'])
|
||||
elif cfd_mode is CfdModes.CLASSIC:
|
||||
return ClassicTunnelConfig(
|
||||
additional_config=additional_config, cloudflared_binary=config['cloudflared_binary'],
|
||||
hostname=config['classic_hostname'], origincert=config['origincert'])
|
||||
elif cfd_mode is CfdModes.PROXY_DNS:
|
||||
return ProxyDnsConfig(cloudflared_binary=config['cloudflared_binary'])
|
||||
else:
|
||||
raise Exception(f"Unknown cloudflared mode {cfd_mode}")
|
||||
|
||||
return _component_tests_config
|
||||
|
||||
|
||||
# This fixture is automatically called before each tests to make sure the previous cloudflared has been shutdown
|
||||
@pytest.fixture(autouse=True)
|
||||
def wait_previous_cloudflared():
|
||||
sleep(BACKOFF_SECS)
|
||||
@@ -0,0 +1,9 @@
|
||||
METRICS_PORT = 51000
|
||||
MAX_RETRIES = 5
|
||||
BACKOFF_SECS = 7
|
||||
|
||||
PROXY_DNS_PORT = 9053
|
||||
|
||||
|
||||
def protocols():
|
||||
return ["h2mux", "http2", "quic"]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user