mirror of
https://github.com/cloudflare/cloudflared
synced 2026-06-08 13:33:07 +00:00
Compare commits
150 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -1 +0,0 @@
|
||||
.git
|
||||
@@ -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
|
||||
@@ -15,3 +15,4 @@ cscope.*
|
||||
*-session.log
|
||||
ssh_server_tests/.env
|
||||
/.cover
|
||||
built_artifacts/
|
||||
|
||||
+101
-2
@@ -1,5 +1,105 @@
|
||||
**Experimental**: This is a new format for release notes. The format and availability is subject to change.
|
||||
|
||||
## 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
|
||||
@@ -37,7 +137,7 @@ ingress:
|
||||
ports: [80, 443]
|
||||
allow: true
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Improvements
|
||||
|
||||
@@ -89,4 +189,3 @@ ingress:
|
||||
|
||||
- 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.15.7 as builder
|
||||
FROM golang:1.17.1 as builder
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0 \
|
||||
TARGET_GOOS=${TARGET_GOOS} \
|
||||
|
||||
@@ -11,6 +11,13 @@ ifneq ($(GO_BUILD_TAGS),)
|
||||
GO_BUILD_TAGS := -tags $(GO_BUILD_TAGS)
|
||||
endif
|
||||
|
||||
ifeq ($(NIGHTLY), true)
|
||||
DEB_PACKAGE_NAME := cloudflared-nightly
|
||||
NIGHTLY_FLAGS := --conflicts cloudflared --replaces cloudflared
|
||||
else
|
||||
DEB_PACKAGE_NAME := cloudflared
|
||||
endif
|
||||
|
||||
DATE := $(shell date -u '+%Y-%m-%d-%H%M UTC')
|
||||
VERSION_FLAGS := -ldflags='-X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)"'
|
||||
|
||||
@@ -19,16 +26,6 @@ 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)
|
||||
@@ -42,6 +39,8 @@ 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
|
||||
@@ -81,7 +80,7 @@ clean:
|
||||
go clean
|
||||
|
||||
.PHONY: cloudflared
|
||||
cloudflared: tunnel-deps
|
||||
cloudflared:
|
||||
ifeq ($(FIPS), true)
|
||||
$(info Building cloudflared with go-fips)
|
||||
-test -f fips/fips.go && mv fips/fips.go fips/fips.go.linux-amd64
|
||||
@@ -138,7 +137,7 @@ define build_package
|
||||
--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
|
||||
|
||||
@@ -150,6 +149,14 @@ 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
|
||||
@@ -216,14 +223,14 @@ 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)
|
||||
|
||||
.PHONY: github-release-built-pkgs
|
||||
github-release-built-pkgs:
|
||||
python3 github_release.py --path $(PWD)/built_artifacts --release-version $(VERSION)
|
||||
|
||||
.PHONY: github-message
|
||||
github-message:
|
||||
python3 github_message.py --release-version $(VERSION)
|
||||
@@ -233,24 +240,27 @@ github-mac-upload:
|
||||
python3 github_release.py --path artifacts/cloudflared-darwin-amd64.tgz --release-version $(VERSION) --name cloudflared-darwin-amd64.tgz
|
||||
python3 github_release.py --path artifacts/cloudflared-amd64.pkg --release-version $(VERSION) --name cloudflared-amd64.pkg
|
||||
|
||||
bin/equinox:
|
||||
mkdir -p bin
|
||||
curl -s https://bin.equinox.io/c/75JtLRTsJ3n/release-tool-beta-$(EQUINOX_PLATFORM).tgz | tar xz -C bin/
|
||||
|
||||
.PHONY: tunnel-deps
|
||||
tunnel-deps: tunnelrpc/tunnelrpc.capnp.go
|
||||
|
||||
tunnelrpc/tunnelrpc.capnp.go: tunnelrpc/tunnelrpc.capnp
|
||||
.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 (don't do this in build directory or this will cause vendor issues)
|
||||
# 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: msi
|
||||
msi: cloudflared
|
||||
go-msi make --msi cloudflared.msi --version $(MSI_VERSION)
|
||||
.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
|
||||
|
||||
@@ -29,15 +29,13 @@ Once installed, you can authenticate `cloudflared` into your Cloudflare account
|
||||
|
||||
## TryCloudflare
|
||||
|
||||
Want to test Argo Tunnel before adding a website to Cloudflare? You can do so with TryCloudflare using the documentation [available here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/trycloudflare).
|
||||
Want to test Argo Tunnel before adding a website to Cloudflare? You can do so with TryCloudflare using the documentation [available here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/trycloudflare).
|
||||
|
||||
## Deprecated versions
|
||||
|
||||
Cloudflare currently supports all versions of `cloudflared`. Starting on March 20, 2021, Cloudflare will no longer support versions released prior to 2020.5.1.
|
||||
|
||||
All features available in versions released prior to 2020.5.1 are available in current versions. Breaking changes unrelated to feature availability may be introduced that will impact versions released prior to 2020.5.1. You can read more about upgrading `cloudflared` in our [developer documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#updating-cloudflared).
|
||||
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 | Will no longer be supported starting March 20, 2021 |
|
||||
| Versions prior to 2020.5.1 | No longer supported |
|
||||
|
||||
+179
@@ -1,3 +1,182 @@
|
||||
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
|
||||
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
VERSION=$(git describe --tags --always --dirty="-dev" --match "[0-9][0-9][0-9][0-9].*.*")
|
||||
echo $VERSION
|
||||
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
|
||||
|
||||
|
||||
export FIPS=true
|
||||
linuxArchs=("amd64" "386" "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
|
||||
|
||||
+34
-10
@@ -5,20 +5,25 @@ package carrier
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/token"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/token"
|
||||
)
|
||||
|
||||
const LogFieldOriginURL = "originURL"
|
||||
const (
|
||||
LogFieldOriginURL = "originURL"
|
||||
CFAccessTokenHeader = "Cf-Access-Token"
|
||||
cfJumpDestinationHeader = "Cf-Access-Jump-Destination"
|
||||
)
|
||||
|
||||
type StartOptions struct {
|
||||
AppInfo *token.AppInfo
|
||||
@@ -32,15 +37,11 @@ type StartOptions struct {
|
||||
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) {
|
||||
@@ -149,7 +150,7 @@ func BuildAccessRequest(options *StartOptions, log *zerolog.Logger) (*http.Reque
|
||||
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 {
|
||||
@@ -159,3 +160,26 @@ func BuildAccessRequest(options *StartOptions, log *zerolog.Logger) (*http.Reque
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
@@ -156,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+64
-30
@@ -1,19 +1,16 @@
|
||||
package carrier
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
"github.com/cloudflare/cloudflared/socks"
|
||||
"github.com/cloudflare/cloudflared/token"
|
||||
cfwebsocket "github.com/cloudflare/cloudflared/websocket"
|
||||
"net/url"
|
||||
|
||||
"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
|
||||
@@ -23,20 +20,6 @@ type Websocket struct {
|
||||
isSocks bool
|
||||
}
|
||||
|
||||
type wsdialer struct {
|
||||
conn *cfwebsocket.GorillaConn
|
||||
}
|
||||
|
||||
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(log *zerolog.Logger) Connection {
|
||||
return &Websocket{
|
||||
@@ -54,16 +37,10 @@ func (ws *Websocket) ServeStream(options *StartOptions, conn io.ReadWriter) erro
|
||||
}
|
||||
defer wsConn.Close()
|
||||
|
||||
ingress.Stream(wsConn, conn, ws.log)
|
||||
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.log, listener, remote, shutdownC, ingress.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
|
||||
@@ -84,7 +61,7 @@ func createWebsocketStream(options *StartOptions, log *zerolog.Logger) (*cfwebso
|
||||
TLSClientConfig: options.TLSClientConfig,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
wsConn, resp, err := cfwebsocket.ClientConnect(req, dialer)
|
||||
wsConn, resp, err := clientConnect(req, dialer)
|
||||
defer closeRespBody(resp)
|
||||
|
||||
if err != nil && IsAccessResponse(resp) {
|
||||
@@ -111,6 +88,63 @@ func createWebsocketStream(options *StartOptions, log *zerolog.Logger) (*cfwebso
|
||||
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
|
||||
// a connection with the token set on the request. If it still get redirect,
|
||||
// this probably means the token in storage is invalid (expired/revoked). If that
|
||||
@@ -150,7 +184,7 @@ func createAccessWebSocketStream(options *StartOptions, log *zerolog.Logger) (*w
|
||||
dump, err := httputil.DumpRequest(req, false)
|
||||
log.Debug().Msgf("Access Websocket request: %s", string(dump))
|
||||
|
||||
conn, resp, err := cfwebsocket.ClientConnect(req, nil)
|
||||
conn, resp, err := clientConnect(req, nil)
|
||||
|
||||
if resp != nil {
|
||||
r, err := httputil.DumpResponse(resp, true)
|
||||
|
||||
@@ -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]))
|
||||
}
|
||||
+81
-147
@@ -1,5 +1,5 @@
|
||||
pinned_go: &pinned_go go=1.15.7-1
|
||||
pinned_go_fips: &pinned_go_fips go-fips=1.15.5-3
|
||||
pinned_go: &pinned_go go=1.17-1
|
||||
pinned_go_fips: &pinned_go_fips go-boring=1.16.6-6
|
||||
|
||||
build_dir: &build_dir /cfsetup_build
|
||||
default-flavor: buster
|
||||
@@ -14,9 +14,61 @@ stretch: &stretch
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- make cloudflared
|
||||
build-deb:
|
||||
build-non-fips: # helpful to catch problems with non-fips (only used for releasing non-linux artifacts) before releases
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared
|
||||
build-all-packages: #except osxpkg
|
||||
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
|
||||
pre-cache:
|
||||
# TODO: https://jira.cfops.it/browse/TUN-4792 Replace this wixl with the official one once msitools supports
|
||||
# environment.
|
||||
- wget https://github.com/sudarshan-reddy/msitools/releases/download/v0.101b/wixl -P /usr/local/bin
|
||||
- chmod a+x /usr/local/bin/wixl
|
||||
post-cache:
|
||||
- export FIPS=true
|
||||
- ./build-packages.sh
|
||||
github-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-setuptools
|
||||
- python3-pip
|
||||
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 pygithub
|
||||
post-cache:
|
||||
# build all packages 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
|
||||
build-deb:
|
||||
build_dir: *build_dir
|
||||
builddeps: &build_deb_deps
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
- fakeroot
|
||||
@@ -26,13 +78,18 @@ stretch: &stretch
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- make cloudflared-deb
|
||||
build-deb-nightly:
|
||||
build_dir: *build_dir
|
||||
builddeps: *build_deb_deps
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- export NIGHTLY=true
|
||||
- make cloudflared-deb
|
||||
build-deb-arm64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
builddeps: *build_deb_deps
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=arm64
|
||||
@@ -50,143 +107,14 @@ stretch: &stretch
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- make publish-deb
|
||||
release-linux-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- make release
|
||||
github-release-linux-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: &install_pygithub
|
||||
- pip3 install pygithub
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- make github-release
|
||||
release-linux-armv6:
|
||||
build_dir: *build_dir
|
||||
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
|
||||
pre-cache: *install_pygithub
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=arm
|
||||
- export CC=arm-linux-gnueabihf-gcc
|
||||
- make github-release
|
||||
release-linux-386:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-multilib
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=386
|
||||
- make release
|
||||
github-release-linux-386:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-multilib
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *install_pygithub
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=386
|
||||
- make github-release
|
||||
release-windows-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-mingw-w64
|
||||
post-cache:
|
||||
- export GOOS=windows
|
||||
- export GOARCH=amd64
|
||||
- export CC=x86_64-w64-mingw32-gcc
|
||||
- make release
|
||||
github-release-windows-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-mingw-w64
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *install_pygithub
|
||||
post-cache:
|
||||
- export GOOS=windows
|
||||
- export GOARCH=amd64
|
||||
- export CC=x86_64-w64-mingw32-gcc
|
||||
- make github-release
|
||||
release-windows-386:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-mingw-w64
|
||||
post-cache:
|
||||
- export GOOS=windows
|
||||
- export GOARCH=386
|
||||
- export CC=i686-w64-mingw32-gcc-win32
|
||||
- make release
|
||||
github-release-windows-386:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-mingw-w64
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *install_pygithub
|
||||
post-cache:
|
||||
- export GOOS=windows
|
||||
- export GOARCH=386
|
||||
- export CC=i686-w64-mingw32-gcc-win32
|
||||
- make github-release
|
||||
github-release-linux-arm64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- crossbuild-essential-armhf
|
||||
- g++-aarch64-linux-gnu
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *install_pygithub
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=arm64
|
||||
- export CC=aarch64-linux-gnu-gcc
|
||||
- make github-release
|
||||
github-release-macos-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *install_pygithub
|
||||
pre-cache: &install_pygithub
|
||||
- pip3 install pygithub
|
||||
post-cache:
|
||||
- make github-mac-upload
|
||||
test:
|
||||
@@ -195,13 +123,15 @@ stretch: &stretch
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
- gotest-to-teamcity
|
||||
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
|
||||
- export FIPS=true
|
||||
# cd to a non-module directory: https://github.com/golang/go/issues/24250
|
||||
- (cd / && go get github.com/BurntSushi/go-sumtype)
|
||||
- export PATH="$HOME/go/bin:$PATH"
|
||||
- ./fmt-check.sh
|
||||
- make test | gotest-to-teamcity
|
||||
component-test:
|
||||
build_dir: *build_dir
|
||||
@@ -218,9 +148,11 @@ stretch: &stretch
|
||||
pre-cache:
|
||||
- sudo pip3 install --upgrade -r component-tests/requirements.txt
|
||||
post-cache:
|
||||
# Constructs config file from env vars
|
||||
- python3 component-tests/config.py
|
||||
# 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
|
||||
# The Named Tunnel is deleted and its route unprovisioned here.
|
||||
- python3 component-tests/setup.py --type cleanup
|
||||
update-homebrew:
|
||||
builddeps:
|
||||
- openssh-client
|
||||
@@ -275,8 +207,9 @@ centos-7:
|
||||
- https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
|
||||
pre-cache:
|
||||
- yum install -y fakeroot
|
||||
- wget https://golang.org/dl/go1.15.7.linux-amd64.tar.gz -P /tmp/
|
||||
- tar -C /usr/local -xzf /tmp/go1.15.7.linux-amd64.tar.gz
|
||||
- yum upgrade -y binutils-2.27-44.base.el7.x86_64
|
||||
- wget https://golang.org/dl/go1.16.3.linux-amd64.tar.gz -P /tmp/
|
||||
- tar -C /usr/local -xzf /tmp/go1.16.3.linux-amd64.tar.gz
|
||||
post-cache:
|
||||
- export PATH=$PATH:/usr/local/go/bin
|
||||
- export GOOS=linux
|
||||
@@ -287,8 +220,9 @@ centos-7:
|
||||
builddeps: *el7_builddeps
|
||||
pre-cache:
|
||||
- yum install -y fakeroot
|
||||
- wget https://golang.org/dl/go1.15.7.linux-amd64.tar.gz -P /tmp/
|
||||
- tar -C /usr/local -xzf /tmp/go1.15.7.linux-amd64.tar.gz
|
||||
- yum upgrade -y binutils-2.27-44.base.el7.x86_64
|
||||
- wget https://golang.org/dl/go1.16.3.linux-amd64.tar.gz -P /tmp/
|
||||
- tar -C /usr/local -xzf /tmp/go1.16.3.linux-amd64.tar.gz
|
||||
post-cache:
|
||||
- export PATH=$PATH:/usr/local/go/bin
|
||||
- export GOOS=linux
|
||||
|
||||
@@ -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>
|
||||
@@ -6,19 +6,20 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/carrier"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/carrier"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
)
|
||||
|
||||
const (
|
||||
LogFieldHost = "host"
|
||||
LogFieldHost = "host"
|
||||
cfAccessClientIDHeader = "Cf-Access-Client-Id"
|
||||
cfAccessClientSecretHeader = "Cf-Access-Client-Secret"
|
||||
)
|
||||
|
||||
// StartForwarder starts a client side websocket forward
|
||||
@@ -31,16 +32,14 @@ func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, log *z
|
||||
// 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,
|
||||
@@ -72,16 +71,13 @@ 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,
|
||||
|
||||
@@ -11,19 +11,18 @@ import (
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/carrier"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/sshgen"
|
||||
"github.com/cloudflare/cloudflared/token"
|
||||
"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/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 (
|
||||
@@ -144,11 +143,13 @@ 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: logger.LogSSHDirectoryFlag,
|
||||
@@ -286,7 +287,7 @@ func curl(c *cli.Context) error {
|
||||
}
|
||||
|
||||
cmdArgs = append(cmdArgs, "-H")
|
||||
cmdArgs = append(cmdArgs, fmt.Sprintf("%s: %s", h2mux.CFAccessTokenHeader, tok))
|
||||
cmdArgs = append(cmdArgs, fmt.Sprintf("%s: %s", carrier.CFAccessTokenHeader, tok))
|
||||
return run("curl", cmdArgs...)
|
||||
}
|
||||
|
||||
@@ -387,7 +388,7 @@ func sshGen(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := sshgen.GenerateShortLivedCertificate(appInfo, cfdToken); err != nil {
|
||||
if err := sshgen.GenerateShortLivedCertificate(originURL, cfdToken); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -472,10 +473,10 @@ func isFileThere(candidate string) bool {
|
||||
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{AppInfo: appInfo, OriginURL: appUrl.String(), Headers: headers}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// ForwardServiceType is used to identify what kind of overwatch service this is
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/overwatch"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// AppService is the main service that runs when no command lines flags are passed to cloudflared
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package buildinfo
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type BuildInfo struct {
|
||||
|
||||
@@ -16,6 +16,6 @@ func RemovedCommand(name string) *cli.Command {
|
||||
)
|
||||
},
|
||||
Description: fmt.Sprintf("%s is deprecated", name),
|
||||
Hidden: true,
|
||||
Hidden: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,45 +13,38 @@ func Action(actionFunc cli.ActionFunc) cli.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 {
|
||||
if err := setFlagsFromConfigFile(c); err != nil {
|
||||
warnings, err := setFlagsFromConfigFile(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return actionFunc(c)
|
||||
return actionFunc(c, warnings)
|
||||
})
|
||||
}
|
||||
|
||||
func setFlagsFromConfigFile(c *cli.Context) error {
|
||||
func setFlagsFromConfigFile(c *cli.Context) (configWarnings string, err error) {
|
||||
const errorExitCode = 1
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
inputSource, err := config.ReadConfigFile(c, log)
|
||||
inputSource, warnings, err := config.ReadConfigFile(c, log)
|
||||
if err != nil {
|
||||
if err == config.ErrNoConfigFile {
|
||||
return nil
|
||||
return "", nil
|
||||
}
|
||||
return cli.Exit(err, errorExitCode)
|
||||
return "", cli.Exit(err, errorExitCode)
|
||||
}
|
||||
|
||||
if err := applyConfig(c, inputSource); err != nil {
|
||||
return cli.Exit(err, errorExitCode)
|
||||
if err := altsrc.ApplyInputSource(c, inputSource); err != nil {
|
||||
return "", cli.Exit(err, errorExitCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyConfig(c *cli.Context, inputSource altsrc.InputSourceContext) error {
|
||||
for _, context := range c.Lineage() {
|
||||
if context.Command == nil {
|
||||
// we've reached the placeholder root context not associated with the app
|
||||
break
|
||||
}
|
||||
targetFlags := context.Command.Flags
|
||||
if context.Command.Name == "" {
|
||||
// commands that define child subcommands are executed as if they were an app
|
||||
targetFlags = context.App.Flags
|
||||
}
|
||||
if err := altsrc.ApplyInputSourceValues(context, inputSource, targetFlags); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return warnings, nil
|
||||
}
|
||||
|
||||
@@ -7,13 +7,13 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"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/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func runApp(app *cli.App, graceShutdownC chan struct{}) {
|
||||
@@ -238,7 +238,7 @@ func installLinuxService(c *cli.Context) error {
|
||||
"--origincert", serviceConfigDir + "/" + serviceCredentialFile,
|
||||
}
|
||||
} else {
|
||||
src, err := config.ReadConfigFile(c, log)
|
||||
src, _, err := config.ReadConfigFile(c, log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"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 (
|
||||
|
||||
@@ -6,6 +6,12 @@ import (
|
||||
"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/proxydns"
|
||||
@@ -16,11 +22,6 @@ import (
|
||||
"github.com/cloudflare/cloudflared/metrics"
|
||||
"github.com/cloudflare/cloudflared/overwatch"
|
||||
"github.com/cloudflare/cloudflared/watcher"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -47,6 +48,7 @@ func main() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
metrics.RegisterBuildInfo(BuildTime, Version)
|
||||
raven.SetRelease(Version)
|
||||
maxprocs.Set()
|
||||
|
||||
// Graceful shutdown channel used by the app. When closed, app must terminate gracefully.
|
||||
// Windows service manager closes this channel when it receives stop command.
|
||||
|
||||
@@ -73,7 +73,7 @@ func Run(c *cli.Context) error {
|
||||
log.Fatal().Err(err).Msg("Failed to open the metrics listener")
|
||||
}
|
||||
|
||||
go metrics.ServeMetrics(metricsListener, nil, nil, log)
|
||||
go metrics.ServeMetrics(metricsListener, nil, nil, "", log)
|
||||
|
||||
listener, err := tunneldns.CreateListener(
|
||||
c.String("address"),
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"os/exec"
|
||||
"text/template"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
)
|
||||
|
||||
@@ -12,6 +12,15 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-systemd/daemon"
|
||||
"github.com/facebookgo/grace/gracenet"
|
||||
"github.com/getsentry/raven-go"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/proxydns"
|
||||
@@ -27,15 +36,6 @@ import (
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
|
||||
"github.com/coreos/go-systemd/daemon"
|
||||
"github.com/facebookgo/grace/gracenet"
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -87,6 +87,10 @@ const (
|
||||
var (
|
||||
graceShutdownC chan struct{}
|
||||
version string
|
||||
|
||||
routeFailMsg = fmt.Sprintf("failed to provision routing, please create it manually via Cloudflare dashboard or UI; "+
|
||||
"most likely you already have a conflicting record there. You can also rerun this command with --%s to overwrite "+
|
||||
"any existing DNS records for this hostname.", overwriteDNSFlag)
|
||||
)
|
||||
|
||||
func Flags() []cli.Flag {
|
||||
@@ -159,6 +163,12 @@ func TunnelCommand(c *cli.Context) error {
|
||||
return fmt.Errorf("Use `cloudflared tunnel run` to start tunnel %s", ref)
|
||||
}
|
||||
|
||||
// Unauthenticated named tunnel on <random>.<quick-tunnels-service>.com
|
||||
// For now, default to legacy setup unless quick-service is specified
|
||||
if !dnsProxyStandAlone(c, nil) && c.String("hostname") == "" && c.String("quick-service") != "" {
|
||||
return RunQuickTunnel(sc)
|
||||
}
|
||||
|
||||
// Start a classic tunnel
|
||||
return runClassicTunnel(sc)
|
||||
}
|
||||
@@ -181,7 +191,7 @@ func runAdhocNamedTunnel(sc *subcommandContext, name, credentialsOutputPath stri
|
||||
|
||||
if r, ok := routeFromFlag(sc.c); ok {
|
||||
if res, err := sc.route(tunnel.ID, r); err != nil {
|
||||
sc.log.Err(err).Msg("failed to create route, please create it manually")
|
||||
sc.log.Err(err).Str("route", r.String()).Msg(routeFailMsg)
|
||||
} else {
|
||||
sc.log.Info().Msg(res.SuccessSummary())
|
||||
}
|
||||
@@ -199,12 +209,12 @@ func runClassicTunnel(sc *subcommandContext) error {
|
||||
return StartServer(sc.c, version, nil, sc.log, sc.isUIEnabled)
|
||||
}
|
||||
|
||||
func routeFromFlag(c *cli.Context) (tunnelstore.Route, bool) {
|
||||
func routeFromFlag(c *cli.Context) (route tunnelstore.Route, ok bool) {
|
||||
if hostname := c.String("hostname"); hostname != "" {
|
||||
if lbPool := c.String("lb-pool"); lbPool != "" {
|
||||
return tunnelstore.NewLBRoute(hostname, lbPool), true
|
||||
}
|
||||
return tunnelstore.NewDNSRoute(hostname), true
|
||||
return tunnelstore.NewDNSRoute(hostname, c.Bool(overwriteDNSFlagName)), true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
@@ -296,7 +306,7 @@ func StartServer(
|
||||
}()
|
||||
|
||||
// Serve DNS proxy stand-alone if no hostname or tag or app is going to run
|
||||
if dnsProxyStandAlone(c) {
|
||||
if dnsProxyStandAlone(c, namedTunnel) {
|
||||
connectedSignal.Notify()
|
||||
// no grace period, handle SIGINT/SIGTERM immediately
|
||||
return waitToShutdown(&wg, cancel, errC, graceShutdownC, 0, log)
|
||||
@@ -314,6 +324,15 @@ func StartServer(
|
||||
|
||||
observer := connection.NewObserver(log, logTransport, isUIEnabled)
|
||||
|
||||
// Send Quick Tunnel URL to UI if applicable
|
||||
var quickTunnelURL string
|
||||
if namedTunnel != nil {
|
||||
quickTunnelURL = namedTunnel.QuickTunnelUrl
|
||||
}
|
||||
if quickTunnelURL != "" {
|
||||
observer.SendURL(quickTunnelURL)
|
||||
}
|
||||
|
||||
tunnelConfig, ingressRules, err := prepareTunnelConfig(c, buildInfo, version, log, logTransport, observer, namedTunnel)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Couldn't start tunnel")
|
||||
@@ -331,7 +350,7 @@ func StartServer(
|
||||
defer wg.Done()
|
||||
readinessServer := metrics.NewReadyServer(log)
|
||||
observer.RegisterSink(readinessServer)
|
||||
errC <- metrics.ServeMetrics(metricsListener, ctx.Done(), readinessServer, log)
|
||||
errC <- metrics.ServeMetrics(metricsListener, ctx.Done(), readinessServer, quickTunnelURL, log)
|
||||
}()
|
||||
|
||||
if err := ingressRules.StartOrigins(&wg, log, ctx.Done(), errC); err != nil {
|
||||
@@ -365,7 +384,11 @@ func StartServer(
|
||||
observer.RegisterSink(app)
|
||||
}
|
||||
|
||||
return waitToShutdown(&wg, cancel, errC, graceShutdownC, c.Duration("grace-period"), log)
|
||||
gracePeriod, err := gracePeriod(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return waitToShutdown(&wg, cancel, errC, graceShutdownC, gracePeriod, log)
|
||||
}
|
||||
|
||||
func waitToShutdown(wg *sync.WaitGroup,
|
||||
@@ -475,6 +498,11 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
EnvVars: []string{"TUNNEL_EDGE"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "region",
|
||||
Usage: "Cloudflare Edge region to connect to. Omit or set to empty to connect to the global region.",
|
||||
EnvVars: []string{"TUNNEL_REGION"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: tlsconfig.CaCertFlag,
|
||||
Usage: "Certificate Authority authenticating connections with Cloudflare's edge network.",
|
||||
@@ -565,10 +593,10 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "grace-period",
|
||||
Usage: "Duration to accept new requests after cloudflared receives first SIGINT/SIGTERM. A second SIGINT/SIGTERM will force cloudflared to shutdown immediately.",
|
||||
Usage: "When cloudflared receives SIGINT/SIGTERM it will stop accepting new requests, wait for in-progress requests to terminate, then shutdown. Waiting for in-progress requests will timeout after this grace period, or when a second SIGTERM/SIGINT is received.",
|
||||
Value: time.Second * 30,
|
||||
EnvVars: []string{"TUNNEL_GRACE_PERIOD"},
|
||||
Hidden: true,
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||
altsrc.NewIntFlag(&cli.IntFlag{
|
||||
@@ -595,7 +623,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "stdin-control",
|
||||
Usage: "Control the process using commands sent through stdin",
|
||||
EnvVars: []string{"STDIN-CONTROL"},
|
||||
EnvVars: []string{"STDIN_CONTROL"},
|
||||
Hidden: true,
|
||||
Value: false,
|
||||
}),
|
||||
@@ -612,7 +640,14 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
Value: false,
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "quick-service",
|
||||
Usage: "URL for a service which manages unauthenticated 'quick' tunnels.",
|
||||
Value: "https://api.trycloudflare.com",
|
||||
Hidden: true,
|
||||
}),
|
||||
selectProtocolFlag,
|
||||
overwriteDNSFlag,
|
||||
}...)
|
||||
|
||||
return flags
|
||||
@@ -699,7 +734,7 @@ func configureProxyFlags(shouldHide bool) []cli.Flag {
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: ingress.ProxyTCPKeepAlive,
|
||||
Name: ingress.ProxyTCPKeepAliveFlag,
|
||||
Usage: "HTTP proxy TCP keepalive duration",
|
||||
Value: time.Second * 30,
|
||||
Hidden: shouldHide,
|
||||
|
||||
@@ -7,6 +7,14 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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/buildinfo"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
@@ -18,13 +26,6 @@ import (
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
const LogFieldOriginCertPath = "originCertPath"
|
||||
@@ -85,8 +86,8 @@ func logClientOptions(c *cli.Context, log *zerolog.Logger) {
|
||||
}
|
||||
}
|
||||
|
||||
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(originCertPath string, log *zerolog.Logger) (string, error) {
|
||||
@@ -161,7 +162,6 @@ func prepareTunnelConfig(
|
||||
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(log)
|
||||
@@ -178,19 +178,6 @@ func prepareTunnelConfig(
|
||||
|
||||
tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID})
|
||||
|
||||
var originCert []byte
|
||||
if !isFreeTunnel {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
ingressRules ingress.Ingress
|
||||
classicTunnel *connection.ClassicTunnelConfig
|
||||
@@ -217,6 +204,17 @@ func prepareTunnelConfig(
|
||||
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,
|
||||
@@ -248,17 +246,28 @@ func prepareTunnelConfig(
|
||||
|
||||
edgeTLSConfigs := make(map[connection.Protocol]*tls.Config, len(connection.ProtocolList))
|
||||
for _, p := range connection.ProtocolList {
|
||||
edgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c, p.ServerName())
|
||||
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: c.Duration("grace-period"),
|
||||
GracePeriod: gracePeriod,
|
||||
ReplaceExisting: c.Bool("force"),
|
||||
}
|
||||
muxerConfig := &connection.MuxerConfig{
|
||||
@@ -275,10 +284,10 @@ func prepareTunnelConfig(
|
||||
OSArch: buildInfo.OSArch(),
|
||||
ClientID: clientID,
|
||||
EdgeAddrs: c.StringSlice("edge"),
|
||||
Region: c.String("region"),
|
||||
HAConnections: c.Int("ha-connections"),
|
||||
IncidentLookup: origin.NewIncidentLookup(),
|
||||
IsAutoupdated: c.Bool("is-autoupdated"),
|
||||
IsFreeTunnel: isFreeTunnel,
|
||||
LBPool: c.String("lb-pool"),
|
||||
Tags: tags,
|
||||
Log: log,
|
||||
@@ -296,6 +305,14 @@ func prepareTunnelConfig(
|
||||
}, 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
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@ package tunnel
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
)
|
||||
|
||||
type Info struct {
|
||||
|
||||
@@ -45,7 +45,7 @@ func buildIngressSubcommand() *cli.Command {
|
||||
func buildValidateIngressCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "validate",
|
||||
Action: cliutil.ConfiguredAction(validateIngressCommand),
|
||||
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.",
|
||||
@@ -68,7 +68,7 @@ func buildTestURLCommand() *cli.Command {
|
||||
}
|
||||
|
||||
// validateIngressCommand check the syntax of the ingress rules in the cloudflared config file
|
||||
func validateIngressCommand(c *cli.Context) error {
|
||||
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")
|
||||
@@ -81,6 +81,11 @@ func validateIngressCommand(c *cli.Context) error {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
|
||||
const (
|
||||
baseLoginURL = "https://dash.cloudflare.com/argotunnel"
|
||||
callbackStoreURL = "https://login.argotunnel.com/"
|
||||
callbackStoreURL = "https://login.cloudflareaccess.org/"
|
||||
)
|
||||
|
||||
func buildLoginSubcommand(hidden bool) *cli.Command {
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
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)
|
||||
}
|
||||
|
||||
return StartServer(
|
||||
sc.c,
|
||||
version,
|
||||
&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
|
||||
}
|
||||
@@ -20,4 +20,4 @@ func waitForSignal(graceShutdownC chan struct{}, logger *zerolog.Logger) {
|
||||
close(graceShutdownC)
|
||||
case <-graceShutdownC:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// +build !windows
|
||||
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
@@ -101,4 +103,3 @@ func TestWaitForShutdown(t *testing.T) {
|
||||
assert.True(t, contextCancelled)
|
||||
assert.True(t, time.Now().Sub(startTime) < time.Second) // check that wait ended early
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -147,7 +148,7 @@ func (sc *subcommandContext) readTunnelCredentials(credFinder CredFinder) (conne
|
||||
return credentials, nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) create(name string, credentialsOutputPath string) (*tunnelstore.Tunnel, error) {
|
||||
func (sc *subcommandContext) create(name string, credentialsFilePath string) (*tunnelstore.Tunnel, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "couldn't create client to talk to Argo Tunnel backend")
|
||||
@@ -173,27 +174,40 @@ func (sc *subcommandContext) create(name string, credentialsOutputPath string) (
|
||||
TunnelID: tunnel.ID,
|
||||
TunnelName: name,
|
||||
}
|
||||
filePath, writeFileErr := writeTunnelCredentials(credential.certPath, credentialsOutputPath, &tunnelCredentials)
|
||||
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 to the tunnel credentials file at %v.json.", tunnel.Name, tunnel.ID, tunnel.ID))
|
||||
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 tunnelfile"))
|
||||
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)
|
||||
}
|
||||
sc.log.Info().Msgf("Tunnel credentials written to %v. cloudflared chose this file based on where your origin certificate was found. Keep this file secret. To revoke these credentials, delete the tunnel.", filePath)
|
||||
|
||||
if outputFormat := sc.c.String(outputFormatFlag.Name); outputFormat != "" {
|
||||
return nil, renderOutput(outputFormat, &tunnel)
|
||||
}
|
||||
|
||||
sc.log.Info().Msgf("Created tunnel %s with id %s", tunnel.Name, tunnel.ID)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -247,8 +261,16 @@ func (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error {
|
||||
// and add the TunnelID into any old credentials (generated before TUN-3581 added the `TunnelID`
|
||||
// field to credentials files)
|
||||
func (sc *subcommandContext) findCredentials(tunnelID uuid.UUID) (connection.Credentials, error) {
|
||||
credFinder := sc.credentialFinder(tunnelID)
|
||||
credentials, err := sc.readTunnelCredentials(credFinder)
|
||||
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.
|
||||
|
||||
@@ -3,8 +3,9 @@ package tunnel
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/cloudflare/cloudflared/teamnet"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cloudflare/cloudflared/teamnet"
|
||||
)
|
||||
|
||||
const noClientMsg = "error while creating backend client"
|
||||
|
||||
@@ -8,13 +8,13 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func Test_findIDs(t *testing.T) {
|
||||
@@ -191,6 +191,51 @@ func Test_subcommandContext_findCredentials(t *testing.T) {
|
||||
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) {
|
||||
|
||||
@@ -14,12 +14,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
"golang.org/x/net/idna"
|
||||
"gopkg.in/yaml.v2"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||
@@ -29,10 +29,12 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
allSortByOptions = "name, id, createdAt, deletedAt, numConnections"
|
||||
connsSortByOptions = "id, startedAt, numConnections, version"
|
||||
CredFileFlagAlias = "cred-file"
|
||||
CredFileFlag = "credentials-file"
|
||||
allSortByOptions = "name, id, createdAt, deletedAt, numConnections"
|
||||
connsSortByOptions = "id, startedAt, numConnections, version"
|
||||
CredFileFlagAlias = "cred-file"
|
||||
CredFileFlag = "credentials-file"
|
||||
CredContentsFlag = "credentials-contents"
|
||||
overwriteDNSFlagName = "overwrite-dns"
|
||||
|
||||
LogFieldTunnelID = "tunnelID"
|
||||
)
|
||||
@@ -48,6 +50,16 @@ var (
|
||||
Aliases: []string{"n"},
|
||||
Usage: "List tunnels with the given `NAME`",
|
||||
}
|
||||
listNamePrefixFlag = &cli.StringFlag{
|
||||
Name: "name-prefix",
|
||||
Aliases: []string{"np"},
|
||||
Usage: "List tunnels that start with the give `NAME` prefix",
|
||||
}
|
||||
listExcludeNamePrefixFlag = &cli.StringFlag{
|
||||
Name: "exclude-name-prefix",
|
||||
Aliases: []string{"enp"},
|
||||
Usage: "List tunnels whose `NAME` does not start with the given prefix",
|
||||
}
|
||||
listExistedAtFlag = &cli.TimestampFlag{
|
||||
Name: "when",
|
||||
Aliases: []string{"w"},
|
||||
@@ -94,11 +106,17 @@ var (
|
||||
Aliases: []string{"F"},
|
||||
Usage: "Opt into various features that are still being developed or tested.",
|
||||
})
|
||||
credentialsFileFlag = altsrc.NewStringFlag(&cli.StringFlag{
|
||||
credentialsFileFlagCLIOnly = &cli.StringFlag{
|
||||
Name: CredFileFlag,
|
||||
Aliases: []string{CredFileFlagAlias},
|
||||
Usage: "Filepath at which to read/write the tunnel credentials",
|
||||
EnvVars: []string{"TUNNEL_CRED_FILE"},
|
||||
}
|
||||
credentialsFileFlag = altsrc.NewStringFlag(credentialsFileFlagCLIOnly)
|
||||
credentialsContentsFlag = altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: CredContentsFlag,
|
||||
Usage: "Contents of the tunnel credentials JSON file to use. When provided along with credentials-file, this will take precedence.",
|
||||
EnvVars: []string{"TUNNEL_CRED_CONTENTS"},
|
||||
})
|
||||
forceDeleteFlag = &cli.BoolFlag{
|
||||
Name: "force",
|
||||
@@ -109,7 +127,7 @@ var (
|
||||
}
|
||||
selectProtocolFlag = altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "protocol",
|
||||
Value: "h2mux",
|
||||
Value: "auto",
|
||||
Aliases: []string{"p"},
|
||||
Usage: fmt.Sprintf("Protocol implementation to connect with Cloudflare's edge network. %s", connection.AvailableProtocolFlagMessage),
|
||||
EnvVars: []string{"TUNNEL_TRANSPORT_PROTOCOL"},
|
||||
@@ -132,6 +150,12 @@ var (
|
||||
Usage: `Constraints the cleanup to stop the connections of a single Connector (by its ID). You can find the various Connectors (and their IDs) currently connected to your tunnel via 'cloudflared tunnel info <name>'.`,
|
||||
EnvVars: []string{"TUNNEL_CLEANUP_CONNECTOR"},
|
||||
}
|
||||
overwriteDNSFlag = &cli.BoolFlag{
|
||||
Name: overwriteDNSFlagName,
|
||||
Aliases: []string{"f"},
|
||||
Usage: `Overwrites existing DNS records with this hostname`,
|
||||
EnvVars: []string{"TUNNEL_FORCE_PROVISIONING_DNS"},
|
||||
}
|
||||
)
|
||||
|
||||
func buildCreateCommand() *cli.Command {
|
||||
@@ -146,7 +170,7 @@ func buildCreateCommand() *cli.Command {
|
||||
For example, to create a tunnel named 'my-tunnel' run:
|
||||
|
||||
$ cloudflared tunnel create my-tunnel`,
|
||||
Flags: []cli.Flag{outputFormatFlag, credentialsFileFlag},
|
||||
Flags: []cli.Flag{outputFormatFlag, credentialsFileFlagCLIOnly},
|
||||
CustomHelpTemplate: commandHelpTemplate(),
|
||||
}
|
||||
}
|
||||
@@ -182,27 +206,20 @@ func tunnelFilePath(tunnelID uuid.UUID, directory string) (string, error) {
|
||||
return homedir.Expand(filePath)
|
||||
}
|
||||
|
||||
// If an `outputFile` is given, write the credentials there.
|
||||
// Otherwise, write it to the same directory as the originCert,
|
||||
// with the filename `<tunnel id>.json`.
|
||||
func writeTunnelCredentials(
|
||||
originCertPath, outputFile string,
|
||||
credentials *connection.Credentials,
|
||||
) (filePath string, err error) {
|
||||
filePath = outputFile
|
||||
if outputFile == "" {
|
||||
originCertDir := filepath.Dir(originCertPath)
|
||||
filePath, err = tunnelFilePath(credentials.TunnelID, originCertDir)
|
||||
// writeTunnelCredentials saves `credentials` as a JSON into `filePath`, only if
|
||||
// the file does not exist already
|
||||
func writeTunnelCredentials(filePath string, credentials *connection.Credentials) error {
|
||||
if _, err := os.Stat(filePath); !os.IsNotExist(err) {
|
||||
if err == nil {
|
||||
return fmt.Errorf("%s already exists", filePath)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Write the name and ID to the file too
|
||||
body, err := json.Marshal(credentials)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "Unable to marshal tunnel credentials to JSON")
|
||||
return errors.Wrap(err, "Unable to marshal tunnel credentials to JSON")
|
||||
}
|
||||
return filePath, ioutil.WriteFile(filePath, body, 400)
|
||||
return ioutil.WriteFile(filePath, body, 400)
|
||||
}
|
||||
|
||||
func buildListCommand() *cli.Command {
|
||||
@@ -216,6 +233,8 @@ func buildListCommand() *cli.Command {
|
||||
outputFormatFlag,
|
||||
showDeletedFlag,
|
||||
listNameFlag,
|
||||
listNamePrefixFlag,
|
||||
listExcludeNamePrefixFlag,
|
||||
listExistedAtFlag,
|
||||
listIDFlag,
|
||||
showRecentlyDisconnected,
|
||||
@@ -242,6 +261,12 @@ func listCommand(c *cli.Context) error {
|
||||
if name := c.String("name"); name != "" {
|
||||
filter.ByName(name)
|
||||
}
|
||||
if namePrefix := c.String("name-prefix"); namePrefix != "" {
|
||||
filter.ByNamePrefix(namePrefix)
|
||||
}
|
||||
if excludePrefix := c.String("exclude-name-prefix"); excludePrefix != "" {
|
||||
filter.ExcludeNameWithPrefix(excludePrefix)
|
||||
}
|
||||
if existedAt := c.Timestamp("time"); existedAt != nil {
|
||||
filter.ByExistedAt(*existedAt)
|
||||
}
|
||||
@@ -374,8 +399,8 @@ func tunnelInfo(c *cli.Context) error {
|
||||
warningChecker := updater.StartWarningCheck(c)
|
||||
defer warningChecker.LogWarningIfAny(sc.log)
|
||||
|
||||
if c.NArg() > 1 {
|
||||
return cliutil.UsageError(`"cloudflared tunnel info" accepts only one argument, the ID or name of the tunnel to run.`)
|
||||
if c.NArg() != 1 {
|
||||
return cliutil.UsageError(`"cloudflared tunnel info" accepts exactly one argument, the ID or name of the tunnel to get info about.`)
|
||||
}
|
||||
tunnelID, err := sc.findID(c.Args().First())
|
||||
if err != nil {
|
||||
@@ -460,14 +485,30 @@ func formatAndPrintConnectionsList(tunnelInfo Info, showRecentlyDisconnected boo
|
||||
writer := tabWriter()
|
||||
defer writer.Flush()
|
||||
|
||||
// Print the general tunnel info table
|
||||
_, _ = fmt.Fprintf(writer, "NAME: %s\nID: %s\nCREATED: %s\n\n", tunnelInfo.Name, tunnelInfo.ID, tunnelInfo.CreatedAt)
|
||||
|
||||
// Determine whether to print the connector table
|
||||
shouldDisplayTable := false
|
||||
for _, c := range tunnelInfo.Connectors {
|
||||
conns := fmtConnections(c.Connections, showRecentlyDisconnected)
|
||||
if len(conns) > 0 {
|
||||
shouldDisplayTable = true
|
||||
}
|
||||
}
|
||||
if !shouldDisplayTable {
|
||||
fmt.Println("This tunnel has no active connectors.")
|
||||
return
|
||||
}
|
||||
|
||||
// Print the connector table
|
||||
_, _ = fmt.Fprintln(writer, "CONNECTOR ID\tCREATED\tARCHITECTURE\tVERSION\tORIGIN IP\tEDGE\t")
|
||||
for _, c := range tunnelInfo.Connectors {
|
||||
var originIp = ""
|
||||
if len(c.Connections) > 0 {
|
||||
originIp = c.Connections[0].OriginIP.String()
|
||||
conns := fmtConnections(c.Connections, showRecentlyDisconnected)
|
||||
if len(conns) == 0 {
|
||||
continue
|
||||
}
|
||||
originIp := c.Connections[0].OriginIP.String()
|
||||
formattedStr := fmt.Sprintf(
|
||||
"%s\t%s\t%s\t%s\t%s\t%s\t",
|
||||
c.ID,
|
||||
@@ -475,7 +516,7 @@ func formatAndPrintConnectionsList(tunnelInfo Info, showRecentlyDisconnected boo
|
||||
c.Arch,
|
||||
c.Version,
|
||||
originIp,
|
||||
fmtConnections(c.Connections, showRecentlyDisconnected),
|
||||
conns,
|
||||
)
|
||||
_, _ = fmt.Fprintln(writer, formattedStr)
|
||||
}
|
||||
@@ -501,7 +542,7 @@ func buildDeleteCommand() *cli.Command {
|
||||
Usage: "Delete existing tunnel by UUID or name",
|
||||
UsageText: "cloudflared tunnel [tunnel command options] delete [subcommand options] TUNNEL",
|
||||
Description: "cloudflared tunnel delete will delete tunnels with the given tunnel UUIDs or names. A tunnel cannot be deleted if it has active connections. To delete the tunnel unconditionally, use -f flag.",
|
||||
Flags: []cli.Flag{credentialsFileFlag, forceDeleteFlag},
|
||||
Flags: []cli.Flag{credentialsFileFlagCLIOnly, forceDeleteFlag},
|
||||
CustomHelpTemplate: commandHelpTemplate(),
|
||||
}
|
||||
}
|
||||
@@ -544,6 +585,7 @@ func buildRunCommand() *cli.Command {
|
||||
flags := []cli.Flag{
|
||||
forceFlag,
|
||||
credentialsFileFlag,
|
||||
credentialsContentsFlag,
|
||||
selectProtocolFlag,
|
||||
featuresFlag,
|
||||
}
|
||||
@@ -585,6 +627,12 @@ func runCommand(c *cli.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
if c.String("hostname") != "" {
|
||||
sc.log.Warn().Msg("The property `hostname` in your configuration is ignored because you configured a Named Tunnel " +
|
||||
"in the property `tunnel` to run. Make sure to provision the routing (e.g. via `cloudflared tunnel route dns/lb`) or else " +
|
||||
"your origin will not be reachable. You should remove the `hostname` property to avoid this warning.")
|
||||
}
|
||||
|
||||
return runNamedTunnel(sc, tunnelRef)
|
||||
}
|
||||
|
||||
@@ -632,7 +680,6 @@ func cleanupCommand(c *cli.Context) error {
|
||||
func buildRouteCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "route",
|
||||
Action: cliutil.ConfiguredAction(routeCommand),
|
||||
Usage: "Define which traffic routed from Cloudflare edge to this tunnel: requests to a DNS hostname, to a Cloudflare Load Balancer, or traffic originating from Cloudflare WARP clients",
|
||||
UsageText: "cloudflared tunnel [tunnel command options] route [subcommand options] [dns TUNNEL HOSTNAME]|[lb TUNNEL HOSTNAME LB-POOL]|[ip NETWORK TUNNEL]",
|
||||
Description: `The route command defines how Cloudflare will proxy requests to this tunnel.
|
||||
@@ -652,15 +699,30 @@ Further information about managing Cloudflare WARP traffic to your tunnel is ava
|
||||
`,
|
||||
CustomHelpTemplate: commandHelpTemplate(),
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "dns",
|
||||
Action: cliutil.ConfiguredAction(routeDnsCommand),
|
||||
Usage: "Route a hostname by creating a DNS CNAME record to a tunnel",
|
||||
UsageText: "cloudflared tunnel route dns [TUNNEL] [HOSTNAME]",
|
||||
Description: `Creates a DNS CNAME record hostname that points to the tunnel.`,
|
||||
Flags: []cli.Flag{overwriteDNSFlag},
|
||||
},
|
||||
{
|
||||
Name: "lb",
|
||||
Action: cliutil.ConfiguredAction(routeLbCommand),
|
||||
Usage: "Use this tunnel as a load balancer origin, creating pool and load balancer if necessary",
|
||||
UsageText: "cloudflared tunnel route dns [TUNNEL] [HOSTNAME] [LB-POOL]",
|
||||
Description: `Creates Load Balancer with an origin pool that points to the tunnel.`,
|
||||
},
|
||||
buildRouteIPSubcommand(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func dnsRouteFromArg(c *cli.Context) (tunnelstore.Route, error) {
|
||||
func dnsRouteFromArg(c *cli.Context, overwriteExisting bool) (tunnelstore.Route, error) {
|
||||
const (
|
||||
userHostnameIndex = 2
|
||||
expectedNArgs = 3
|
||||
userHostnameIndex = 1
|
||||
expectedNArgs = 2
|
||||
)
|
||||
if c.NArg() != expectedNArgs {
|
||||
return nil, cliutil.UsageError("Expected %d arguments, got %d", expectedNArgs, c.NArg())
|
||||
@@ -671,14 +733,14 @@ func dnsRouteFromArg(c *cli.Context) (tunnelstore.Route, error) {
|
||||
} else if !validateHostname(userHostname, true) {
|
||||
return nil, errors.Errorf("%s is not a valid hostname", userHostname)
|
||||
}
|
||||
return tunnelstore.NewDNSRoute(userHostname), nil
|
||||
return tunnelstore.NewDNSRoute(userHostname, overwriteExisting), nil
|
||||
}
|
||||
|
||||
func lbRouteFromArg(c *cli.Context) (tunnelstore.Route, error) {
|
||||
const (
|
||||
lbNameIndex = 2
|
||||
lbPoolIndex = 3
|
||||
expectedNArgs = 4
|
||||
lbNameIndex = 1
|
||||
lbPoolIndex = 2
|
||||
expectedNArgs = 3
|
||||
)
|
||||
if c.NArg() != expectedNArgs {
|
||||
return nil, cliutil.UsageError("Expected %d arguments, got %d", expectedNArgs, c.NArg())
|
||||
@@ -720,41 +782,39 @@ func validateHostname(s string, allowWildcardSubdomain bool) bool {
|
||||
return err == nil && validateName(puny, allowWildcardSubdomain)
|
||||
}
|
||||
|
||||
func routeCommand(c *cli.Context) error {
|
||||
if c.NArg() < 2 {
|
||||
return cliutil.UsageError(`"cloudflared tunnel route" requires the first argument to be the route type(dns or lb), followed by the ID or name of the tunnel`)
|
||||
func routeDnsCommand(c *cli.Context) error {
|
||||
if c.NArg() != 2 {
|
||||
return cliutil.UsageError(`This command expects the format "cloudflared tunnel route dns <tunnel name/id> <hostname>"`)
|
||||
}
|
||||
return routeCommand(c, "dns")
|
||||
}
|
||||
|
||||
func routeLbCommand(c *cli.Context) error {
|
||||
if c.NArg() != 3 {
|
||||
return cliutil.UsageError(`This command expects the format "cloudflared tunnel route lb <tunnel name/id> <hostname> <load balancer pool>"`)
|
||||
}
|
||||
return routeCommand(c, "lb")
|
||||
}
|
||||
|
||||
func routeCommand(c *cli.Context, routeType string) error {
|
||||
sc, err := newSubcommandContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
const tunnelIDIndex = 1
|
||||
|
||||
routeType := c.Args().First()
|
||||
tunnelID, err := sc.findID(c.Args().Get(0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var route tunnelstore.Route
|
||||
var tunnelID uuid.UUID
|
||||
switch routeType {
|
||||
case "dns":
|
||||
tunnelID, err = sc.findID(c.Args().Get(tunnelIDIndex))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
route, err = dnsRouteFromArg(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
route, err = dnsRouteFromArg(c, c.Bool(overwriteDNSFlagName))
|
||||
case "lb":
|
||||
tunnelID, err = sc.findID(c.Args().Get(tunnelIDIndex))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
route, err = lbRouteFromArg(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return cliutil.UsageError("%s is not a recognized route type. Supported route types are dns and lb", routeType)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := sc.route(tunnelID, route)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
"github.com/google/uuid"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
)
|
||||
|
||||
func Test_fmtConnections(t *testing.T) {
|
||||
@@ -94,7 +94,7 @@ func TestTunnelfilePath(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
homeDir, err := homedir.Dir()
|
||||
assert.NoError(t, err)
|
||||
expected := fmt.Sprintf("%s/.cloudflared/%v.json", homeDir, tunnelID)
|
||||
expected := filepath.Join(homeDir, ".cloudflared", tunnelID.String()+".json")
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
|
||||
"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 {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// +build !windows
|
||||
|
||||
package updater
|
||||
|
||||
import (
|
||||
@@ -13,6 +15,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -21,6 +24,8 @@ import (
|
||||
"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)
|
||||
|
||||
@@ -208,7 +213,6 @@ func TestUpdateService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
testFilePath := "tmpfile"
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
log.Println("server url: ", ts.URL)
|
||||
@@ -229,7 +233,6 @@ func TestBetaUpdateService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
testFilePath := "tmpfile"
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
|
||||
@@ -249,7 +252,6 @@ func TestFailUpdateService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
testFilePath := "tmpfile"
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
|
||||
@@ -263,7 +265,6 @@ func TestNoUpdateService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
testFilePath := "tmpfile"
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
|
||||
@@ -278,7 +279,6 @@ func TestForcedUpdateService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
testFilePath := "tmpfile"
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
|
||||
@@ -298,7 +298,6 @@ func TestUpdateSpecificVersionService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
testFilePath := "tmpfile"
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
reqVersion := "2020.9.1"
|
||||
@@ -319,7 +318,6 @@ func TestCompressedUpdateService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
testFilePath := "tmpfile"
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
|
||||
@@ -339,7 +337,6 @@ func TestUpdateWhenRunningKnownBuggyVersion(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
testFilePath := "tmpfile"
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
|
||||
|
||||
@@ -13,15 +13,14 @@ import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
|
||||
"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 (
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
#!/usr/bin/env python
|
||||
import base64
|
||||
import copy
|
||||
import os
|
||||
import yaml
|
||||
|
||||
from dataclasses import dataclass, InitVar
|
||||
|
||||
from constants import METRICS_PORT
|
||||
|
||||
from util import LOGGER
|
||||
from constants import METRICS_PORT, PROXY_DNS_PORT
|
||||
|
||||
# frozen=True raises exception when assigning to fields. This emulates immutability
|
||||
|
||||
@@ -100,33 +95,10 @@ class ClassicTunnelConfig(ClassicTunnelBaseConfig):
|
||||
return "https://" + self.hostname
|
||||
|
||||
|
||||
def build_config_from_env():
|
||||
config_path = get_env("COMPONENT_TESTS_CONFIG")
|
||||
config_content = base64.b64decode(
|
||||
get_env("COMPONENT_TESTS_CONFIG_CONTENT")).decode('utf-8')
|
||||
config_yaml = yaml.safe_load(config_content)
|
||||
@dataclass(frozen=True)
|
||||
class ProxyDnsConfig(BaseConfig):
|
||||
full_config = {
|
||||
"port": PROXY_DNS_PORT,
|
||||
"no-autoupdate": True,
|
||||
}
|
||||
|
||||
credentials_file = get_env("COMPONENT_TESTS_CREDENTIALS_FILE")
|
||||
write_file(credentials_file, config_yaml["credentials_file"])
|
||||
|
||||
origincert = get_env("COMPONENT_TESTS_ORIGINCERT")
|
||||
write_file(origincert, config_yaml["origincert"])
|
||||
|
||||
write_file(config_content, config_path)
|
||||
|
||||
|
||||
def write_file(content, path):
|
||||
with open(path, 'w') as outfile:
|
||||
outfile.write(content)
|
||||
outfile.close
|
||||
|
||||
|
||||
def get_env(env_name):
|
||||
val = os.getenv(env_name)
|
||||
if val is None:
|
||||
raise Exception(f"{env_name} is not set")
|
||||
return val
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
build_config_from_env()
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import os
|
||||
from enum import Enum, auto
|
||||
from time import sleep
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from time import sleep
|
||||
|
||||
from config import NamedTunnelConfig, ClassicTunnelConfig
|
||||
from constants import BACKOFF_SECS
|
||||
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")
|
||||
@@ -19,12 +26,30 @@ def component_tests_config():
|
||||
config = yaml.safe_load(stream)
|
||||
LOGGER.info(f"component tests base config {config}")
|
||||
|
||||
def _component_tests_config(additional_config={}, named_tunnel=True):
|
||||
if named_tunnel:
|
||||
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'])
|
||||
return ClassicTunnelConfig(
|
||||
additional_config=additional_config, cloudflared_binary=config['cloudflared_binary'], hostname=config['classic_hostname'], origincert=config['origincert'])
|
||||
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
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
METRICS_PORT = 51000
|
||||
MAX_RETRIES = 5
|
||||
BACKOFF_SECS = 7
|
||||
|
||||
PROXY_DNS_PORT = 9053
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
cloudflare==2.8.15
|
||||
flaky==3.7.0
|
||||
pytest==6.2.2
|
||||
pyyaml==5.4.1
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import uuid
|
||||
|
||||
import CloudFlare
|
||||
import yaml
|
||||
from retrying import retry
|
||||
|
||||
from constants import MAX_RETRIES, BACKOFF_SECS
|
||||
from util import LOGGER
|
||||
|
||||
|
||||
def get_config_from_env():
|
||||
config_content = base64.b64decode(get_env("COMPONENT_TESTS_CONFIG_CONTENT")).decode('utf-8')
|
||||
return yaml.safe_load(config_content)
|
||||
|
||||
|
||||
def get_config_from_file():
|
||||
config_path = get_env("COMPONENT_TESTS_CONFIG")
|
||||
with open(config_path, 'r') as infile:
|
||||
return yaml.safe_load(infile)
|
||||
|
||||
|
||||
def persist_config(config):
|
||||
config_path = get_env("COMPONENT_TESTS_CONFIG")
|
||||
with open(config_path, 'w') as outfile:
|
||||
yaml.safe_dump(config, outfile)
|
||||
|
||||
|
||||
def persist_origin_cert(config):
|
||||
origincert = get_env("COMPONENT_TESTS_ORIGINCERT")
|
||||
path = config["origincert"]
|
||||
with open(path, 'w') as outfile:
|
||||
outfile.write(origincert)
|
||||
return path
|
||||
|
||||
|
||||
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
|
||||
def create_tunnel(config, origincert_path, random_uuid):
|
||||
# Delete any previous existing credentials file. If the agent keeps files around (that's the case in Windows) then
|
||||
# cloudflared tunnel create will refuse to create the tunnel because it does not want to overwrite credentials
|
||||
# files.
|
||||
credentials_path = config["credentials_file"]
|
||||
try:
|
||||
os.remove(credentials_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
tunnel_name = "cfd_component_test-" + random_uuid
|
||||
create_cmd = [config["cloudflared_binary"], "tunnel", "--origincert", origincert_path, "create",
|
||||
"--credentials-file", credentials_path, tunnel_name]
|
||||
LOGGER.info(f"Creating tunnel with {create_cmd}")
|
||||
subprocess.run(create_cmd, check=True)
|
||||
|
||||
list_cmd = [config["cloudflared_binary"], "tunnel", "--origincert", origincert_path, "list", "--name",
|
||||
tunnel_name, "--output", "json"]
|
||||
LOGGER.info(f"Listing tunnel with {list_cmd}")
|
||||
cloudflared = subprocess.run(list_cmd, check=True, capture_output=True)
|
||||
return json.loads(cloudflared.stdout)[0]["id"]
|
||||
|
||||
|
||||
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
|
||||
def delete_tunnel(config):
|
||||
credentials_path = config["credentials_file"]
|
||||
delete_cmd = [config["cloudflared_binary"], "tunnel", "--origincert", config["origincert"], "delete",
|
||||
"--credentials-file", credentials_path, "-f", config["tunnel"]]
|
||||
LOGGER.info(f"Deleting tunnel with {delete_cmd}")
|
||||
subprocess.run(delete_cmd, check=True)
|
||||
|
||||
|
||||
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
|
||||
def create_dns(config, hostname, type, content):
|
||||
cf = CloudFlare.CloudFlare(debug=True, token=get_env("DNS_API_TOKEN"))
|
||||
cf.zones.dns_records.post(
|
||||
config["zone_tag"],
|
||||
data={'name': hostname, 'type': type, 'content': content, 'proxied': True}
|
||||
)
|
||||
|
||||
|
||||
def create_classic_dns(config, random_uuid):
|
||||
classic_hostname = "classic-" + random_uuid + "." + config["zone_domain"]
|
||||
create_dns(config, classic_hostname, "AAAA", "fd10:aec2:5dae::")
|
||||
return classic_hostname
|
||||
|
||||
|
||||
def create_named_dns(config, random_uuid):
|
||||
hostname = "named-" + random_uuid + "." + config["zone_domain"]
|
||||
create_dns(config, hostname, "CNAME", config["tunnel"] + ".cfargotunnel.com")
|
||||
return hostname
|
||||
|
||||
|
||||
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
|
||||
def delete_dns(config, hostname):
|
||||
cf = CloudFlare.CloudFlare(debug=True, token=get_env("DNS_API_TOKEN"))
|
||||
zone_tag = config["zone_tag"]
|
||||
dns_records = cf.zones.dns_records.get(zone_tag, params={'name': hostname})
|
||||
if len(dns_records) > 0:
|
||||
cf.zones.dns_records.delete(zone_tag, dns_records[0]['id'])
|
||||
|
||||
|
||||
def write_file(content, path):
|
||||
with open(path, 'w') as outfile:
|
||||
outfile.write(content)
|
||||
|
||||
|
||||
def get_env(env_name):
|
||||
val = os.getenv(env_name)
|
||||
if val is None:
|
||||
raise Exception(f"{env_name} is not set")
|
||||
return val
|
||||
|
||||
|
||||
def create():
|
||||
"""
|
||||
Creates the necessary resources for the components test to run.
|
||||
- Creates a named tunnel with a random name.
|
||||
- Creates a random CNAME DNS entry for that tunnel.
|
||||
- Creates a random AAAA DNS entry for a classic tunnel.
|
||||
|
||||
Those created resources are added to the config (obtained from an environment variable).
|
||||
The resulting configuration is persisted for the tests to use.
|
||||
"""
|
||||
config = get_config_from_env()
|
||||
origincert_path = persist_origin_cert(config)
|
||||
|
||||
random_uuid = str(uuid.uuid4())
|
||||
config["tunnel"] = create_tunnel(config, origincert_path, random_uuid)
|
||||
config["classic_hostname"] = create_classic_dns(config, random_uuid)
|
||||
config["ingress"] = [
|
||||
{
|
||||
"hostname": create_named_dns(config, random_uuid),
|
||||
"service": "hello_world"
|
||||
},
|
||||
{
|
||||
"service": "http_status:404"
|
||||
}
|
||||
]
|
||||
|
||||
persist_config(config)
|
||||
|
||||
|
||||
def cleanup():
|
||||
"""
|
||||
Reads the persisted configuration that was created previously.
|
||||
Deletes the resources that were created there.
|
||||
"""
|
||||
config = get_config_from_file()
|
||||
delete_tunnel(config)
|
||||
delete_dns(config, config["classic_hostname"])
|
||||
delete_dns(config, config["ingress"][0]["hostname"])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description='setup component tests')
|
||||
parser.add_argument('--type', choices=['create', 'cleanup'], default='create')
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.type == 'create':
|
||||
create()
|
||||
else:
|
||||
cleanup()
|
||||
@@ -1,24 +1,74 @@
|
||||
#!/usr/bin/env python
|
||||
import json
|
||||
import os
|
||||
from util import start_cloudflared, wait_tunnel_ready, send_requests, LOGGER
|
||||
|
||||
from util import start_cloudflared, wait_tunnel_ready, send_requests
|
||||
|
||||
# Rolling logger rotate log files after 1 MB
|
||||
rotate_after_size = 1000 * 1000
|
||||
default_log_file = "cloudflared.log"
|
||||
expect_message = "Starting Hello"
|
||||
|
||||
|
||||
def assert_log_to_terminal(cloudflared):
|
||||
stderr = cloudflared.stderr.read(1500)
|
||||
assert expect_message.encode() in stderr, f"{stderr} doesn't contain {expect_message}"
|
||||
|
||||
|
||||
def assert_log_in_file(file):
|
||||
with open(file, "r") as f:
|
||||
log = f.read(2000)
|
||||
assert expect_message in log, f"{log} doesn't contain {expect_message}"
|
||||
|
||||
|
||||
def assert_json_log(file):
|
||||
def assert_in_json(j, key):
|
||||
assert key in j, f"{key} is not in j"
|
||||
|
||||
with open(file, "r") as f:
|
||||
line = f.readline()
|
||||
json_log = json.loads(line)
|
||||
assert_in_json(json_log, "level")
|
||||
assert_in_json(json_log, "time")
|
||||
assert_in_json(json_log, "message")
|
||||
|
||||
|
||||
def assert_log_to_dir(config, log_dir):
|
||||
max_batches = 3
|
||||
batch_requests = 1000
|
||||
for _ in range(max_batches):
|
||||
send_requests(config.get_url(),
|
||||
batch_requests, require_ok=False)
|
||||
files = os.listdir(log_dir)
|
||||
if len(files) == 2:
|
||||
current_log_file_index = files.index(default_log_file)
|
||||
current_file = log_dir / files[current_log_file_index]
|
||||
stats = os.stat(current_file)
|
||||
assert stats.st_size > 0
|
||||
assert_json_log(current_file)
|
||||
|
||||
# One file is the current log file, the other is the rotated log file
|
||||
rotated_log_file_index = 0 if current_log_file_index == 1 else 1
|
||||
rotated_file = log_dir / files[rotated_log_file_index]
|
||||
stats = os.stat(rotated_file)
|
||||
assert stats.st_size > rotate_after_size
|
||||
assert_log_in_file(rotated_file)
|
||||
assert_json_log(current_file)
|
||||
return
|
||||
|
||||
raise Exception(
|
||||
f"Log file isn't rotated after sending {max_batches * batch_requests} requests")
|
||||
|
||||
|
||||
class TestLogging:
|
||||
# TODO: Test logging when running as a service https://jira.cfops.it/browse/TUN-4082
|
||||
# Rolling logger rotate log files after 1 MB
|
||||
rotate_after_size = 1000 * 1000
|
||||
default_log_file = "cloudflared.log"
|
||||
expect_message = "Starting tunnel"
|
||||
|
||||
def test_logging_to_terminal(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config()
|
||||
with start_cloudflared(tmp_path, config, new_process=True) as cloudflared:
|
||||
wait_tunnel_ready(tunnel_url=config.get_url())
|
||||
self.assert_log_to_terminal(cloudflared)
|
||||
assert_log_to_terminal(cloudflared)
|
||||
|
||||
def test_logging_to_file(self, tmp_path, component_tests_config):
|
||||
log_file = tmp_path / self.default_log_file
|
||||
log_file = tmp_path / default_log_file
|
||||
extra_config = {
|
||||
# Convert from pathlib.Path to str
|
||||
"logfile": str(log_file),
|
||||
@@ -26,8 +76,8 @@ class TestLogging:
|
||||
config = component_tests_config(extra_config)
|
||||
with start_cloudflared(tmp_path, config, new_process=True, capture_output=False):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url())
|
||||
self.assert_log_in_file(log_file)
|
||||
self.assert_json_log(log_file)
|
||||
assert_log_in_file(log_file)
|
||||
assert_json_log(log_file)
|
||||
|
||||
def test_logging_to_dir(self, tmp_path, component_tests_config):
|
||||
log_dir = tmp_path / "logs"
|
||||
@@ -39,55 +89,4 @@ class TestLogging:
|
||||
config = component_tests_config(extra_config)
|
||||
with start_cloudflared(tmp_path, config, new_process=True, capture_output=False):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url())
|
||||
self.assert_log_to_dir(config, log_dir)
|
||||
|
||||
def assert_log_to_terminal(self, cloudflared):
|
||||
stderr = cloudflared.stderr.read(200)
|
||||
# cloudflared logs the following when it first starts
|
||||
# 2021-03-10T12:30:39Z INF Starting tunnel tunnelID=<tunnel ID>
|
||||
assert self.expect_message.encode(
|
||||
) in stderr, f"{stderr} doesn't contain {self.expect_message}"
|
||||
|
||||
def assert_log_in_file(self, file, expect_message=""):
|
||||
with open(file, "r") as f:
|
||||
log = f.read(200)
|
||||
# cloudflared logs the following when it first starts
|
||||
# {"level":"info","tunnelID":"<tunnel ID>","time":"2021-03-10T12:21:13Z","message":"Starting tunnel"}
|
||||
assert self.expect_message in log, f"{log} doesn't contain {self.expect_message}"
|
||||
|
||||
def assert_json_log(self, file):
|
||||
with open(file, "r") as f:
|
||||
line = f.readline()
|
||||
json_log = json.loads(line)
|
||||
self.assert_in_json(json_log, "level")
|
||||
self.assert_in_json(json_log, "time")
|
||||
self.assert_in_json(json_log, "message")
|
||||
|
||||
def assert_in_json(self, j, key):
|
||||
assert key in j, f"{key} is not in j"
|
||||
|
||||
def assert_log_to_dir(self, config, log_dir):
|
||||
max_batches = 3
|
||||
batch_requests = 1000
|
||||
for _ in range(max_batches):
|
||||
send_requests(config.get_url(),
|
||||
batch_requests, require_ok=False)
|
||||
files = os.listdir(log_dir)
|
||||
if len(files) == 2:
|
||||
current_log_file_index = files.index(self.default_log_file)
|
||||
current_file = log_dir / files[current_log_file_index]
|
||||
stats = os.stat(current_file)
|
||||
assert stats.st_size > 0
|
||||
self.assert_json_log(current_file)
|
||||
|
||||
# One file is the current log file, the other is the rotated log file
|
||||
rotated_log_file_index = 0 if current_log_file_index == 1 else 1
|
||||
rotated_file = log_dir / files[rotated_log_file_index]
|
||||
stats = os.stat(rotated_file)
|
||||
assert stats.st_size > self.rotate_after_size
|
||||
self.assert_log_in_file(rotated_file)
|
||||
self.assert_json_log(current_file)
|
||||
return
|
||||
|
||||
raise Exception(
|
||||
f"Log file isn't rotated after sending {max_batches * batch_requests} requests")
|
||||
assert_log_to_dir(config, log_dir)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python
|
||||
import socket
|
||||
from time import sleep
|
||||
|
||||
import constants
|
||||
from conftest import CfdModes
|
||||
from util import start_cloudflared, wait_tunnel_ready, check_tunnel_not_connected
|
||||
|
||||
|
||||
# Sanity checks that test that we only run Proxy DNS and Tunnel when we really expect them to be there.
|
||||
class TestProxyDns:
|
||||
def test_proxy_dns_with_named_tunnel(self, tmp_path, component_tests_config):
|
||||
run_test_scenario(tmp_path, component_tests_config, CfdModes.NAMED, run_proxy_dns=True)
|
||||
|
||||
def test_proxy_dns_with_classic_tunnel(self, tmp_path, component_tests_config):
|
||||
run_test_scenario(tmp_path, component_tests_config, CfdModes.CLASSIC, run_proxy_dns=True)
|
||||
|
||||
def test_proxy_dns_alone(self, tmp_path, component_tests_config):
|
||||
run_test_scenario(tmp_path, component_tests_config, CfdModes.PROXY_DNS, run_proxy_dns=True)
|
||||
|
||||
def test_named_tunnel_alone(self, tmp_path, component_tests_config):
|
||||
run_test_scenario(tmp_path, component_tests_config, CfdModes.NAMED, run_proxy_dns=False)
|
||||
|
||||
def test_classic_tunnel_alone(self, tmp_path, component_tests_config):
|
||||
run_test_scenario(tmp_path, component_tests_config, CfdModes.CLASSIC, run_proxy_dns=False)
|
||||
|
||||
|
||||
def run_test_scenario(tmp_path, component_tests_config, cfd_mode, run_proxy_dns):
|
||||
expect_proxy_dns = run_proxy_dns
|
||||
expect_tunnel = False
|
||||
|
||||
if cfd_mode == CfdModes.NAMED:
|
||||
expect_tunnel = True
|
||||
pre_args = ["tunnel"]
|
||||
args = ["run"]
|
||||
elif cfd_mode == CfdModes.CLASSIC:
|
||||
expect_tunnel = True
|
||||
pre_args = []
|
||||
args = []
|
||||
elif cfd_mode == CfdModes.PROXY_DNS:
|
||||
expect_proxy_dns = True
|
||||
pre_args = []
|
||||
args = ["proxy-dns", "--port", str(constants.PROXY_DNS_PORT)]
|
||||
else:
|
||||
assert False, f"Unknown cfd_mode {cfd_mode}"
|
||||
|
||||
config = component_tests_config(cfd_mode=cfd_mode, run_proxy_dns=run_proxy_dns)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=pre_args, cfd_args=args, new_process=True, capture_output=False):
|
||||
if expect_tunnel:
|
||||
wait_tunnel_ready()
|
||||
else:
|
||||
check_tunnel_not_connected()
|
||||
verify_proxy_dns(expect_proxy_dns)
|
||||
|
||||
|
||||
def verify_proxy_dns(should_be_running):
|
||||
# Wait for the Proxy DNS listener to come up.
|
||||
sleep(constants.BACKOFF_SECS)
|
||||
had_failure = False
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
sock.connect(('localhost', constants.PROXY_DNS_PORT))
|
||||
sock.send(b"anything")
|
||||
except:
|
||||
if should_be_running:
|
||||
assert False, "Expected Proxy DNS to be running, but it was not."
|
||||
had_failure = True
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
if not should_be_running and not had_failure:
|
||||
assert False, "Proxy DNS should not have been running, but it was."
|
||||
@@ -1,32 +1,35 @@
|
||||
#!/usr/bin/env python
|
||||
import copy
|
||||
from flaky import flaky
|
||||
from retrying import retry
|
||||
import platform
|
||||
from time import sleep
|
||||
|
||||
from util import start_cloudflared, wait_tunnel_ready, check_tunnel_not_connected, send_requests
|
||||
import pytest
|
||||
from flaky import flaky
|
||||
|
||||
from conftest import CfdModes
|
||||
from util import start_cloudflared, wait_tunnel_ready, check_tunnel_not_connected
|
||||
|
||||
|
||||
@flaky(max_runs=3, min_passes=1)
|
||||
class TestReconnect():
|
||||
class TestReconnect:
|
||||
default_ha_conns = 4
|
||||
default_reconnect_secs = 5
|
||||
default_reconnect_secs = 15
|
||||
extra_config = {
|
||||
"stdin-control": True,
|
||||
}
|
||||
|
||||
@pytest.mark.skipif(platform.system() == "Windows", reason=f"Currently buggy on Windows TUN-4584")
|
||||
def test_named_reconnect(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config(self.extra_config)
|
||||
with start_cloudflared(tmp_path, config, new_process=True, allow_input=True) as cloudflared:
|
||||
with start_cloudflared(tmp_path, config, new_process=True, allow_input=True, capture_output=False) as cloudflared:
|
||||
# Repeat the test multiple times because some issues only occur after multiple reconnects
|
||||
self.assert_reconnect(config, cloudflared, 5)
|
||||
|
||||
def test_classic_reconnect(self, tmp_path, component_tests_config):
|
||||
extra_config = copy.copy(self.extra_config)
|
||||
extra_config["hello-world"] = True
|
||||
config = component_tests_config(
|
||||
additional_config=extra_config, named_tunnel=False)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=[], new_process=True, allow_input=True) as cloudflared:
|
||||
config = component_tests_config(additional_config=extra_config, cfd_mode=CfdModes.CLASSIC)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=[], new_process=True, allow_input=True, capture_output=False) as cloudflared:
|
||||
self.assert_reconnect(config, cloudflared, 1)
|
||||
|
||||
def send_reconnect(self, cloudflared, secs):
|
||||
@@ -36,7 +39,7 @@ class TestReconnect():
|
||||
cloudflared.stdin.flush()
|
||||
|
||||
def assert_reconnect(self, config, cloudflared, repeat):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url())
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=self.default_ha_conns)
|
||||
for _ in range(repeat):
|
||||
for i in range(self.default_ha_conns):
|
||||
self.send_reconnect(cloudflared, self.default_reconnect_secs)
|
||||
@@ -44,11 +47,9 @@ class TestReconnect():
|
||||
if expect_connections > 0:
|
||||
# Don't check if tunnel returns 200 here because there is a race condition between wait_tunnel_ready
|
||||
# retrying to get 200 response and reconnecting
|
||||
wait_tunnel_ready(
|
||||
require_min_connections=expect_connections)
|
||||
wait_tunnel_ready(require_min_connections=expect_connections)
|
||||
else:
|
||||
check_tunnel_not_connected()
|
||||
|
||||
sleep(self.default_reconnect_secs + 10)
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(),
|
||||
require_min_connections=self.default_ha_conns)
|
||||
sleep(self.default_reconnect_secs * 2)
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=self.default_ha_conns)
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
#!/usr/bin/env python
|
||||
from contextlib import contextmanager
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import pytest
|
||||
import subprocess
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
|
||||
from util import start_cloudflared, cloudflared_cmd, wait_tunnel_ready, LOGGER
|
||||
import pytest
|
||||
|
||||
import test_logging
|
||||
from conftest import CfdModes
|
||||
from util import start_cloudflared, wait_tunnel_ready
|
||||
|
||||
|
||||
def select_platform(plat):
|
||||
@@ -22,34 +25,94 @@ def default_config_file():
|
||||
return os.path.join(default_config_dir(), "config.yml")
|
||||
|
||||
|
||||
class TestServiceMode():
|
||||
class TestServiceMode:
|
||||
@select_platform("Darwin")
|
||||
@pytest.mark.skipif(os.path.exists(default_config_file()), reason=f"There is already a config file in default path")
|
||||
def test_launchd_service(self, component_tests_config):
|
||||
# On Darwin cloudflared service defaults to run classic tunnel command
|
||||
def test_launchd_service_log_to_file(self, tmp_path, component_tests_config):
|
||||
log_file = tmp_path / test_logging.default_log_file
|
||||
additional_config = {
|
||||
# On Darwin cloudflared service defaults to run classic tunnel command
|
||||
"hello-world": True,
|
||||
"logfile": str(log_file),
|
||||
}
|
||||
config = component_tests_config(
|
||||
additional_config=additional_config, named_tunnel=False)
|
||||
config = component_tests_config(additional_config=additional_config, cfd_mode=CfdModes.CLASSIC)
|
||||
|
||||
def assert_log_file():
|
||||
test_logging.assert_log_in_file(log_file)
|
||||
test_logging.assert_json_log(log_file)
|
||||
|
||||
self.launchd_service_scenario(config, assert_log_file)
|
||||
|
||||
@select_platform("Darwin")
|
||||
@pytest.mark.skipif(os.path.exists(default_config_file()), reason=f"There is already a config file in default path")
|
||||
def test_launchd_service_rotating_log(self, tmp_path, component_tests_config):
|
||||
log_dir = tmp_path / "logs"
|
||||
additional_config = {
|
||||
# On Darwin cloudflared service defaults to run classic tunnel command
|
||||
"hello-world": True,
|
||||
"loglevel": "debug",
|
||||
"log-directory": str(log_dir),
|
||||
}
|
||||
config = component_tests_config(additional_config=additional_config, cfd_mode=CfdModes.CLASSIC)
|
||||
|
||||
def assert_rotating_log():
|
||||
test_logging.assert_log_to_dir(config, log_dir)
|
||||
|
||||
self.launchd_service_scenario(config, assert_rotating_log)
|
||||
|
||||
def launchd_service_scenario(self, config, extra_assertions):
|
||||
with self.run_service(Path(default_config_dir()), config):
|
||||
self.launchctl_cmd("list")
|
||||
self.launchctl_cmd("start")
|
||||
wait_tunnel_ready(tunnel_url=config.get_url())
|
||||
extra_assertions()
|
||||
self.launchctl_cmd("stop")
|
||||
|
||||
os.remove(default_config_file())
|
||||
self.launchctl_cmd("list", success=False)
|
||||
|
||||
@select_platform("Linux")
|
||||
@pytest.mark.skipif(os.path.exists("/etc/cloudflared/config.yml"), reason=f"There is already a config file in default path")
|
||||
def test_sysv_service(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config()
|
||||
@pytest.mark.skipif(os.path.exists("/etc/cloudflared/config.yml"),
|
||||
reason=f"There is already a config file in default path")
|
||||
def test_sysv_service_log_to_file(self, tmp_path, component_tests_config):
|
||||
log_file = tmp_path / test_logging.default_log_file
|
||||
additional_config = {
|
||||
"logfile": str(log_file),
|
||||
}
|
||||
config = component_tests_config(additional_config=additional_config)
|
||||
|
||||
def assert_log_file():
|
||||
test_logging.assert_log_in_file(log_file)
|
||||
test_logging.assert_json_log(log_file)
|
||||
|
||||
self.sysv_service_scenario(config, tmp_path, assert_log_file)
|
||||
|
||||
@select_platform("Linux")
|
||||
@pytest.mark.skipif(os.path.exists("/etc/cloudflared/config.yml"),
|
||||
reason=f"There is already a config file in default path")
|
||||
def test_sysv_service_rotating_log(self, tmp_path, component_tests_config):
|
||||
log_dir = tmp_path / "logs"
|
||||
additional_config = {
|
||||
"loglevel": "debug",
|
||||
"log-directory": str(log_dir),
|
||||
}
|
||||
config = component_tests_config(additional_config=additional_config)
|
||||
|
||||
def assert_rotating_log():
|
||||
# We need the folder to have executable permissions for the "stat" command in the assertions to work.
|
||||
subprocess.check_call(['sudo', 'chmod', 'o+x', log_dir])
|
||||
test_logging.assert_log_to_dir(config, log_dir)
|
||||
|
||||
self.sysv_service_scenario(config, tmp_path, assert_rotating_log)
|
||||
|
||||
def sysv_service_scenario(self, config, tmp_path, extra_assertions):
|
||||
with self.run_service(tmp_path, config, root=True):
|
||||
self.sysv_cmd("start")
|
||||
self.sysv_cmd("status")
|
||||
wait_tunnel_ready(tunnel_url=config.get_url())
|
||||
extra_assertions()
|
||||
self.sysv_cmd("stop")
|
||||
|
||||
# Service install copies config file to /etc/cloudflared/config.yml
|
||||
subprocess.run(["sudo", "rm", "/etc/cloudflared/config.yml"])
|
||||
self.sysv_cmd("status", success=False)
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
#!/usr/bin/env python
|
||||
from contextlib import contextmanager
|
||||
import requests
|
||||
import platform
|
||||
import signal
|
||||
import threading
|
||||
import time
|
||||
|
||||
from util import start_cloudflared, wait_tunnel_ready, check_tunnel_not_connected, LOGGER
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from util import start_cloudflared, wait_tunnel_ready, check_tunnel_not_connected
|
||||
|
||||
|
||||
def supported_signals():
|
||||
if platform.system() == "Windows":
|
||||
return [signal.SIGTERM]
|
||||
return [signal.SIGTERM, signal.SIGINT]
|
||||
|
||||
|
||||
class TestTermination():
|
||||
@@ -14,57 +23,56 @@ class TestTermination():
|
||||
extra_config = {
|
||||
"grace-period": f"{grace_period}s",
|
||||
}
|
||||
signals = [signal.SIGTERM, signal.SIGINT]
|
||||
sse_endpoint = "/sse?freq=1s"
|
||||
|
||||
def test_graceful_shutdown(self, tmp_path, component_tests_config):
|
||||
@pytest.mark.parametrize("signal", supported_signals())
|
||||
def test_graceful_shutdown(self, tmp_path, component_tests_config, signal):
|
||||
config = component_tests_config(self.extra_config)
|
||||
for sig in self.signals:
|
||||
with start_cloudflared(
|
||||
tmp_path, config, new_process=True, capture_output=False) as cloudflared:
|
||||
wait_tunnel_ready(tunnel_url=config.get_url())
|
||||
with start_cloudflared(
|
||||
tmp_path, config, new_process=True, capture_output=False) as cloudflared:
|
||||
wait_tunnel_ready(tunnel_url=config.get_url())
|
||||
|
||||
connected = threading.Condition()
|
||||
in_flight_req = threading.Thread(
|
||||
target=self.stream_request, args=(config, connected,))
|
||||
in_flight_req.start()
|
||||
connected = threading.Condition()
|
||||
in_flight_req = threading.Thread(
|
||||
target=self.stream_request, args=(config, connected, False, ))
|
||||
in_flight_req.start()
|
||||
|
||||
with connected:
|
||||
connected.wait(self.timeout)
|
||||
# Send signal after the SSE connection is established
|
||||
self.terminate_by_signal(cloudflared, sig)
|
||||
self.wait_eyeball_thread(
|
||||
in_flight_req, self.grace_period + self.timeout)
|
||||
with connected:
|
||||
connected.wait(self.timeout)
|
||||
# Send signal after the SSE connection is established
|
||||
self.terminate_by_signal(cloudflared, signal)
|
||||
self.wait_eyeball_thread(
|
||||
in_flight_req, self.grace_period + self.timeout)
|
||||
|
||||
# test cloudflared terminates before grace period expires when all eyeball
|
||||
# connections are drained
|
||||
def test_shutdown_once_no_connection(self, tmp_path, component_tests_config):
|
||||
@pytest.mark.parametrize("signal", supported_signals())
|
||||
def test_shutdown_once_no_connection(self, tmp_path, component_tests_config, signal):
|
||||
config = component_tests_config(self.extra_config)
|
||||
for sig in self.signals:
|
||||
with start_cloudflared(
|
||||
tmp_path, config, new_process=True, capture_output=False) as cloudflared:
|
||||
wait_tunnel_ready(tunnel_url=config.get_url())
|
||||
with start_cloudflared(
|
||||
tmp_path, config, new_process=True, capture_output=False) as cloudflared:
|
||||
wait_tunnel_ready(tunnel_url=config.get_url())
|
||||
|
||||
connected = threading.Condition()
|
||||
in_flight_req = threading.Thread(
|
||||
target=self.stream_request, args=(config, connected, True, ))
|
||||
in_flight_req.start()
|
||||
connected = threading.Condition()
|
||||
in_flight_req = threading.Thread(
|
||||
target=self.stream_request, args=(config, connected, True, ))
|
||||
in_flight_req.start()
|
||||
|
||||
with connected:
|
||||
connected.wait(self.timeout)
|
||||
with self.within_grace_period():
|
||||
# Send signal after the SSE connection is established
|
||||
self.terminate_by_signal(cloudflared, sig)
|
||||
self.wait_eyeball_thread(in_flight_req, self.grace_period)
|
||||
with connected:
|
||||
connected.wait(self.timeout)
|
||||
with self.within_grace_period():
|
||||
# Send signal after the SSE connection is established
|
||||
self.terminate_by_signal(cloudflared, signal)
|
||||
self.wait_eyeball_thread(in_flight_req, self.grace_period)
|
||||
|
||||
def test_no_connection_shutdown(self, tmp_path, component_tests_config):
|
||||
@pytest.mark.parametrize("signal", supported_signals())
|
||||
def test_no_connection_shutdown(self, tmp_path, component_tests_config, signal):
|
||||
config = component_tests_config(self.extra_config)
|
||||
for sig in self.signals:
|
||||
with start_cloudflared(
|
||||
tmp_path, config, new_process=True, capture_output=False) as cloudflared:
|
||||
wait_tunnel_ready(tunnel_url=config.get_url())
|
||||
with self.within_grace_period():
|
||||
self.terminate_by_signal(cloudflared, sig)
|
||||
with start_cloudflared(
|
||||
tmp_path, config, new_process=True, capture_output=False) as cloudflared:
|
||||
wait_tunnel_ready(tunnel_url=config.get_url())
|
||||
with self.within_grace_period():
|
||||
self.terminate_by_signal(cloudflared, signal)
|
||||
|
||||
def terminate_by_signal(self, cloudflared, sig):
|
||||
cloudflared.send_signal(sig)
|
||||
|
||||
@@ -34,7 +34,7 @@ def cloudflared_cmd(config, config_path, cfd_args, cfd_pre_args, root):
|
||||
cmd += ["sudo"]
|
||||
cmd += [config.cloudflared_binary]
|
||||
cmd += cfd_pre_args
|
||||
cmd += ["--config", config_path]
|
||||
cmd += ["--config", str(config_path)]
|
||||
cmd += cfd_args
|
||||
LOGGER.info(f"Run cmd {cmd} with config {config}")
|
||||
return cmd
|
||||
@@ -50,8 +50,7 @@ def run_cloudflared_background(cmd, allow_input, capture_output):
|
||||
finally:
|
||||
cfd.terminate()
|
||||
if capture_output:
|
||||
LOGGER.info(
|
||||
f"cloudflared log: {cfd.stderr.read()}")
|
||||
LOGGER.info(f"cloudflared log: {cfd.stderr.read()}")
|
||||
|
||||
|
||||
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
|
||||
|
||||
+20
-9
@@ -9,11 +9,11 @@ import (
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gopkg.in/yaml.v2"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
)
|
||||
@@ -358,13 +358,13 @@ func GetConfiguration() *Configuration {
|
||||
// ReadConfigFile returns InputSourceContext initialized from the configuration file.
|
||||
// On repeat calls returns with the same file, returns without reading the file again; however,
|
||||
// if value of "config" flag changes, will read the new config file
|
||||
func ReadConfigFile(c *cli.Context, log *zerolog.Logger) (*configFileSettings, error) {
|
||||
func ReadConfigFile(c *cli.Context, log *zerolog.Logger) (settings *configFileSettings, warnings string, err error) {
|
||||
configFile := c.String("config")
|
||||
if configuration.Source() == configFile || configFile == "" {
|
||||
if configuration.Source() == "" {
|
||||
return nil, ErrNoConfigFile
|
||||
return nil, "", ErrNoConfigFile
|
||||
}
|
||||
return &configuration, nil
|
||||
return &configuration, "", nil
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Loading configuration from %s", configFile)
|
||||
@@ -373,16 +373,27 @@ func ReadConfigFile(c *cli.Context, log *zerolog.Logger) (*configFileSettings, e
|
||||
if os.IsNotExist(err) {
|
||||
err = ErrNoConfigFile
|
||||
}
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
defer file.Close()
|
||||
if err := yaml.NewDecoder(file).Decode(&configuration); err != nil {
|
||||
if err == io.EOF {
|
||||
log.Error().Msgf("Configuration file %s was empty", configFile)
|
||||
return &configuration, nil
|
||||
return &configuration, "", nil
|
||||
}
|
||||
return nil, errors.Wrap(err, "error parsing YAML in config file at "+configFile)
|
||||
return nil, "", errors.Wrap(err, "error parsing YAML in config file at "+configFile)
|
||||
}
|
||||
configuration.sourceFile = configFile
|
||||
return &configuration, nil
|
||||
|
||||
// Parse it again, with strict mode, to find warnings.
|
||||
if file, err := os.Open(configFile); err == nil {
|
||||
decoder := yaml.NewDecoder(file)
|
||||
decoder.SetStrict(true)
|
||||
var unusedConfig configFileSettings
|
||||
if err := decoder.Decode(&unusedConfig); err != nil {
|
||||
warnings = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
return &configuration, warnings, nil
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/yaml.v2"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func TestConfigFileSettings(t *testing.T) {
|
||||
|
||||
+3
-3
@@ -4,11 +4,11 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/cloudflare/cloudflared/watcher"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"gopkg.in/yaml.v2"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/watcher"
|
||||
)
|
||||
|
||||
// Notifier sends out config updates
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/watcher"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/watcher"
|
||||
)
|
||||
|
||||
type mockNotifier struct {
|
||||
|
||||
+79
-10
@@ -1,6 +1,7 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -8,11 +9,19 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
)
|
||||
|
||||
const LogFieldConnIndex = "connIndex"
|
||||
const (
|
||||
lbProbeUserAgentPrefix = "Mozilla/5.0 (compatible; Cloudflare-Traffic-Manager/1.0; +https://www.cloudflare.com/traffic-manager/;"
|
||||
LogFieldConnIndex = "connIndex"
|
||||
MaxGracePeriod = time.Minute * 3
|
||||
)
|
||||
|
||||
var switchingProtocolText = fmt.Sprintf("%d %s", http.StatusSwitchingProtocols, http.StatusText(http.StatusSwitchingProtocols))
|
||||
|
||||
type Config struct {
|
||||
OriginProxy OriginProxy
|
||||
@@ -21,8 +30,9 @@ type Config struct {
|
||||
}
|
||||
|
||||
type NamedTunnelConfig struct {
|
||||
Credentials Credentials
|
||||
Client pogs.ClientInfo
|
||||
Credentials Credentials
|
||||
Client pogs.ClientInfo
|
||||
QuickTunnelUrl string
|
||||
}
|
||||
|
||||
// Credentials are stored in the credentials file and contain all info needed to run a tunnel.
|
||||
@@ -47,10 +57,6 @@ type ClassicTunnelConfig struct {
|
||||
UseReconnectToken bool
|
||||
}
|
||||
|
||||
func (c *ClassicTunnelConfig) IsTrialZone() bool {
|
||||
return c.Hostname == ""
|
||||
}
|
||||
|
||||
// Type indicates the connection type of the connection.
|
||||
type Type int
|
||||
|
||||
@@ -86,9 +92,64 @@ func (t Type) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
// OriginProxy is how data flows from cloudflared to the origin services running behind it.
|
||||
type OriginProxy interface {
|
||||
// If Proxy returns an error, the caller is responsible for writing the error status to ResponseWriter
|
||||
Proxy(w ResponseWriter, req *http.Request, sourceConnectionType Type) error
|
||||
ProxyHTTP(w ResponseWriter, req *http.Request, isWebsocket bool) error
|
||||
ProxyTCP(ctx context.Context, rwa ReadWriteAcker, req *TCPRequest) error
|
||||
}
|
||||
|
||||
// TCPRequest defines the input format needed to perform a TCP proxy.
|
||||
type TCPRequest struct {
|
||||
Dest string
|
||||
CFRay string
|
||||
LBProbe bool
|
||||
}
|
||||
|
||||
// ReadWriteAcker is a readwriter with the ability to Acknowledge to the downstream (edge) that the origin has
|
||||
// accepted the connection.
|
||||
type ReadWriteAcker interface {
|
||||
io.ReadWriter
|
||||
AckConnection() error
|
||||
}
|
||||
|
||||
// HTTPResponseReadWriteAcker is an HTTP implementation of ReadWriteAcker.
|
||||
type HTTPResponseReadWriteAcker struct {
|
||||
r io.Reader
|
||||
w ResponseWriter
|
||||
req *http.Request
|
||||
}
|
||||
|
||||
// NewHTTPResponseReadWriterAcker returns a new instance of HTTPResponseReadWriteAcker.
|
||||
func NewHTTPResponseReadWriterAcker(w ResponseWriter, req *http.Request) *HTTPResponseReadWriteAcker {
|
||||
return &HTTPResponseReadWriteAcker{
|
||||
r: req.Body,
|
||||
w: w,
|
||||
req: req,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HTTPResponseReadWriteAcker) Read(p []byte) (int, error) {
|
||||
return h.r.Read(p)
|
||||
}
|
||||
|
||||
func (h *HTTPResponseReadWriteAcker) Write(p []byte) (int, error) {
|
||||
return h.w.Write(p)
|
||||
}
|
||||
|
||||
// AckConnection acks an HTTP connection by sending a switch protocols status code that enables the caller to
|
||||
// upgrade to streams.
|
||||
func (h *HTTPResponseReadWriteAcker) AckConnection() error {
|
||||
resp := &http.Response{
|
||||
Status: switchingProtocolText,
|
||||
StatusCode: http.StatusSwitchingProtocols,
|
||||
ContentLength: -1,
|
||||
}
|
||||
|
||||
if secWebsocketKey := h.req.Header.Get("Sec-WebSocket-Key"); secWebsocketKey != "" {
|
||||
resp.Header = websocket.NewResponseHeader(h.req)
|
||||
}
|
||||
|
||||
return h.w.WriteRespHeaders(resp.StatusCode, resp.Header)
|
||||
}
|
||||
|
||||
type ResponseWriter interface {
|
||||
@@ -111,3 +172,11 @@ func IsServerSentEvent(headers http.Header) bool {
|
||||
func uint8ToString(input uint8) string {
|
||||
return strconv.FormatUint(uint64(input), 10)
|
||||
}
|
||||
|
||||
func FindCfRayHeader(req *http.Request) string {
|
||||
return req.Header.Get("Cf-Ray")
|
||||
}
|
||||
|
||||
func IsLBProbeRequest(req *http.Request) bool {
|
||||
return strings.HasPrefix(req.UserAgent(), lbProbeUserAgentPrefix)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -11,6 +12,8 @@ import (
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -18,7 +21,8 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
testConfig = &Config{
|
||||
unusedWarpRoutingService = (*ingress.WarpRoutingService)(nil)
|
||||
testConfig = &Config{
|
||||
OriginProxy: &mockOriginProxy{},
|
||||
GracePeriod: time.Millisecond * 100,
|
||||
}
|
||||
@@ -38,14 +42,17 @@ type testRequest struct {
|
||||
isProxyError bool
|
||||
}
|
||||
|
||||
type mockOriginProxy struct {
|
||||
}
|
||||
type mockOriginProxy struct{}
|
||||
|
||||
func (moc *mockOriginProxy) Proxy(w ResponseWriter, r *http.Request, sourceConnectionType Type) error {
|
||||
if sourceConnectionType == TypeWebsocket {
|
||||
return wsEndpoint(w, r)
|
||||
func (moc *mockOriginProxy) ProxyHTTP(
|
||||
w ResponseWriter,
|
||||
req *http.Request,
|
||||
isWebsocket bool,
|
||||
) error {
|
||||
if isWebsocket {
|
||||
return wsEndpoint(w, req)
|
||||
}
|
||||
switch r.URL.Path {
|
||||
switch req.URL.Path {
|
||||
case "/ok":
|
||||
originRespEndpoint(w, http.StatusOK, []byte(http.StatusText(http.StatusOK)))
|
||||
case "/large_file":
|
||||
@@ -60,6 +67,15 @@ func (moc *mockOriginProxy) Proxy(w ResponseWriter, r *http.Request, sourceConne
|
||||
originRespEndpoint(w, http.StatusNotFound, []byte("page not found"))
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (moc *mockOriginProxy) ProxyTCP(
|
||||
ctx context.Context,
|
||||
rwa ReadWriteAcker,
|
||||
r *TCPRequest,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type nowriter struct {
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
// RPCClientFunc derives a named tunnel rpc client that can then be used to register and unregister connections.
|
||||
type RPCClientFunc func(context.Context, io.ReadWriteCloser, *zerolog.Logger) NamedTunnelRPCClient
|
||||
|
||||
type controlStream struct {
|
||||
observer *Observer
|
||||
|
||||
connectedFuse ConnectedFuse
|
||||
namedTunnelConfig *NamedTunnelConfig
|
||||
connIndex uint8
|
||||
|
||||
newRPCClientFunc RPCClientFunc
|
||||
|
||||
gracefulShutdownC <-chan struct{}
|
||||
gracePeriod time.Duration
|
||||
stoppedGracefully bool
|
||||
}
|
||||
|
||||
// ControlStreamHandler registers connections with origintunneld and initiates graceful shutdown.
|
||||
type ControlStreamHandler interface {
|
||||
ServeControlStream(ctx context.Context, rw io.ReadWriteCloser, connOptions *tunnelpogs.ConnectionOptions, shouldWaitForUnregister bool) error
|
||||
IsStopped() bool
|
||||
}
|
||||
|
||||
// NewControlStream returns a new instance of ControlStreamHandler
|
||||
func NewControlStream(
|
||||
observer *Observer,
|
||||
connectedFuse ConnectedFuse,
|
||||
namedTunnelConfig *NamedTunnelConfig,
|
||||
connIndex uint8,
|
||||
newRPCClientFunc RPCClientFunc,
|
||||
gracefulShutdownC <-chan struct{},
|
||||
gracePeriod time.Duration,
|
||||
) ControlStreamHandler {
|
||||
if newRPCClientFunc == nil {
|
||||
newRPCClientFunc = newRegistrationRPCClient
|
||||
}
|
||||
return &controlStream{
|
||||
observer: observer,
|
||||
connectedFuse: connectedFuse,
|
||||
namedTunnelConfig: namedTunnelConfig,
|
||||
newRPCClientFunc: newRPCClientFunc,
|
||||
connIndex: connIndex,
|
||||
gracefulShutdownC: gracefulShutdownC,
|
||||
gracePeriod: gracePeriod,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *controlStream) ServeControlStream(
|
||||
ctx context.Context,
|
||||
rw io.ReadWriteCloser,
|
||||
connOptions *tunnelpogs.ConnectionOptions,
|
||||
shouldWaitForUnregister bool,
|
||||
) error {
|
||||
rpcClient := c.newRPCClientFunc(ctx, rw, c.observer.log)
|
||||
|
||||
if err := rpcClient.RegisterConnection(ctx, c.namedTunnelConfig, connOptions, c.connIndex, c.observer); err != nil {
|
||||
rpcClient.Close()
|
||||
return err
|
||||
}
|
||||
c.connectedFuse.Connected()
|
||||
|
||||
if shouldWaitForUnregister {
|
||||
c.waitForUnregister(ctx, rpcClient)
|
||||
} else {
|
||||
go c.waitForUnregister(ctx, rpcClient)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *controlStream) waitForUnregister(ctx context.Context, rpcClient NamedTunnelRPCClient) {
|
||||
// wait for connection termination or start of graceful shutdown
|
||||
defer rpcClient.Close()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break
|
||||
case <-c.gracefulShutdownC:
|
||||
c.stoppedGracefully = true
|
||||
}
|
||||
|
||||
c.observer.sendUnregisteringEvent(c.connIndex)
|
||||
rpcClient.GracefulShutdown(ctx, c.gracePeriod)
|
||||
c.observer.log.Info().Uint8(LogFieldConnIndex, c.connIndex).Msg("Unregistered tunnel connection")
|
||||
}
|
||||
|
||||
func (c *controlStream) IsStopped() bool {
|
||||
return c.stoppedGracefully
|
||||
}
|
||||
+1
-1
@@ -18,7 +18,7 @@ const (
|
||||
Connected
|
||||
// Reconnecting means the connection to the edge is being re-established.
|
||||
Reconnecting
|
||||
// SetURL means this connection's tunnel was given a URL by the edge. Used for free tunnels.
|
||||
// SetURL means this connection's tunnel was given a URL by the edge. Used for quick tunnels.
|
||||
SetURL
|
||||
// RegisteringTunnel means the non-named tunnel is registering its connection.
|
||||
RegisteringTunnel
|
||||
|
||||
+15
-13
@@ -7,13 +7,13 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -33,6 +33,8 @@ type h2muxConnection struct {
|
||||
gracefulShutdownC <-chan struct{}
|
||||
stoppedGracefully bool
|
||||
|
||||
log *zerolog.Logger
|
||||
|
||||
// newRPCClientFunc allows us to mock RPCs during testing
|
||||
newRPCClientFunc func(context.Context, io.ReadWriteCloser, *zerolog.Logger) NamedTunnelRPCClient
|
||||
}
|
||||
@@ -167,7 +169,8 @@ func (h *h2muxConnection) serveMuxer(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) controlLoop(ctx context.Context, connectedFuse ConnectedFuse, isNamedTunnel bool) {
|
||||
updateMetricsTickC := time.Tick(h.muxerConfig.MetricsUpdateFreq)
|
||||
updateMetricsTicker := time.NewTicker(h.muxerConfig.MetricsUpdateFreq)
|
||||
defer updateMetricsTicker.Stop()
|
||||
var shutdownCompleted <-chan struct{}
|
||||
for {
|
||||
select {
|
||||
@@ -191,7 +194,7 @@ func (h *h2muxConnection) controlLoop(ctx context.Context, connectedFuse Connect
|
||||
// don't wait for shutdown to finish when context is closed, this is the hard termination path
|
||||
return
|
||||
|
||||
case <-updateMetricsTickC:
|
||||
case <-updateMetricsTicker.C:
|
||||
h.observer.metrics.updateMuxerMetrics(h.connIndexStr, h.muxer.Metrics())
|
||||
}
|
||||
}
|
||||
@@ -221,12 +224,11 @@ func (h *h2muxConnection) ServeStream(stream *h2mux.MuxedStream) error {
|
||||
sourceConnectionType = TypeWebsocket
|
||||
}
|
||||
|
||||
err := h.config.OriginProxy.Proxy(respWriter, req, sourceConnectionType)
|
||||
err := h.config.OriginProxy.ProxyHTTP(respWriter, req, sourceConnectionType == TypeWebsocket)
|
||||
if err != nil {
|
||||
respWriter.WriteErrorResponse()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) newRequest(stream *h2mux.MuxedStream) (*http.Request, error) {
|
||||
@@ -234,7 +236,7 @@ func (h *h2muxConnection) newRequest(stream *h2mux.MuxedStream) (*http.Request,
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unexpected error from http.NewRequest")
|
||||
}
|
||||
err = h2mux.H2RequestHeadersToH1Request(stream.Headers, req)
|
||||
err = H2RequestHeadersToH1Request(stream.Headers, req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "invalid request received")
|
||||
}
|
||||
@@ -246,15 +248,15 @@ type h2muxRespWriter struct {
|
||||
}
|
||||
|
||||
func (rp *h2muxRespWriter) WriteRespHeaders(status int, header http.Header) error {
|
||||
headers := h2mux.H1ResponseToH2ResponseHeaders(status, header)
|
||||
headers = append(headers, h2mux.Header{Name: ResponseMetaHeaderField, Value: responseMetaHeaderOrigin})
|
||||
headers := H1ResponseToH2ResponseHeaders(status, header)
|
||||
headers = append(headers, h2mux.Header{Name: ResponseMetaHeader, Value: responseMetaHeaderOrigin})
|
||||
return rp.WriteHeaders(headers)
|
||||
}
|
||||
|
||||
func (rp *h2muxRespWriter) WriteErrorResponse() {
|
||||
_ = rp.WriteHeaders([]h2mux.Header{
|
||||
{Name: ":status", Value: "502"},
|
||||
{Name: ResponseMetaHeaderField, Value: responseMetaHeaderCfd},
|
||||
{Name: ResponseMetaHeader, Value: responseMetaHeaderCfd},
|
||||
})
|
||||
_, _ = rp.Write([]byte("502 Bad Gateway"))
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ func newH2MuxConnection(t require.TestingT) (*h2muxConnection, *h2mux.Muxer) {
|
||||
// we only expect RPC traffic in client->edge direction, provide minimal support for mocking
|
||||
require.True(t, stream.IsRPCStream())
|
||||
return stream.WriteHeaders([]h2mux.Header{
|
||||
{Name: ":status", Value: "200"},
|
||||
{Name: ":status", Value: "200"},
|
||||
})
|
||||
}),
|
||||
}
|
||||
@@ -115,9 +115,9 @@ func TestServeStreamHTTP(t *testing.T) {
|
||||
require.True(t, hasHeader(stream, ":status", strconv.Itoa(test.expectedStatus)))
|
||||
|
||||
if test.isProxyError {
|
||||
assert.True(t, hasHeader(stream, ResponseMetaHeaderField, responseMetaHeaderCfd))
|
||||
assert.True(t, hasHeader(stream, ResponseMetaHeader, responseMetaHeaderCfd))
|
||||
} else {
|
||||
assert.True(t, hasHeader(stream, ResponseMetaHeaderField, responseMetaHeaderOrigin))
|
||||
assert.True(t, hasHeader(stream, ResponseMetaHeader, responseMetaHeaderOrigin))
|
||||
body := make([]byte, len(test.expectedBody))
|
||||
_, err = stream.Read(body)
|
||||
require.NoError(t, err)
|
||||
@@ -164,7 +164,7 @@ func TestServeStreamWS(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, hasHeader(stream, ":status", strconv.Itoa(http.StatusSwitchingProtocols)))
|
||||
assert.True(t, hasHeader(stream, ResponseMetaHeaderField, responseMetaHeaderOrigin))
|
||||
assert.True(t, hasHeader(stream, ResponseMetaHeader, responseMetaHeaderOrigin))
|
||||
|
||||
data := []byte("test websocket")
|
||||
err = wsutil.WriteClientText(writePipe, data)
|
||||
@@ -268,7 +268,7 @@ func benchmarkServeStreamHTTPSimple(b *testing.B, test testRequest) {
|
||||
b.StopTimer()
|
||||
|
||||
require.NoError(b, openstreamErr)
|
||||
assert.True(b, hasHeader(stream, ResponseMetaHeaderField, responseMetaHeaderOrigin))
|
||||
assert.True(b, hasHeader(stream, ResponseMetaHeader, responseMetaHeaderOrigin))
|
||||
require.True(b, hasHeader(stream, ":status", strconv.Itoa(http.StatusOK)))
|
||||
require.NoError(b, readBodyErr)
|
||||
require.Equal(b, test.expectedBody, body)
|
||||
|
||||
+226
-6
@@ -1,21 +1,33 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
)
|
||||
|
||||
const (
|
||||
ResponseMetaHeaderField = "cf-cloudflared-response-meta"
|
||||
var (
|
||||
// h2mux-style special headers
|
||||
RequestUserHeaders = "cf-cloudflared-request-headers"
|
||||
ResponseUserHeaders = "cf-cloudflared-response-headers"
|
||||
ResponseMetaHeader = "cf-cloudflared-response-meta"
|
||||
|
||||
// h2mux-style special headers
|
||||
CanonicalResponseUserHeaders = http.CanonicalHeaderKey(ResponseUserHeaders)
|
||||
CanonicalResponseMetaHeader = http.CanonicalHeaderKey(ResponseMetaHeader)
|
||||
)
|
||||
|
||||
var (
|
||||
canonicalResponseUserHeadersField = http.CanonicalHeaderKey(h2mux.ResponseUserHeadersField)
|
||||
canonicalResponseMetaHeaderField = http.CanonicalHeaderKey(ResponseMetaHeaderField)
|
||||
responseMetaHeaderCfd = mustInitRespMetaHeader("cloudflared")
|
||||
responseMetaHeaderOrigin = mustInitRespMetaHeader("origin")
|
||||
// pre-generate possible values for res
|
||||
responseMetaHeaderCfd = mustInitRespMetaHeader("cloudflared")
|
||||
responseMetaHeaderOrigin = mustInitRespMetaHeader("origin")
|
||||
)
|
||||
|
||||
type responseMetaHeader struct {
|
||||
@@ -29,3 +41,211 @@ func mustInitRespMetaHeader(src string) string {
|
||||
}
|
||||
return string(header)
|
||||
}
|
||||
|
||||
var headerEncoding = base64.RawStdEncoding
|
||||
|
||||
// note: all h2mux headers should be lower-case (http/2 style)
|
||||
const ()
|
||||
|
||||
// H2RequestHeadersToH1Request converts the HTTP/2 headers coming from origintunneld
|
||||
// to an HTTP/1 Request object destined for the local origin web service.
|
||||
// This operation includes conversion of the pseudo-headers into their closest
|
||||
// HTTP/1 equivalents. See https://tools.ietf.org/html/rfc7540#section-8.1.2.3
|
||||
func H2RequestHeadersToH1Request(h2 []h2mux.Header, h1 *http.Request) error {
|
||||
for _, header := range h2 {
|
||||
name := strings.ToLower(header.Name)
|
||||
if !IsControlRequestHeader(name) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch name {
|
||||
case ":method":
|
||||
h1.Method = header.Value
|
||||
case ":scheme":
|
||||
// noop - use the preexisting scheme from h1.URL
|
||||
case ":authority":
|
||||
// Otherwise the host header will be based on the origin URL
|
||||
h1.Host = header.Value
|
||||
case ":path":
|
||||
// We don't want to be an "opinionated" proxy, so ideally we would use :path as-is.
|
||||
// However, this HTTP/1 Request object belongs to the Go standard library,
|
||||
// whose URL package makes some opinionated decisions about the encoding of
|
||||
// URL characters: see the docs of https://godoc.org/net/url#URL,
|
||||
// in particular the EscapedPath method https://godoc.org/net/url#URL.EscapedPath,
|
||||
// which is always used when computing url.URL.String(), whether we'd like it or not.
|
||||
//
|
||||
// Well, not *always*. We could circumvent this by using url.URL.Opaque. But
|
||||
// that would present unusual difficulties when using an HTTP proxy: url.URL.Opaque
|
||||
// is treated differently when HTTP_PROXY is set!
|
||||
// See https://github.com/golang/go/issues/5684#issuecomment-66080888
|
||||
//
|
||||
// This means we are subject to the behavior of net/url's function `shouldEscape`
|
||||
// (as invoked with mode=encodePath): https://github.com/golang/go/blob/go1.12.7/src/net/url/url.go#L101
|
||||
|
||||
if header.Value == "*" {
|
||||
h1.URL.Path = "*"
|
||||
continue
|
||||
}
|
||||
// Due to the behavior of validation.ValidateUrl, h1.URL may
|
||||
// already have a partial value, with or without a trailing slash.
|
||||
base := h1.URL.String()
|
||||
base = strings.TrimRight(base, "/")
|
||||
// But we know :path begins with '/', because we handled '*' above - see RFC7540
|
||||
requestURL, err := url.Parse(base + header.Value)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("invalid path '%v'", header.Value))
|
||||
}
|
||||
h1.URL = requestURL
|
||||
case "content-length":
|
||||
contentLength, err := strconv.ParseInt(header.Value, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unparseable content length")
|
||||
}
|
||||
h1.ContentLength = contentLength
|
||||
case RequestUserHeaders:
|
||||
// Do not forward the serialized headers to the origin -- deserialize them, and ditch the serialized version
|
||||
// Find and parse user headers serialized into a single one
|
||||
userHeaders, err := DeserializeHeaders(header.Value)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to parse user headers")
|
||||
}
|
||||
for _, userHeader := range userHeaders {
|
||||
h1.Header.Add(userHeader.Name, userHeader.Value)
|
||||
}
|
||||
default:
|
||||
// All other control headers shall just be proxied transparently
|
||||
h1.Header.Add(header.Name, header.Value)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsControlRequestHeader(headerName string) bool {
|
||||
return headerName == "content-length" ||
|
||||
headerName == "connection" || headerName == "upgrade" || // Websocket request headers
|
||||
strings.HasPrefix(headerName, ":") ||
|
||||
strings.HasPrefix(headerName, "cf-")
|
||||
}
|
||||
|
||||
func IsControlResponseHeader(headerName string) bool {
|
||||
return headerName == "content-length" ||
|
||||
strings.HasPrefix(headerName, ":") ||
|
||||
strings.HasPrefix(headerName, "cf-int-") ||
|
||||
strings.HasPrefix(headerName, "cf-cloudflared-")
|
||||
}
|
||||
|
||||
// isWebsocketClientHeader returns true if the header name is required by the client to upgrade properly
|
||||
func IsWebsocketClientHeader(headerName string) bool {
|
||||
return headerName == "sec-websocket-accept" ||
|
||||
headerName == "connection" ||
|
||||
headerName == "upgrade"
|
||||
}
|
||||
|
||||
func H1ResponseToH2ResponseHeaders(status int, h1 http.Header) (h2 []h2mux.Header) {
|
||||
h2 = []h2mux.Header{
|
||||
{Name: ":status", Value: strconv.Itoa(status)},
|
||||
}
|
||||
userHeaders := make(http.Header, len(h1))
|
||||
for header, values := range h1 {
|
||||
h2name := strings.ToLower(header)
|
||||
if h2name == "content-length" {
|
||||
// This header has meaning in HTTP/2 and will be used by the edge,
|
||||
// so it should be sent as an HTTP/2 response header.
|
||||
|
||||
// Since these are http2 headers, they're required to be lowercase
|
||||
h2 = append(h2, h2mux.Header{Name: "content-length", Value: values[0]})
|
||||
} else if !IsControlResponseHeader(h2name) || IsWebsocketClientHeader(h2name) {
|
||||
// User headers, on the other hand, must all be serialized so that
|
||||
// HTTP/2 header validation won't be applied to HTTP/1 header values
|
||||
userHeaders[header] = values
|
||||
}
|
||||
}
|
||||
|
||||
// Perform user header serialization and set them in the single header
|
||||
h2 = append(h2, h2mux.Header{Name: ResponseUserHeaders, Value: SerializeHeaders(userHeaders)})
|
||||
return h2
|
||||
}
|
||||
|
||||
// Serialize HTTP1.x headers by base64-encoding each header name and value,
|
||||
// and then joining them in the format of [key:value;]
|
||||
func SerializeHeaders(h1Headers http.Header) string {
|
||||
// compute size of the fully serialized value and largest temp buffer we will need
|
||||
serializedLen := 0
|
||||
maxTempLen := 0
|
||||
for headerName, headerValues := range h1Headers {
|
||||
for _, headerValue := range headerValues {
|
||||
nameLen := headerEncoding.EncodedLen(len(headerName))
|
||||
valueLen := headerEncoding.EncodedLen(len(headerValue))
|
||||
const delims = 2
|
||||
serializedLen += delims + nameLen + valueLen
|
||||
if nameLen > maxTempLen {
|
||||
maxTempLen = nameLen
|
||||
}
|
||||
if valueLen > maxTempLen {
|
||||
maxTempLen = valueLen
|
||||
}
|
||||
}
|
||||
}
|
||||
var buf strings.Builder
|
||||
buf.Grow(serializedLen)
|
||||
|
||||
temp := make([]byte, maxTempLen)
|
||||
writeB64 := func(s string) {
|
||||
n := headerEncoding.EncodedLen(len(s))
|
||||
if n > len(temp) {
|
||||
temp = make([]byte, n)
|
||||
}
|
||||
headerEncoding.Encode(temp[:n], []byte(s))
|
||||
buf.Write(temp[:n])
|
||||
}
|
||||
|
||||
for headerName, headerValues := range h1Headers {
|
||||
for _, headerValue := range headerValues {
|
||||
if buf.Len() > 0 {
|
||||
buf.WriteByte(';')
|
||||
}
|
||||
writeB64(headerName)
|
||||
buf.WriteByte(':')
|
||||
writeB64(headerValue)
|
||||
}
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Deserialize headers serialized by `SerializeHeader`
|
||||
func DeserializeHeaders(serializedHeaders string) ([]h2mux.Header, error) {
|
||||
const unableToDeserializeErr = "Unable to deserialize headers"
|
||||
|
||||
var deserialized []h2mux.Header
|
||||
for _, serializedPair := range strings.Split(serializedHeaders, ";") {
|
||||
if len(serializedPair) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
serializedHeaderParts := strings.Split(serializedPair, ":")
|
||||
if len(serializedHeaderParts) != 2 {
|
||||
return nil, errors.New(unableToDeserializeErr)
|
||||
}
|
||||
|
||||
serializedName := serializedHeaderParts[0]
|
||||
serializedValue := serializedHeaderParts[1]
|
||||
deserializedName := make([]byte, headerEncoding.DecodedLen(len(serializedName)))
|
||||
deserializedValue := make([]byte, headerEncoding.DecodedLen(len(serializedValue)))
|
||||
|
||||
if _, err := headerEncoding.Decode(deserializedName, []byte(serializedName)); err != nil {
|
||||
return nil, errors.Wrap(err, unableToDeserializeErr)
|
||||
}
|
||||
if _, err := headerEncoding.Decode(deserializedValue, []byte(serializedValue)); err != nil {
|
||||
return nil, errors.Wrap(err, unableToDeserializeErr)
|
||||
}
|
||||
|
||||
deserialized = append(deserialized, h2mux.Header{
|
||||
Name: string(deserializedName),
|
||||
Value: string(deserializedValue),
|
||||
})
|
||||
}
|
||||
|
||||
return deserialized, nil
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package h2mux
|
||||
package connection
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -14,9 +14,11 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
)
|
||||
|
||||
type ByName []Header
|
||||
type ByName []h2mux.Header
|
||||
|
||||
func (a ByName) Len() int { return len(a) }
|
||||
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
@@ -37,16 +39,16 @@ func TestH2RequestHeadersToH1Request_RegularHeaders(t *testing.T) {
|
||||
"Mock header 2": {"Mock value 2"},
|
||||
}
|
||||
|
||||
headersConversionErr := H2RequestHeadersToH1Request(createSerializedHeaders(RequestUserHeadersField, mockHeaders), request)
|
||||
headersConversionErr := H2RequestHeadersToH1Request(createSerializedHeaders(RequestUserHeaders, mockHeaders), request)
|
||||
|
||||
assert.True(t, reflect.DeepEqual(mockHeaders, request.Header))
|
||||
assert.NoError(t, headersConversionErr)
|
||||
}
|
||||
|
||||
func createSerializedHeaders(headersField string, headers http.Header) []Header {
|
||||
return []Header{{
|
||||
headersField,
|
||||
SerializeHeaders(headers),
|
||||
func createSerializedHeaders(headersField string, headers http.Header) []h2mux.Header {
|
||||
return []h2mux.Header{{
|
||||
Name: headersField,
|
||||
Value: SerializeHeaders(headers),
|
||||
}}
|
||||
}
|
||||
|
||||
@@ -54,15 +56,16 @@ func TestH2RequestHeadersToH1Request_NoHeaders(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
emptyHeaders := make(http.Header)
|
||||
headersConversionErr := H2RequestHeadersToH1Request(
|
||||
[]Header{{
|
||||
RequestUserHeadersField,
|
||||
SerializeHeaders(http.Header{}),
|
||||
[]h2mux.Header{{
|
||||
Name: RequestUserHeaders,
|
||||
Value: SerializeHeaders(emptyHeaders),
|
||||
}},
|
||||
request,
|
||||
)
|
||||
|
||||
assert.True(t, reflect.DeepEqual(http.Header{}, request.Header))
|
||||
assert.True(t, reflect.DeepEqual(emptyHeaders, request.Header))
|
||||
assert.NoError(t, headersConversionErr)
|
||||
}
|
||||
|
||||
@@ -70,9 +73,9 @@ func TestH2RequestHeadersToH1Request_InvalidHostPath(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockRequestHeaders := []Header{
|
||||
mockRequestHeaders := []h2mux.Header{
|
||||
{Name: ":path", Value: "//bad_path/"},
|
||||
{Name: RequestUserHeadersField, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
|
||||
{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
|
||||
}
|
||||
|
||||
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
|
||||
@@ -90,9 +93,9 @@ func TestH2RequestHeadersToH1Request_HostPathWithQuery(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockRequestHeaders := []Header{
|
||||
mockRequestHeaders := []h2mux.Header{
|
||||
{Name: ":path", Value: "/?query=mock%20value"},
|
||||
{Name: RequestUserHeadersField, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
|
||||
{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
|
||||
}
|
||||
|
||||
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
|
||||
@@ -110,9 +113,9 @@ func TestH2RequestHeadersToH1Request_HostPathWithURLEncoding(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockRequestHeaders := []Header{
|
||||
mockRequestHeaders := []h2mux.Header{
|
||||
{Name: ":path", Value: "/mock%20path"},
|
||||
{Name: RequestUserHeadersField, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
|
||||
{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
|
||||
}
|
||||
|
||||
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
|
||||
@@ -267,9 +270,9 @@ func TestH2RequestHeadersToH1Request_WeirdURLs(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, requestURL, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockRequestHeaders := []Header{
|
||||
mockRequestHeaders := []h2mux.Header{
|
||||
{Name: ":path", Value: testCase.path},
|
||||
{Name: RequestUserHeadersField, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
|
||||
{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
|
||||
}
|
||||
|
||||
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
|
||||
@@ -337,12 +340,12 @@ func TestH2RequestHeadersToH1Request_QuickCheck(t *testing.T) {
|
||||
const expectedMethod = "POST"
|
||||
const expectedHostname = "request.hostname.example.com"
|
||||
|
||||
h2 := []Header{
|
||||
h2 := []h2mux.Header{
|
||||
{Name: ":method", Value: expectedMethod},
|
||||
{Name: ":scheme", Value: testScheme},
|
||||
{Name: ":authority", Value: expectedHostname},
|
||||
{Name: ":path", Value: testPath},
|
||||
{Name: RequestUserHeadersField, Value: ""},
|
||||
{Name: RequestUserHeaders, Value: ""},
|
||||
}
|
||||
h1, err := http.NewRequest("GET", testOrigin.url, nil)
|
||||
require.NoError(t, err)
|
||||
@@ -424,10 +427,10 @@ func randomHTTP2Path(t *testing.T, rand *rand.Rand) string {
|
||||
return result
|
||||
}
|
||||
|
||||
func stdlibHeaderToH2muxHeader(headers http.Header) (h2muxHeaders []Header) {
|
||||
func stdlibHeaderToH2muxHeader(headers http.Header) (h2muxHeaders []h2mux.Header) {
|
||||
for name, values := range headers {
|
||||
for _, value := range values {
|
||||
h2muxHeaders = append(h2muxHeaders, Header{name, value})
|
||||
h2muxHeaders = append(h2muxHeaders, h2mux.Header{Name: name, Value: value})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,21 +511,21 @@ func TestDeserializeMalformed(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHeaders(t *testing.T) {
|
||||
func TestParseRequestHeaders(t *testing.T) {
|
||||
mockUserHeadersToSerialize := http.Header{
|
||||
"Mock-Header-One": {"1", "1.5"},
|
||||
"Mock-Header-Two": {"2"},
|
||||
"Mock-Header-Three": {"3"},
|
||||
}
|
||||
|
||||
mockHeaders := []Header{
|
||||
mockHeaders := []h2mux.Header{
|
||||
{Name: "One", Value: "1"}, // will be dropped
|
||||
{Name: "Cf-Two", Value: "cf-value-1"},
|
||||
{Name: "Cf-Two", Value: "cf-value-2"},
|
||||
{Name: RequestUserHeadersField, Value: SerializeHeaders(mockUserHeadersToSerialize)},
|
||||
{Name: RequestUserHeaders, Value: SerializeHeaders(mockUserHeadersToSerialize)},
|
||||
}
|
||||
|
||||
expectedHeaders := []Header{
|
||||
expectedHeaders := []h2mux.Header{
|
||||
{Name: "Cf-Two", Value: "cf-value-1"},
|
||||
{Name: "Cf-Two", Value: "cf-value-2"},
|
||||
{Name: "Mock-Header-One", Value: "1"},
|
||||
@@ -538,8 +541,8 @@ func TestParseHeaders(t *testing.T) {
|
||||
assert.ElementsMatch(t, expectedHeaders, stdlibHeaderToH2muxHeader(h1.Header))
|
||||
}
|
||||
|
||||
func TestIsControlHeader(t *testing.T) {
|
||||
controlHeaders := []string{
|
||||
func TestIsControlRequestHeader(t *testing.T) {
|
||||
controlRequestHeaders := []string{
|
||||
// Anything that begins with cf-
|
||||
"cf-sample-header",
|
||||
|
||||
@@ -549,30 +552,69 @@ func TestIsControlHeader(t *testing.T) {
|
||||
// content-length is a special case, it has to be there
|
||||
// for some requests to work (per the HTTP2 spec)
|
||||
"content-length",
|
||||
|
||||
// Websocket request headers
|
||||
"connection",
|
||||
"upgrade",
|
||||
}
|
||||
|
||||
for _, header := range controlHeaders {
|
||||
assert.True(t, IsControlHeader(header))
|
||||
for _, header := range controlRequestHeaders {
|
||||
assert.True(t, IsControlRequestHeader(header))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsNotControlHeader(t *testing.T) {
|
||||
notControlHeaders := []string{
|
||||
func TestIsControlResponseHeader(t *testing.T) {
|
||||
controlResponseHeaders := []string{
|
||||
// Anything that begins with cf-int- or cf-cloudflared-
|
||||
"cf-int-sample-header",
|
||||
"cf-cloudflared-sample-header",
|
||||
|
||||
// Any http2 pseudoheader
|
||||
":sample-pseudo-header",
|
||||
|
||||
// content-length is a special case, it has to be there
|
||||
// for some requests to work (per the HTTP2 spec)
|
||||
"content-length",
|
||||
}
|
||||
|
||||
for _, header := range controlResponseHeaders {
|
||||
assert.True(t, IsControlResponseHeader(header))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsNotControlRequestHeader(t *testing.T) {
|
||||
notControlRequestHeaders := []string{
|
||||
"mock-header",
|
||||
"another-sample-header",
|
||||
}
|
||||
|
||||
for _, header := range notControlHeaders {
|
||||
assert.False(t, IsControlHeader(header))
|
||||
for _, header := range notControlRequestHeaders {
|
||||
assert.False(t, IsControlRequestHeader(header))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsNotControlResponseHeader(t *testing.T) {
|
||||
notControlResponseHeaders := []string{
|
||||
"mock-header",
|
||||
"another-sample-header",
|
||||
"upgrade",
|
||||
"connection",
|
||||
"cf-whatever", // On the response path, we only want to filter cf-int- and cf-cloudflared-
|
||||
}
|
||||
|
||||
for _, header := range notControlResponseHeaders {
|
||||
assert.False(t, IsControlResponseHeader(header))
|
||||
}
|
||||
}
|
||||
|
||||
func TestH1ResponseToH2ResponseHeaders(t *testing.T) {
|
||||
mockHeaders := http.Header{
|
||||
"User-header-one": {""},
|
||||
"User-header-two": {"1", "2"},
|
||||
"cf-header": {"cf-value"},
|
||||
"Content-Length": {"123"},
|
||||
"User-header-one": {""},
|
||||
"User-header-two": {"1", "2"},
|
||||
"cf-header": {"cf-value"},
|
||||
"cf-int-header": {"cf-int-value"},
|
||||
"cf-cloudflared-header": {"cf-cloudflared-value"},
|
||||
"Content-Length": {"123"},
|
||||
}
|
||||
mockResponse := http.Response{
|
||||
StatusCode: 200,
|
||||
@@ -583,7 +625,7 @@ func TestH1ResponseToH2ResponseHeaders(t *testing.T) {
|
||||
|
||||
serializedHeadersIndex := -1
|
||||
for i, header := range headers {
|
||||
if header.Name == ResponseUserHeadersField {
|
||||
if header.Name == ResponseUserHeaders {
|
||||
serializedHeadersIndex = i
|
||||
break
|
||||
}
|
||||
@@ -593,7 +635,7 @@ func TestH1ResponseToH2ResponseHeaders(t *testing.T) {
|
||||
headers[:serializedHeadersIndex],
|
||||
headers[serializedHeadersIndex+1:]...,
|
||||
)
|
||||
expectedControlHeaders := []Header{
|
||||
expectedControlHeaders := []h2mux.Header{
|
||||
{Name: ":status", Value: "200"},
|
||||
{Name: "content-length", Value: "123"},
|
||||
}
|
||||
@@ -601,10 +643,11 @@ func TestH1ResponseToH2ResponseHeaders(t *testing.T) {
|
||||
assert.ElementsMatch(t, expectedControlHeaders, actualControlHeaders)
|
||||
|
||||
actualUserHeaders, err := DeserializeHeaders(headers[serializedHeadersIndex].Value)
|
||||
expectedUserHeaders := []Header{
|
||||
expectedUserHeaders := []h2mux.Header{
|
||||
{Name: "User-header-one", Value: ""},
|
||||
{Name: "User-header-two", Value: "1"},
|
||||
{Name: "User-header-two", Value: "2"},
|
||||
{Name: "cf-header", Value: "cf-value"},
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, expectedUserHeaders, actualUserHeaders)
|
||||
@@ -630,7 +673,7 @@ func TestHeaderSize(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, header := range serializedHeaders {
|
||||
if header.Name != ResponseUserHeadersField {
|
||||
if header.Name != ResponseUserHeaders {
|
||||
continue
|
||||
}
|
||||
|
||||
+119
-92
@@ -7,72 +7,73 @@ import (
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/net/http2"
|
||||
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
// note: these constants are exported so we can reuse them in the edge-side code
|
||||
const (
|
||||
internalUpgradeHeader = "Cf-Cloudflared-Proxy-Connection-Upgrade"
|
||||
tcpStreamHeader = "Cf-Cloudflared-Proxy-Src"
|
||||
websocketUpgrade = "websocket"
|
||||
controlStreamUpgrade = "control-stream"
|
||||
InternalUpgradeHeader = "Cf-Cloudflared-Proxy-Connection-Upgrade"
|
||||
InternalTCPProxySrcHeader = "Cf-Cloudflared-Proxy-Src"
|
||||
WebsocketUpgrade = "websocket"
|
||||
ControlStreamUpgrade = "control-stream"
|
||||
)
|
||||
|
||||
var errEdgeConnectionClosed = fmt.Errorf("connection with edge closed")
|
||||
|
||||
type http2Connection struct {
|
||||
conn net.Conn
|
||||
server *http2.Server
|
||||
config *Config
|
||||
namedTunnel *NamedTunnelConfig
|
||||
connOptions *tunnelpogs.ConnectionOptions
|
||||
observer *Observer
|
||||
connIndexStr string
|
||||
connIndex uint8
|
||||
// HTTP2Connection represents a net.Conn that uses HTTP2 frames to proxy traffic from the edge to cloudflared on the
|
||||
// origin.
|
||||
type HTTP2Connection struct {
|
||||
conn net.Conn
|
||||
server *http2.Server
|
||||
config *Config
|
||||
connOptions *tunnelpogs.ConnectionOptions
|
||||
observer *Observer
|
||||
connIndex uint8
|
||||
// newRPCClientFunc allows us to mock RPCs during testing
|
||||
newRPCClientFunc func(context.Context, io.ReadWriteCloser, *zerolog.Logger) NamedTunnelRPCClient
|
||||
|
||||
activeRequestsWG sync.WaitGroup
|
||||
connectedFuse ConnectedFuse
|
||||
gracefulShutdownC <-chan struct{}
|
||||
stoppedGracefully bool
|
||||
controlStreamErr error // result of running control stream handler
|
||||
log *zerolog.Logger
|
||||
activeRequestsWG sync.WaitGroup
|
||||
controlStreamHandler ControlStreamHandler
|
||||
stoppedGracefully bool
|
||||
controlStreamErr error // result of running control stream handler
|
||||
}
|
||||
|
||||
// NewHTTP2Connection returns a new instance of HTTP2Connection.
|
||||
func NewHTTP2Connection(
|
||||
conn net.Conn,
|
||||
config *Config,
|
||||
namedTunnelConfig *NamedTunnelConfig,
|
||||
connOptions *tunnelpogs.ConnectionOptions,
|
||||
observer *Observer,
|
||||
connIndex uint8,
|
||||
connectedFuse ConnectedFuse,
|
||||
gracefulShutdownC <-chan struct{},
|
||||
) *http2Connection {
|
||||
return &http2Connection{
|
||||
controlStreamHandler ControlStreamHandler,
|
||||
log *zerolog.Logger,
|
||||
) *HTTP2Connection {
|
||||
return &HTTP2Connection{
|
||||
conn: conn,
|
||||
server: &http2.Server{
|
||||
MaxConcurrentStreams: math.MaxUint32,
|
||||
},
|
||||
config: config,
|
||||
namedTunnel: namedTunnelConfig,
|
||||
connOptions: connOptions,
|
||||
observer: observer,
|
||||
connIndexStr: uint8ToString(connIndex),
|
||||
connIndex: connIndex,
|
||||
newRPCClientFunc: newRegistrationRPCClient,
|
||||
connectedFuse: connectedFuse,
|
||||
gracefulShutdownC: gracefulShutdownC,
|
||||
config: config,
|
||||
connOptions: connOptions,
|
||||
observer: observer,
|
||||
connIndex: connIndex,
|
||||
newRPCClientFunc: newRegistrationRPCClient,
|
||||
controlStreamHandler: controlStreamHandler,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *http2Connection) Serve(ctx context.Context) error {
|
||||
// Serve serves an HTTP2 server that the edge can talk to.
|
||||
func (c *HTTP2Connection) Serve(ctx context.Context) error {
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
c.close()
|
||||
@@ -83,7 +84,7 @@ func (c *http2Connection) Serve(ctx context.Context) error {
|
||||
})
|
||||
|
||||
switch {
|
||||
case c.stoppedGracefully:
|
||||
case c.controlStreamHandler.IsStopped():
|
||||
return nil
|
||||
case c.controlStreamErr != nil:
|
||||
return c.controlStreamErr
|
||||
@@ -93,57 +94,60 @@ func (c *http2Connection) Serve(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *http2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
func (c *HTTP2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
c.activeRequestsWG.Add(1)
|
||||
defer c.activeRequestsWG.Done()
|
||||
|
||||
connType := determineHTTP2Type(r)
|
||||
respWriter, err := newHTTP2RespWriter(r, w, connType)
|
||||
handleMissingRequestParts(connType, r)
|
||||
|
||||
respWriter, err := NewHTTP2RespWriter(r, w, connType)
|
||||
if err != nil {
|
||||
c.observer.log.Error().Msg(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var proxyErr error
|
||||
switch connType {
|
||||
case TypeControlStream:
|
||||
proxyErr = c.serveControlStream(r.Context(), respWriter)
|
||||
c.controlStreamErr = proxyErr
|
||||
case TypeWebsocket:
|
||||
if err := c.controlStreamHandler.ServeControlStream(r.Context(), respWriter, c.connOptions, true); err != nil {
|
||||
c.controlStreamErr = err
|
||||
c.log.Error().Err(err)
|
||||
respWriter.WriteErrorResponse()
|
||||
}
|
||||
|
||||
case TypeWebsocket, TypeHTTP:
|
||||
stripWebsocketUpgradeHeader(r)
|
||||
proxyErr = c.config.OriginProxy.Proxy(respWriter, r, TypeWebsocket)
|
||||
if err := c.config.OriginProxy.ProxyHTTP(respWriter, r, connType == TypeWebsocket); err != nil {
|
||||
err := fmt.Errorf("Failed to proxy HTTP: %w", err)
|
||||
c.log.Error().Err(err)
|
||||
respWriter.WriteErrorResponse()
|
||||
}
|
||||
|
||||
case TypeTCP:
|
||||
host, err := getRequestHost(r)
|
||||
if err != nil {
|
||||
err := fmt.Errorf(`cloudflared recieved a warp-routing request with an empty host value: %w`, err)
|
||||
c.log.Error().Err(err)
|
||||
respWriter.WriteErrorResponse()
|
||||
}
|
||||
|
||||
rws := NewHTTPResponseReadWriterAcker(respWriter, r)
|
||||
if err := c.config.OriginProxy.ProxyTCP(r.Context(), rws, &TCPRequest{
|
||||
Dest: host,
|
||||
CFRay: FindCfRayHeader(r),
|
||||
LBProbe: IsLBProbeRequest(r),
|
||||
}); err != nil {
|
||||
respWriter.WriteErrorResponse()
|
||||
}
|
||||
|
||||
default:
|
||||
proxyErr = c.config.OriginProxy.Proxy(respWriter, r, connType)
|
||||
}
|
||||
if proxyErr != nil {
|
||||
err := fmt.Errorf("Received unknown connection type: %s", connType)
|
||||
c.log.Error().Err(err)
|
||||
respWriter.WriteErrorResponse()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *http2Connection) serveControlStream(ctx context.Context, respWriter *http2RespWriter) error {
|
||||
rpcClient := c.newRPCClientFunc(ctx, respWriter, c.observer.log)
|
||||
defer rpcClient.Close()
|
||||
|
||||
if err := rpcClient.RegisterConnection(ctx, c.namedTunnel, c.connOptions, c.connIndex, c.observer); err != nil {
|
||||
return err
|
||||
}
|
||||
c.connectedFuse.Connected()
|
||||
|
||||
// wait for connection termination or start of graceful shutdown
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break
|
||||
case <-c.gracefulShutdownC:
|
||||
c.stoppedGracefully = true
|
||||
}
|
||||
|
||||
c.observer.sendUnregisteringEvent(c.connIndex)
|
||||
rpcClient.GracefulShutdown(ctx, c.config.GracePeriod)
|
||||
c.observer.log.Info().Uint8(LogFieldConnIndex, c.connIndex).Msg("Unregistered tunnel connection")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *http2Connection) close() {
|
||||
func (c *HTTP2Connection) close() {
|
||||
// Wait for all serve HTTP handlers to return
|
||||
c.activeRequestsWG.Wait()
|
||||
c.conn.Close()
|
||||
@@ -156,7 +160,7 @@ type http2RespWriter struct {
|
||||
shouldFlush bool
|
||||
}
|
||||
|
||||
func newHTTP2RespWriter(r *http.Request, w http.ResponseWriter, connType Type) (*http2RespWriter, error) {
|
||||
func NewHTTP2RespWriter(r *http.Request, w http.ResponseWriter, connType Type) (*http2RespWriter, error) {
|
||||
flusher, isFlusher := w.(http.Flusher)
|
||||
if !isFlusher {
|
||||
respWriter := &http2RespWriter{
|
||||
@@ -178,25 +182,23 @@ func newHTTP2RespWriter(r *http.Request, w http.ResponseWriter, connType Type) (
|
||||
func (rp *http2RespWriter) WriteRespHeaders(status int, header http.Header) error {
|
||||
dest := rp.w.Header()
|
||||
userHeaders := make(http.Header, len(header))
|
||||
for header, values := range header {
|
||||
for name, values := range header {
|
||||
// Since these are http2 headers, they're required to be lowercase
|
||||
h2name := strings.ToLower(header)
|
||||
for _, v := range values {
|
||||
if h2name == "content-length" {
|
||||
// This header has meaning in HTTP/2 and will be used by the edge,
|
||||
// so it should be sent as an HTTP/2 response header.
|
||||
dest.Add(h2name, v)
|
||||
// Since these are http2 headers, they're required to be lowercase
|
||||
} else if !h2mux.IsControlHeader(h2name) || h2mux.IsWebsocketClientHeader(h2name) {
|
||||
// User headers, on the other hand, must all be serialized so that
|
||||
// HTTP/2 header validation won't be applied to HTTP/1 header values
|
||||
userHeaders.Add(h2name, v)
|
||||
}
|
||||
h2name := strings.ToLower(name)
|
||||
if h2name == "content-length" {
|
||||
// This header has meaning in HTTP/2 and will be used by the edge,
|
||||
// so it should be sent as an HTTP/2 response header.
|
||||
dest[name] = values
|
||||
// Since these are http2 headers, they're required to be lowercase
|
||||
} else if !IsControlResponseHeader(h2name) || IsWebsocketClientHeader(h2name) {
|
||||
// User headers, on the other hand, must all be serialized so that
|
||||
// HTTP/2 header validation won't be applied to HTTP/1 header values
|
||||
userHeaders[name] = values
|
||||
}
|
||||
}
|
||||
|
||||
// Perform user header serialization and set them in the single header
|
||||
dest.Set(canonicalResponseUserHeadersField, h2mux.SerializeHeaders(userHeaders))
|
||||
dest.Set(CanonicalResponseUserHeaders, SerializeHeaders(userHeaders))
|
||||
rp.setResponseMetaHeader(responseMetaHeaderOrigin)
|
||||
// HTTP2 removes support for 101 Switching Protocols https://tools.ietf.org/html/rfc7540#section-8.1.1
|
||||
if status == http.StatusSwitchingProtocols {
|
||||
@@ -218,7 +220,7 @@ func (rp *http2RespWriter) WriteErrorResponse() {
|
||||
}
|
||||
|
||||
func (rp *http2RespWriter) setResponseMetaHeader(value string) {
|
||||
rp.w.Header().Set(canonicalResponseMetaHeaderField, value)
|
||||
rp.w.Header().Set(CanonicalResponseMetaHeader, value)
|
||||
}
|
||||
|
||||
func (rp *http2RespWriter) Read(p []byte) (n int, err error) {
|
||||
@@ -230,7 +232,7 @@ func (rp *http2RespWriter) Write(p []byte) (n int, err error) {
|
||||
// Implementer of OriginClient should make sure it doesn't write to the connection after Proxy returns
|
||||
// Register a recover routine just in case.
|
||||
if r := recover(); r != nil {
|
||||
println("Recover from http2 response writer panic, error", r)
|
||||
println(fmt.Sprintf("Recover from http2 response writer panic, error %s", debug.Stack()))
|
||||
}
|
||||
}()
|
||||
n, err = rp.w.Write(p)
|
||||
@@ -257,19 +259,44 @@ func determineHTTP2Type(r *http.Request) Type {
|
||||
}
|
||||
}
|
||||
|
||||
func handleMissingRequestParts(connType Type, r *http.Request) {
|
||||
if connType == TypeHTTP {
|
||||
// http library has no guarantees that we receive a filled URL. If not, then we fill it, as we reuse the request
|
||||
// for proxying. We use the same values as we used to in h2mux. For proxying they should not matter since we
|
||||
// control the dialer on every egress proxied.
|
||||
if len(r.URL.Scheme) == 0 {
|
||||
r.URL.Scheme = "http"
|
||||
}
|
||||
if len(r.URL.Host) == 0 {
|
||||
r.URL.Host = "localhost:8080"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isControlStreamUpgrade(r *http.Request) bool {
|
||||
return r.Header.Get(internalUpgradeHeader) == controlStreamUpgrade
|
||||
return r.Header.Get(InternalUpgradeHeader) == ControlStreamUpgrade
|
||||
}
|
||||
|
||||
func isWebsocketUpgrade(r *http.Request) bool {
|
||||
return r.Header.Get(internalUpgradeHeader) == websocketUpgrade
|
||||
return r.Header.Get(InternalUpgradeHeader) == WebsocketUpgrade
|
||||
}
|
||||
|
||||
// IsTCPStream discerns if the connection request needs a tcp stream proxy.
|
||||
func IsTCPStream(r *http.Request) bool {
|
||||
return r.Header.Get(tcpStreamHeader) != ""
|
||||
return r.Header.Get(InternalTCPProxySrcHeader) != ""
|
||||
}
|
||||
|
||||
func stripWebsocketUpgradeHeader(r *http.Request) {
|
||||
r.Header.Del(internalUpgradeHeader)
|
||||
r.Header.Del(InternalUpgradeHeader)
|
||||
}
|
||||
|
||||
// getRequestHost returns the host of the http.Request.
|
||||
func getRequestHost(r *http.Request) (string, error) {
|
||||
if r.Host != "" {
|
||||
return r.Host, nil
|
||||
}
|
||||
if r.URL != nil {
|
||||
return r.URL.Host, nil
|
||||
}
|
||||
return "", errors.New("host not set in incoming request")
|
||||
}
|
||||
|
||||
+65
-23
@@ -12,33 +12,42 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/http2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
var (
|
||||
testTransport = http2.Transport{}
|
||||
)
|
||||
|
||||
func newTestHTTP2Connection() (*http2Connection, net.Conn) {
|
||||
func newTestHTTP2Connection() (*HTTP2Connection, net.Conn) {
|
||||
edgeConn, originConn := net.Pipe()
|
||||
var connIndex = uint8(0)
|
||||
log := zerolog.Nop()
|
||||
obs := NewObserver(&log, &log, false)
|
||||
controlStream := NewControlStream(
|
||||
obs,
|
||||
mockConnectedFuse{},
|
||||
&NamedTunnelConfig{},
|
||||
connIndex,
|
||||
nil,
|
||||
nil,
|
||||
1*time.Second,
|
||||
)
|
||||
return NewHTTP2Connection(
|
||||
originConn,
|
||||
testConfig,
|
||||
&NamedTunnelConfig{},
|
||||
&pogs.ConnectionOptions{},
|
||||
NewObserver(&log, &log, false),
|
||||
obs,
|
||||
connIndex,
|
||||
mockConnectedFuse{},
|
||||
nil,
|
||||
controlStream,
|
||||
&log,
|
||||
), edgeConn
|
||||
}
|
||||
|
||||
@@ -104,9 +113,9 @@ func TestServeHTTP(t *testing.T) {
|
||||
require.Equal(t, test.expectedBody, respBody)
|
||||
}
|
||||
if test.isProxyError {
|
||||
require.Equal(t, responseMetaHeaderCfd, resp.Header.Get(ResponseMetaHeaderField))
|
||||
require.Equal(t, responseMetaHeaderCfd, resp.Header.Get(ResponseMetaHeader))
|
||||
} else {
|
||||
require.Equal(t, responseMetaHeaderOrigin, resp.Header.Get(ResponseMetaHeaderField))
|
||||
require.Equal(t, responseMetaHeaderOrigin, resp.Header.Get(ResponseMetaHeader))
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
@@ -192,7 +201,7 @@ func TestServeWS(t *testing.T) {
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/ws", readPipe)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set(internalUpgradeHeader, websocketUpgrade)
|
||||
req.Header.Set(InternalUpgradeHeader, WebsocketUpgrade)
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
@@ -212,7 +221,7 @@ func TestServeWS(t *testing.T) {
|
||||
resp := respWriter.Result()
|
||||
// http2RespWriter should rewrite status 101 to 200
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.Equal(t, responseMetaHeaderOrigin, resp.Header.Get(ResponseMetaHeaderField))
|
||||
require.Equal(t, responseMetaHeaderOrigin, resp.Header.Get(ResponseMetaHeader))
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
@@ -224,7 +233,18 @@ func TestServeControlStream(t *testing.T) {
|
||||
registered: make(chan struct{}),
|
||||
unregistered: make(chan struct{}),
|
||||
}
|
||||
http2Conn.newRPCClientFunc = rpcClientFactory.newMockRPCClient
|
||||
|
||||
obs := NewObserver(&log, &log, false)
|
||||
controlStream := NewControlStream(
|
||||
obs,
|
||||
mockConnectedFuse{},
|
||||
&NamedTunnelConfig{},
|
||||
1,
|
||||
rpcClientFactory.newMockRPCClient,
|
||||
nil,
|
||||
1*time.Second,
|
||||
)
|
||||
http2Conn.controlStreamHandler = controlStream
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var wg sync.WaitGroup
|
||||
@@ -236,7 +256,7 @@ func TestServeControlStream(t *testing.T) {
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/", nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set(internalUpgradeHeader, controlStreamUpgrade)
|
||||
req.Header.Set(InternalUpgradeHeader, ControlStreamUpgrade)
|
||||
|
||||
edgeHTTP2Conn, err := testTransport.NewClientConn(edgeConn)
|
||||
require.NoError(t, err)
|
||||
@@ -263,7 +283,18 @@ func TestFailRegistration(t *testing.T) {
|
||||
registered: make(chan struct{}),
|
||||
unregistered: make(chan struct{}),
|
||||
}
|
||||
http2Conn.newRPCClientFunc = rpcClientFactory.newMockRPCClient
|
||||
|
||||
obs := NewObserver(&log, &log, false)
|
||||
controlStream := NewControlStream(
|
||||
obs,
|
||||
mockConnectedFuse{},
|
||||
&NamedTunnelConfig{},
|
||||
http2Conn.connIndex,
|
||||
rpcClientFactory.newMockRPCClient,
|
||||
nil,
|
||||
1*time.Second,
|
||||
)
|
||||
http2Conn.controlStreamHandler = controlStream
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var wg sync.WaitGroup
|
||||
@@ -275,7 +306,7 @@ func TestFailRegistration(t *testing.T) {
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/", nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set(internalUpgradeHeader, controlStreamUpgrade)
|
||||
req.Header.Set(InternalUpgradeHeader, ControlStreamUpgrade)
|
||||
|
||||
edgeHTTP2Conn, err := testTransport.NewClientConn(edgeConn)
|
||||
require.NoError(t, err)
|
||||
@@ -296,10 +327,21 @@ func TestGracefulShutdownHTTP2(t *testing.T) {
|
||||
unregistered: make(chan struct{}),
|
||||
}
|
||||
events := &eventCollectorSink{}
|
||||
http2Conn.newRPCClientFunc = rpcClientFactory.newMockRPCClient
|
||||
http2Conn.observer.RegisterSink(events)
|
||||
|
||||
shutdownC := make(chan struct{})
|
||||
http2Conn.gracefulShutdownC = shutdownC
|
||||
obs := NewObserver(&log, &log, false)
|
||||
obs.RegisterSink(events)
|
||||
controlStream := NewControlStream(
|
||||
obs,
|
||||
mockConnectedFuse{},
|
||||
&NamedTunnelConfig{},
|
||||
http2Conn.connIndex,
|
||||
rpcClientFactory.newMockRPCClient,
|
||||
shutdownC,
|
||||
1*time.Second,
|
||||
)
|
||||
|
||||
http2Conn.controlStreamHandler = controlStream
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var wg sync.WaitGroup
|
||||
@@ -311,7 +353,7 @@ func TestGracefulShutdownHTTP2(t *testing.T) {
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/", nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set(internalUpgradeHeader, controlStreamUpgrade)
|
||||
req.Header.Set(InternalUpgradeHeader, ControlStreamUpgrade)
|
||||
|
||||
edgeHTTP2Conn, err := testTransport.NewClientConn(edgeConn)
|
||||
require.NoError(t, err)
|
||||
@@ -338,7 +380,7 @@ func TestGracefulShutdownHTTP2(t *testing.T) {
|
||||
case <-time.Tick(time.Second):
|
||||
t.Fatal("timeout out waiting for unregistered signal")
|
||||
}
|
||||
assert.True(t, http2Conn.stoppedGracefully)
|
||||
assert.True(t, controlStream.IsStopped())
|
||||
|
||||
cancel()
|
||||
wg.Wait()
|
||||
|
||||
@@ -4,8 +4,9 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -414,4 +415,4 @@ func newTunnelMetrics() *tunnelMetrics {
|
||||
tunnelMetricsInternal.metrics = initTunnelMetrics()
|
||||
})
|
||||
return tunnelMetricsInternal.metrics
|
||||
}
|
||||
}
|
||||
|
||||
+8
-52
@@ -1,12 +1,8 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
@@ -54,53 +50,6 @@ func (o *Observer) logServerInfo(connIndex uint8, location, msg string) {
|
||||
o.metrics.registerServerLocation(uint8ToString(connIndex), location)
|
||||
}
|
||||
|
||||
func (o *Observer) logTrialHostname(registration *tunnelpogs.TunnelRegistration) error {
|
||||
// Print out the user's trial zone URL in a nice box (if they requested and got one and UI flag is not set)
|
||||
if !o.uiEnabled {
|
||||
if registrationURL, err := url.Parse(registration.Url); err == nil {
|
||||
for _, line := range asciiBox(trialZoneMsg(registrationURL.String()), 2) {
|
||||
o.log.Info().Msg(line)
|
||||
}
|
||||
} else {
|
||||
o.log.Error().Msg("Failed to connect tunnel, please try again.")
|
||||
return fmt.Errorf("empty URL in response from Cloudflare edge")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Print out the given lines in a nice ASCII box.
|
||||
func asciiBox(lines []string, padding int) (box []string) {
|
||||
maxLen := maxLen(lines)
|
||||
spacer := strings.Repeat(" ", padding)
|
||||
|
||||
border := "+" + strings.Repeat("-", maxLen+(padding*2)) + "+"
|
||||
|
||||
box = append(box, border)
|
||||
for _, line := range lines {
|
||||
box = append(box, "|"+spacer+line+strings.Repeat(" ", maxLen-len(line))+spacer+"|")
|
||||
}
|
||||
box = append(box, border)
|
||||
return
|
||||
}
|
||||
|
||||
func maxLen(lines []string) int {
|
||||
max := 0
|
||||
for _, line := range lines {
|
||||
if len(line) > max {
|
||||
max = len(line)
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
func trialZoneMsg(url string) []string {
|
||||
return []string{
|
||||
"Your free tunnel has started! Visit it:",
|
||||
" " + url,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Observer) sendRegisteringEvent(connIndex uint8) {
|
||||
o.sendEvent(Event{Index: connIndex, EventType: RegisteringTunnel})
|
||||
}
|
||||
@@ -109,8 +58,15 @@ func (o *Observer) sendConnectedEvent(connIndex uint8, location string) {
|
||||
o.sendEvent(Event{Index: connIndex, EventType: Connected, Location: location})
|
||||
}
|
||||
|
||||
func (o *Observer) sendURL(url string) {
|
||||
func (o *Observer) SendURL(url string) {
|
||||
o.sendEvent(Event{EventType: SetURL, URL: url})
|
||||
|
||||
if !strings.HasPrefix(url, "https://") {
|
||||
// We add https:// in the prefix for backwards compatibility as we used to do that with the old free tunnels
|
||||
// and some tools (like `wrangler tail`) are regexp-ing for that specifically.
|
||||
url = "https://" + url
|
||||
}
|
||||
o.metrics.userHostnamesCounts.WithLabelValues(url).Inc()
|
||||
}
|
||||
|
||||
func (o *Observer) SendReconnect(connIndex uint8) {
|
||||
|
||||
@@ -6,9 +6,28 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSendUrl(t *testing.T) {
|
||||
observer := NewObserver(&log, &log, false)
|
||||
|
||||
observer.SendURL("my-url.com")
|
||||
assert.Equal(t, 1.0, getCounterValue(t, observer.metrics.userHostnamesCounts, "https://my-url.com"))
|
||||
|
||||
observer.SendURL("https://another-long-one.com")
|
||||
assert.Equal(t, 1.0, getCounterValue(t, observer.metrics.userHostnamesCounts, "https://another-long-one.com"))
|
||||
}
|
||||
|
||||
func getCounterValue(t *testing.T, metric *prometheus.CounterVec, val string) float64 {
|
||||
var m = &dto.Metric{}
|
||||
err := metric.WithLabelValues(val).Write(m)
|
||||
assert.NoError(t, err)
|
||||
return m.Counter.GetValue()
|
||||
}
|
||||
|
||||
func TestRegisterServerLocation(t *testing.T) {
|
||||
m := newTunnelMetrics()
|
||||
tunnels := 20
|
||||
@@ -67,10 +86,9 @@ func TestObserverEventsDontBlock(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type eventCollectorSink struct {
|
||||
observedEvents []Event
|
||||
mu sync.Mutex
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (s *eventCollectorSink) OnTunnelEvent(event Event) {
|
||||
@@ -83,4 +101,4 @@ func (s *eventCollectorSink) assertSawEvent(t *testing.T, event Event) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
assert.Contains(t, s.observedEvents, event)
|
||||
}
|
||||
}
|
||||
|
||||
+53
-15
@@ -15,33 +15,29 @@ const (
|
||||
edgeH2muxTLSServerName = "cftunnel.com"
|
||||
// edgeH2TLSServerName is the server name to establish http2 connection with edge
|
||||
edgeH2TLSServerName = "h2.cftunnel.com"
|
||||
// edgeQUICServerName is the server name to establish quic connection with edge.
|
||||
edgeQUICServerName = "quic.cftunnel.com"
|
||||
// threshold to switch back to h2mux when the user intentionally pick --protocol http2
|
||||
explicitHTTP2FallbackThreshold = -1
|
||||
autoSelectFlag = "auto"
|
||||
)
|
||||
|
||||
var (
|
||||
ProtocolList = []Protocol{H2mux, HTTP2}
|
||||
// ProtocolList represents a list of supported protocols for communication with the edge.
|
||||
ProtocolList = []Protocol{H2mux, HTTP2, QUIC}
|
||||
)
|
||||
|
||||
type Protocol int64
|
||||
|
||||
const (
|
||||
// H2mux protocol can be used both with Classic and Named Tunnels. .
|
||||
H2mux Protocol = iota
|
||||
// HTTP2 is used only with named tunnels. It's more efficient than H2mux for L4 proxying.
|
||||
HTTP2
|
||||
// QUIC is used only with named tunnels.
|
||||
QUIC
|
||||
)
|
||||
|
||||
func (p Protocol) ServerName() string {
|
||||
switch p {
|
||||
case H2mux:
|
||||
return edgeH2muxTLSServerName
|
||||
case HTTP2:
|
||||
return edgeH2TLSServerName
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback returns the fallback protocol and whether the protocol has a fallback
|
||||
func (p Protocol) fallback() (Protocol, bool) {
|
||||
switch p {
|
||||
@@ -49,6 +45,8 @@ func (p Protocol) fallback() (Protocol, bool) {
|
||||
return 0, false
|
||||
case HTTP2:
|
||||
return H2mux, true
|
||||
case QUIC:
|
||||
return HTTP2, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
@@ -60,11 +58,38 @@ func (p Protocol) String() string {
|
||||
return "h2mux"
|
||||
case HTTP2:
|
||||
return "http2"
|
||||
case QUIC:
|
||||
return "quic"
|
||||
default:
|
||||
return fmt.Sprintf("unknown protocol")
|
||||
}
|
||||
}
|
||||
|
||||
func (p Protocol) TLSSettings() *TLSSettings {
|
||||
switch p {
|
||||
case H2mux:
|
||||
return &TLSSettings{
|
||||
ServerName: edgeH2muxTLSServerName,
|
||||
}
|
||||
case HTTP2:
|
||||
return &TLSSettings{
|
||||
ServerName: edgeH2TLSServerName,
|
||||
}
|
||||
case QUIC:
|
||||
return &TLSSettings{
|
||||
ServerName: edgeQUICServerName,
|
||||
NextProtos: []string{"argotunnel"},
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type TLSSettings struct {
|
||||
ServerName string
|
||||
NextProtos []string
|
||||
}
|
||||
|
||||
type ProtocolSelector interface {
|
||||
Current() Protocol
|
||||
Fallback() (Protocol, bool)
|
||||
@@ -154,10 +179,16 @@ func NewProtocolSelector(
|
||||
}, nil
|
||||
}
|
||||
|
||||
// warp routing can only be served over http2 connections
|
||||
// warp routing cannot be served over h2mux connections
|
||||
if warpRoutingEnabled {
|
||||
if protocolFlag == H2mux.String() {
|
||||
log.Warn().Msg("Warp routing is only supported by http2 protocol. Upgrading protocol to http2")
|
||||
log.Warn().Msg("Warp routing is not supported in h2mux protocol. Upgrading to http2 to allow it.")
|
||||
}
|
||||
|
||||
if protocolFlag == QUIC.String() {
|
||||
return &staticProtocolSelector{
|
||||
current: QUIC,
|
||||
}, nil
|
||||
}
|
||||
return &staticProtocolSelector{
|
||||
current: HTTP2,
|
||||
@@ -170,9 +201,16 @@ func NewProtocolSelector(
|
||||
}, nil
|
||||
}
|
||||
|
||||
if protocolFlag == QUIC.String() {
|
||||
return newAutoProtocolSelector(QUIC, explicitHTTP2FallbackThreshold, fetchFunc, ttl, log), nil
|
||||
}
|
||||
|
||||
http2Percentage, err := fetchFunc()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
log.Err(err).Msg("Unable to lookup protocol. Defaulting to `http2`. If this fails, you can set `--protocol h2mux` in your cloudflared command.")
|
||||
return &staticProtocolSelector{
|
||||
current: HTTP2,
|
||||
}, nil
|
||||
}
|
||||
if protocolFlag == HTTP2.String() {
|
||||
if http2Percentage < 0 {
|
||||
|
||||
@@ -158,7 +158,8 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
protocol: "unknown",
|
||||
fetchFunc: mockFetcherWithError(),
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
wantErr: true,
|
||||
expectedProtocol: HTTP2,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/lucas-clemente/quic-go"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
quicpogs "github.com/cloudflare/cloudflared/quic"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
const (
|
||||
// HTTPHeaderKey is used to get or set http headers in QUIC ALPN if the underlying proxy connection type is HTTP.
|
||||
HTTPHeaderKey = "HttpHeader"
|
||||
// HTTPMethodKey is used to get or set http method in QUIC ALPN if the underlying proxy connection type is HTTP.
|
||||
HTTPMethodKey = "HttpMethod"
|
||||
// HTTPHostKey is used to get or set http Method in QUIC ALPN if the underlying proxy connection type is HTTP.
|
||||
HTTPHostKey = "HttpHost"
|
||||
)
|
||||
|
||||
// QUICConnection represents the type that facilitates Proxying via QUIC streams.
|
||||
type QUICConnection struct {
|
||||
session quic.Session
|
||||
logger *zerolog.Logger
|
||||
httpProxy OriginProxy
|
||||
gracefulShutdownC <-chan struct{}
|
||||
stoppedGracefully bool
|
||||
}
|
||||
|
||||
// NewQUICConnection returns a new instance of QUICConnection.
|
||||
func NewQUICConnection(
|
||||
ctx context.Context,
|
||||
quicConfig *quic.Config,
|
||||
edgeAddr net.Addr,
|
||||
tlsConfig *tls.Config,
|
||||
httpProxy OriginProxy,
|
||||
connOptions *tunnelpogs.ConnectionOptions,
|
||||
controlStreamHandler ControlStreamHandler,
|
||||
observer *Observer,
|
||||
) (*QUICConnection, error) {
|
||||
session, err := quic.DialAddr(edgeAddr.String(), tlsConfig, quicConfig)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to dial to edge")
|
||||
}
|
||||
|
||||
registrationStream, err := session.OpenStream()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to open a registration stream")
|
||||
}
|
||||
|
||||
err = controlStreamHandler.ServeControlStream(ctx, registrationStream, connOptions, false)
|
||||
if err != nil {
|
||||
// Not wrapping error here to be consistent with the http2 message.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &QUICConnection{
|
||||
session: session,
|
||||
httpProxy: httpProxy,
|
||||
logger: observer.log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Serve starts a QUIC session that begins accepting streams.
|
||||
func (q *QUICConnection) Serve(ctx context.Context) error {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
for {
|
||||
stream, err := q.session.AcceptStream(ctx)
|
||||
if err != nil {
|
||||
// context.Canceled is usually a user ctrl+c. We don't want to log an error here as it's intentional.
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return nil
|
||||
}
|
||||
return errors.Wrap(err, "failed to accept QUIC stream")
|
||||
}
|
||||
go func() {
|
||||
defer stream.Close()
|
||||
if err = q.handleStream(stream); err != nil {
|
||||
q.logger.Err(err).Msg("Failed to handle QUIC stream")
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the session with no errors specified.
|
||||
func (q *QUICConnection) Close() {
|
||||
q.session.CloseWithError(0, "")
|
||||
}
|
||||
|
||||
func (q *QUICConnection) handleStream(stream quic.Stream) error {
|
||||
connectRequest, err := quicpogs.ReadConnectRequestData(stream)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch connectRequest.Type {
|
||||
case quicpogs.ConnectionTypeHTTP, quicpogs.ConnectionTypeWebsocket:
|
||||
req, err := buildHTTPRequest(connectRequest, stream)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w := newHTTPResponseAdapter(stream)
|
||||
return q.httpProxy.ProxyHTTP(w, req, connectRequest.Type == quicpogs.ConnectionTypeWebsocket)
|
||||
case quicpogs.ConnectionTypeTCP:
|
||||
rwa := &streamReadWriteAcker{
|
||||
ReadWriter: stream,
|
||||
}
|
||||
return q.httpProxy.ProxyTCP(context.Background(), rwa, &TCPRequest{Dest: connectRequest.Dest})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// streamReadWriteAcker is a light wrapper over QUIC streams with a callback to send response back to
|
||||
// the client.
|
||||
type streamReadWriteAcker struct {
|
||||
io.ReadWriter
|
||||
}
|
||||
|
||||
// AckConnection acks response back to the proxy.
|
||||
func (s *streamReadWriteAcker) AckConnection() error {
|
||||
return quicpogs.WriteConnectResponseData(s, nil)
|
||||
}
|
||||
|
||||
// httpResponseAdapter translates responses written by the HTTP Proxy into ones that can be used in QUIC.
|
||||
type httpResponseAdapter struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func newHTTPResponseAdapter(w io.Writer) httpResponseAdapter {
|
||||
return httpResponseAdapter{w}
|
||||
}
|
||||
|
||||
func (hrw httpResponseAdapter) WriteRespHeaders(status int, header http.Header) error {
|
||||
metadata := make([]quicpogs.Metadata, 0)
|
||||
metadata = append(metadata, quicpogs.Metadata{Key: "HttpStatus", Val: strconv.Itoa(status)})
|
||||
for k, vv := range header {
|
||||
for _, v := range vv {
|
||||
httpHeaderKey := fmt.Sprintf("%s:%s", HTTPHeaderKey, k)
|
||||
metadata = append(metadata, quicpogs.Metadata{Key: httpHeaderKey, Val: v})
|
||||
}
|
||||
}
|
||||
return quicpogs.WriteConnectResponseData(hrw, nil, metadata...)
|
||||
}
|
||||
|
||||
func (hrw httpResponseAdapter) WriteErrorResponse(err error) {
|
||||
quicpogs.WriteConnectResponseData(hrw, err, quicpogs.Metadata{Key: "HttpStatus", Val: strconv.Itoa(http.StatusBadGateway)})
|
||||
}
|
||||
|
||||
func buildHTTPRequest(connectRequest *quicpogs.ConnectRequest, body io.Reader) (*http.Request, error) {
|
||||
metadata := connectRequest.MetadataMap()
|
||||
dest := connectRequest.Dest
|
||||
method := metadata[HTTPMethodKey]
|
||||
host := metadata[HTTPHostKey]
|
||||
|
||||
req, err := http.NewRequest(method, dest, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Host = host
|
||||
for _, metadata := range connectRequest.Metadata {
|
||||
if strings.Contains(metadata.Key, HTTPHeaderKey) {
|
||||
// metadata.Key is off the format httpHeaderKey:<HTTPHeader>
|
||||
httpHeaderKey := strings.Split(metadata.Key, ":")
|
||||
if len(httpHeaderKey) != 2 {
|
||||
return nil, fmt.Errorf("Header Key: %s malformed", metadata.Key)
|
||||
}
|
||||
req.Header.Add(httpHeaderKey[1], metadata.Val)
|
||||
}
|
||||
}
|
||||
// Go's http.Client automatically sends chunked request body if this value is not set on the
|
||||
// *http.Request struct regardless of header:
|
||||
// https://go.googlesource.com/go/+/go1.8rc2/src/net/http/transfer.go#154.
|
||||
if err := setContentLength(req); err != nil {
|
||||
return nil, fmt.Errorf("Error setting content-length: %w", err)
|
||||
}
|
||||
stripWebsocketUpgradeHeader(req)
|
||||
return req, err
|
||||
}
|
||||
|
||||
func setContentLength(req *http.Request) error {
|
||||
var err error
|
||||
if contentLengthStr := req.Header.Get("Content-Length"); contentLengthStr != "" {
|
||||
req.ContentLength, err = strconv.ParseInt(contentLengthStr, 10, 64)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
"github.com/lucas-clemente/quic-go"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
quicpogs "github.com/cloudflare/cloudflared/quic"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
// TestQUICServer tests if a quic server accepts and responds to a quic client with the acceptance protocol.
|
||||
// It also serves as a demonstration for communication with the QUIC connection started by a cloudflared.
|
||||
func TestQUICServer(t *testing.T) {
|
||||
quicConfig := &quic.Config{
|
||||
KeepAlive: true,
|
||||
}
|
||||
|
||||
// Setup test.
|
||||
log := zerolog.New(os.Stdout)
|
||||
|
||||
// Start a UDP Listener for QUIC.
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
udpListener, err := net.ListenUDP(udpAddr.Network(), udpAddr)
|
||||
require.NoError(t, err)
|
||||
defer udpListener.Close()
|
||||
|
||||
// Create a simple tls config.
|
||||
tlsConfig := generateTLSConfig()
|
||||
|
||||
// Create a client config
|
||||
tlsClientConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{"argotunnel"},
|
||||
}
|
||||
|
||||
// Start a mock httpProxy
|
||||
originProxy := &mockOriginProxyWithRequest{}
|
||||
|
||||
// This is simply a sample websocket frame message.
|
||||
wsBuf := &bytes.Buffer{}
|
||||
wsutil.WriteClientText(wsBuf, []byte("Hello"))
|
||||
|
||||
var tests = []struct {
|
||||
desc string
|
||||
dest string
|
||||
connectionType quicpogs.ConnectionType
|
||||
metadata []quicpogs.Metadata
|
||||
message []byte
|
||||
expectedResponse []byte
|
||||
}{
|
||||
{
|
||||
desc: "test http proxy",
|
||||
dest: "/ok",
|
||||
connectionType: quicpogs.ConnectionTypeHTTP,
|
||||
metadata: []quicpogs.Metadata{
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHeader:Cf-Ray",
|
||||
Val: "123123123",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHost",
|
||||
Val: "cf.host",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpMethod",
|
||||
Val: "GET",
|
||||
},
|
||||
},
|
||||
expectedResponse: []byte("OK"),
|
||||
},
|
||||
{
|
||||
desc: "test http body request streaming",
|
||||
dest: "/echo_body",
|
||||
connectionType: quicpogs.ConnectionTypeHTTP,
|
||||
metadata: []quicpogs.Metadata{
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHeader:Cf-Ray",
|
||||
Val: "123123123",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHost",
|
||||
Val: "cf.host",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpMethod",
|
||||
Val: "POST",
|
||||
},
|
||||
},
|
||||
message: []byte("This is the message body"),
|
||||
expectedResponse: []byte("This is the message body"),
|
||||
},
|
||||
{
|
||||
desc: "test ws proxy",
|
||||
dest: "/ok",
|
||||
connectionType: quicpogs.ConnectionTypeWebsocket,
|
||||
metadata: []quicpogs.Metadata{
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHeader:Cf-Cloudflared-Proxy-Connection-Upgrade",
|
||||
Val: "Websocket",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHeader:Another-Header",
|
||||
Val: "Misc",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHost",
|
||||
Val: "cf.host",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpMethod",
|
||||
Val: "get",
|
||||
},
|
||||
},
|
||||
message: wsBuf.Bytes(),
|
||||
expectedResponse: []byte{0x81, 0x5, 0x48, 0x65, 0x6c, 0x6c, 0x6f},
|
||||
},
|
||||
{
|
||||
desc: "test tcp proxy",
|
||||
connectionType: quicpogs.ConnectionTypeTCP,
|
||||
metadata: []quicpogs.Metadata{},
|
||||
message: []byte("Here is some tcp data"),
|
||||
expectedResponse: []byte("Here is some tcp data"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var wg sync.WaitGroup
|
||||
go func() {
|
||||
wg.Add(1)
|
||||
defer wg.Done()
|
||||
quicServer(
|
||||
t, udpListener, tlsConfig, quicConfig,
|
||||
test.dest, test.connectionType, test.metadata, test.message, test.expectedResponse,
|
||||
)
|
||||
}()
|
||||
|
||||
controlStream := fakeControlStream{}
|
||||
|
||||
qC, err := NewQUICConnection(
|
||||
ctx,
|
||||
quicConfig,
|
||||
udpListener.LocalAddr(),
|
||||
tlsClientConfig,
|
||||
originProxy,
|
||||
&pogs.ConnectionOptions{},
|
||||
controlStream,
|
||||
NewObserver(&log, &log, false),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
go qC.Serve(ctx)
|
||||
|
||||
wg.Wait()
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fakeControlStream struct {
|
||||
ControlStreamHandler
|
||||
}
|
||||
|
||||
func (fakeControlStream) ServeControlStream(ctx context.Context, rw io.ReadWriteCloser, connOptions *tunnelpogs.ConnectionOptions, shouldWaitForUnregister bool) error {
|
||||
return nil
|
||||
}
|
||||
func (fakeControlStream) IsStopped() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func quicServer(
|
||||
t *testing.T,
|
||||
conn net.PacketConn,
|
||||
tlsConf *tls.Config,
|
||||
config *quic.Config,
|
||||
dest string,
|
||||
connectionType quicpogs.ConnectionType,
|
||||
metadata []quicpogs.Metadata,
|
||||
message []byte,
|
||||
expectedResponse []byte,
|
||||
) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
earlyListener, err := quic.Listen(conn, tlsConf, config)
|
||||
require.NoError(t, err)
|
||||
|
||||
session, err := earlyListener.Accept(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
stream, err := session.OpenStreamSync(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
err = quicpogs.WriteConnectRequestData(stream, dest, connectionType, metadata...)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = quicpogs.ReadConnectResponseData(stream)
|
||||
require.NoError(t, err)
|
||||
|
||||
if message != nil {
|
||||
// ALPN successful. Write data.
|
||||
_, err := stream.Write([]byte(message))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
response := make([]byte, len(expectedResponse))
|
||||
stream.Read(response)
|
||||
require.NoError(t, err)
|
||||
|
||||
// For now it is an echo server. Verify if the same data is returned.
|
||||
assert.Equal(t, expectedResponse, response)
|
||||
}
|
||||
|
||||
// Setup a bare-bones TLS config for the server
|
||||
func generateTLSConfig() *tls.Config {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
template := x509.Certificate{SerialNumber: big.NewInt(1)}
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||||
|
||||
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{tlsCert},
|
||||
NextProtos: []string{"argotunnel"},
|
||||
}
|
||||
}
|
||||
|
||||
type mockOriginProxyWithRequest struct{}
|
||||
|
||||
func (moc *mockOriginProxyWithRequest) ProxyHTTP(w ResponseWriter, r *http.Request, isWebsocket bool) error {
|
||||
// These are a series of crude tests to ensure the headers and http related data is transferred from
|
||||
// metadata.
|
||||
if r.Method == "" {
|
||||
return errors.New("method not sent")
|
||||
}
|
||||
if r.Host == "" {
|
||||
return errors.New("host not sent")
|
||||
}
|
||||
if len(r.Header) == 0 {
|
||||
return errors.New("headers not set")
|
||||
}
|
||||
|
||||
if isWebsocket {
|
||||
return wsEndpoint(w, r)
|
||||
}
|
||||
switch r.URL.Path {
|
||||
case "/ok":
|
||||
originRespEndpoint(w, http.StatusOK, []byte(http.StatusText(http.StatusOK)))
|
||||
case "/echo_body":
|
||||
resp := &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
}
|
||||
_ = w.WriteRespHeaders(resp.StatusCode, resp.Header)
|
||||
io.Copy(w, r.Body)
|
||||
case "/error":
|
||||
return fmt.Errorf("Failed to proxy to origin")
|
||||
default:
|
||||
originRespEndpoint(w, http.StatusNotFound, []byte("page not found"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestBuildHTTPRequest(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
connectRequest *quicpogs.ConnectRequest
|
||||
req *http.Request
|
||||
}{
|
||||
{
|
||||
name: "check if http.Request is built correctly with content length",
|
||||
connectRequest: &quicpogs.ConnectRequest{
|
||||
Dest: "http://test.com",
|
||||
Metadata: []quicpogs.Metadata{
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHeader:Cf-Cloudflared-Proxy-Connection-Upgrade",
|
||||
Val: "Websocket",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHeader:Content-Length",
|
||||
Val: "514",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHeader:Another-Header",
|
||||
Val: "Misc",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHost",
|
||||
Val: "cf.host",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpMethod",
|
||||
Val: "get",
|
||||
},
|
||||
},
|
||||
},
|
||||
req: &http.Request{
|
||||
Method: "get",
|
||||
URL: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "test.com",
|
||||
},
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Header: http.Header{
|
||||
"Another-Header": []string{"Misc"},
|
||||
"Content-Length": []string{"514"},
|
||||
},
|
||||
ContentLength: 514,
|
||||
Host: "cf.host",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "if content length isn't part of request headers, then it's not set",
|
||||
connectRequest: &quicpogs.ConnectRequest{
|
||||
Dest: "http://test.com",
|
||||
Metadata: []quicpogs.Metadata{
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHeader:Cf-Cloudflared-Proxy-Connection-Upgrade",
|
||||
Val: "Websocket",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHeader:Another-Header",
|
||||
Val: "Misc",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHost",
|
||||
Val: "cf.host",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpMethod",
|
||||
Val: "get",
|
||||
},
|
||||
},
|
||||
},
|
||||
req: &http.Request{
|
||||
Method: "get",
|
||||
URL: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "test.com",
|
||||
},
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Header: http.Header{
|
||||
"Another-Header": []string{"Misc"},
|
||||
},
|
||||
ContentLength: 0,
|
||||
Host: "cf.host",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req, err := buildHTTPRequest(test.connectRequest, nil)
|
||||
assert.NoError(t, err)
|
||||
test.req = test.req.WithContext(req.Context())
|
||||
assert.Equal(t, test.req, req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (moc *mockOriginProxyWithRequest) ProxyTCP(ctx context.Context, rwa ReadWriteAcker, tcpRequest *TCPRequest) error {
|
||||
rwa.AckConnection()
|
||||
io.Copy(rwa, rwa)
|
||||
return nil
|
||||
}
|
||||
+3
-13
@@ -6,11 +6,11 @@ import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"zombiezen.com/go/capnproto2/rpc"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
type tunnelServerClient struct {
|
||||
@@ -159,8 +159,6 @@ func (h *h2muxConnection) registerTunnel(ctx context.Context, credentialSetter C
|
||||
return h.processRegisterTunnelError(registrationErr, register)
|
||||
}
|
||||
|
||||
// Send free tunnel URL to UI
|
||||
h.observer.sendURL(registration.Url)
|
||||
credentialSetter.SetEventDigest(h.connIndex, registration.EventDigest)
|
||||
return h.processRegistrationSuccess(registration, register, credentialSetter, classicTunnel)
|
||||
}
|
||||
@@ -187,14 +185,6 @@ func (h *h2muxConnection) processRegistrationSuccess(
|
||||
h.observer.log.Info().Msgf("Each HA connection's tunnel IDs: %v", h.observer.metrics.tunnelsHA.String())
|
||||
}
|
||||
|
||||
// Print out the user's trial zone URL in a nice box (if they requested and got one and UI flag is not set)
|
||||
if classicTunnel.IsTrialZone() {
|
||||
err := h.observer.logTrialHostname(registration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
credentialManager.SetConnDigest(h.connIndex, registration.ConnDigest)
|
||||
h.observer.metrics.userHostnamesCounts.WithLabelValues(registration.Url).Inc()
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.15.7 as builder
|
||||
FROM golang:1.16.4 as builder
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0
|
||||
WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
||||
|
||||
@@ -22,6 +22,8 @@ const (
|
||||
dotServerName = "cloudflare-dns.com"
|
||||
dotServerAddr = "1.1.1.1:853"
|
||||
dotTimeout = 15 * time.Second
|
||||
|
||||
logFieldAddress = "address"
|
||||
)
|
||||
|
||||
// Redeclare network functions so they can be overridden in tests.
|
||||
@@ -30,6 +32,12 @@ var (
|
||||
netLookupIP = net.LookupIP
|
||||
)
|
||||
|
||||
// EdgeAddr is a representation of possible ways to refer an edge location.
|
||||
type EdgeAddr struct {
|
||||
TCP *net.TCPAddr
|
||||
UDP *net.UDPAddr
|
||||
}
|
||||
|
||||
// If the call to net.LookupSRV fails, try to fall back to DoT from Cloudflare directly.
|
||||
//
|
||||
// Note: Instead of DoT, we could also have used DoH. Either of these:
|
||||
@@ -53,7 +61,9 @@ var friendlyDNSErrorLines = []string{
|
||||
}
|
||||
|
||||
// EdgeDiscovery implements HA service discovery lookup.
|
||||
func edgeDiscovery(log *zerolog.Logger) ([][]*net.TCPAddr, error) {
|
||||
func edgeDiscovery(log *zerolog.Logger, srvService string) ([][]*EdgeAddr, error) {
|
||||
log.Debug().Str("domain", "_"+srvService+"._"+srvProto+"."+srvName).Msg("looking up edge SRV record")
|
||||
|
||||
_, addrs, err := netLookupSRV(srvService, srvProto, srvName)
|
||||
if err != nil {
|
||||
_, fallbackAddrs, fallbackErr := fallbackLookupSRV(srvService, srvProto, srvName)
|
||||
@@ -69,19 +79,19 @@ func edgeDiscovery(log *zerolog.Logger) ([][]*net.TCPAddr, error) {
|
||||
addrs = fallbackAddrs
|
||||
}
|
||||
|
||||
var resolvedIPsPerCNAME [][]*net.TCPAddr
|
||||
var resolvedAddrPerCNAME [][]*EdgeAddr
|
||||
for _, addr := range addrs {
|
||||
ips, err := resolveSRVToTCP(addr)
|
||||
edgeAddrs, err := resolveSRV(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resolvedIPsPerCNAME = append(resolvedIPsPerCNAME, ips)
|
||||
resolvedAddrPerCNAME = append(resolvedAddrPerCNAME, edgeAddrs)
|
||||
}
|
||||
|
||||
return resolvedIPsPerCNAME, nil
|
||||
return resolvedAddrPerCNAME, nil
|
||||
}
|
||||
|
||||
func lookupSRVWithDOT(service, proto, name string) (cname string, addrs []*net.SRV, err error) {
|
||||
func lookupSRVWithDOT(srvService string, srvProto string, srvName string) (cname string, addrs []*net.SRV, err error) {
|
||||
// Inspiration: https://github.com/artyom/dot/blob/master/dot.go
|
||||
r := &net.Resolver{
|
||||
PreferGo: true,
|
||||
@@ -100,7 +110,7 @@ func lookupSRVWithDOT(service, proto, name string) (cname string, addrs []*net.S
|
||||
return r.LookupSRV(ctx, srvService, srvProto, srvName)
|
||||
}
|
||||
|
||||
func resolveSRVToTCP(srv *net.SRV) ([]*net.TCPAddr, error) {
|
||||
func resolveSRV(srv *net.SRV) ([]*EdgeAddr, error) {
|
||||
ips, err := netLookupIP(srv.Target)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Couldn't resolve SRV record %v", srv)
|
||||
@@ -108,23 +118,36 @@ func resolveSRVToTCP(srv *net.SRV) ([]*net.TCPAddr, error) {
|
||||
if len(ips) == 0 {
|
||||
return nil, fmt.Errorf("SRV record %v had no IPs", srv)
|
||||
}
|
||||
addrs := make([]*net.TCPAddr, len(ips))
|
||||
addrs := make([]*EdgeAddr, len(ips))
|
||||
for i, ip := range ips {
|
||||
addrs[i] = &net.TCPAddr{IP: ip, Port: int(srv.Port)}
|
||||
addrs[i] = &EdgeAddr{
|
||||
TCP: &net.TCPAddr{IP: ip, Port: int(srv.Port)},
|
||||
UDP: &net.UDPAddr{IP: ip, Port: int(srv.Port)},
|
||||
}
|
||||
}
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
// ResolveAddrs resolves TCP address given a list of addresses. Address can be a hostname, however, it will return at most one
|
||||
// of the hostname's IP addresses.
|
||||
func ResolveAddrs(addrs []string, log *zerolog.Logger) (resolved []*net.TCPAddr) {
|
||||
func ResolveAddrs(addrs []string, log *zerolog.Logger) (resolved []*EdgeAddr) {
|
||||
for _, addr := range addrs {
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
|
||||
if err != nil {
|
||||
log.Err(err).Msgf("Failed to resolve %s", addr)
|
||||
} else {
|
||||
resolved = append(resolved, tcpAddr)
|
||||
log.Error().Str(logFieldAddress, addr).Err(err).Msg("failed to resolve to TCP address")
|
||||
continue
|
||||
}
|
||||
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||
if err != nil {
|
||||
log.Error().Str(logFieldAddress, addr).Err(err).Msg("failed to resolve to UDP address")
|
||||
continue
|
||||
}
|
||||
resolved = append(resolved, &EdgeAddr{
|
||||
TCP: tcpAddr,
|
||||
UDP: udpAddr,
|
||||
})
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package allregions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func (ea *EdgeAddr) String() string {
|
||||
return fmt.Sprintf("%s-%s", ea.TCP, ea.UDP)
|
||||
}
|
||||
|
||||
func TestEdgeDiscovery(t *testing.T) {
|
||||
mockAddrs := newMockAddrs(19, 2, 5)
|
||||
netLookupSRV = mockNetLookupSRV(mockAddrs)
|
||||
@@ -20,7 +25,7 @@ func TestEdgeDiscovery(t *testing.T) {
|
||||
}
|
||||
|
||||
l := zerolog.Nop()
|
||||
addrLists, err := edgeDiscovery(&l)
|
||||
addrLists, err := edgeDiscovery(&l, "")
|
||||
assert.NoError(t, err)
|
||||
actualAddrSet := map[string]bool{}
|
||||
for _, addrs := range addrLists {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
type mockAddrs struct {
|
||||
// a set of synthetic SRV records
|
||||
addrMap map[net.SRV][]*net.TCPAddr
|
||||
addrMap map[net.SRV][]*EdgeAddr
|
||||
// the total number of addresses, aggregated across addrMap.
|
||||
// For the convenience of test code that would otherwise have to compute
|
||||
// this by hand every time.
|
||||
@@ -19,19 +19,24 @@ type mockAddrs struct {
|
||||
}
|
||||
|
||||
func newMockAddrs(port uint16, numRegions uint8, numAddrsPerRegion uint8) mockAddrs {
|
||||
addrMap := make(map[net.SRV][]*net.TCPAddr)
|
||||
addrMap := make(map[net.SRV][]*EdgeAddr)
|
||||
numAddrs := 0
|
||||
|
||||
for r := uint8(0); r < numRegions; r++ {
|
||||
var (
|
||||
srv = net.SRV{Target: fmt.Sprintf("test-region-%v.example.com", r), Port: port}
|
||||
addrs []*net.TCPAddr
|
||||
addrs []*EdgeAddr
|
||||
)
|
||||
for a := uint8(0); a < numAddrsPerRegion; a++ {
|
||||
addrs = append(addrs, &net.TCPAddr{
|
||||
tcpAddr := &net.TCPAddr{
|
||||
IP: net.ParseIP(fmt.Sprintf("10.0.%v.%v", r, a)),
|
||||
Port: int(port),
|
||||
})
|
||||
}
|
||||
udpAddr := &net.UDPAddr{
|
||||
IP: net.ParseIP(fmt.Sprintf("10.0.%v.%v", r, a)),
|
||||
Port: int(port),
|
||||
}
|
||||
addrs = append(addrs, &EdgeAddr{tcpAddr, udpAddr})
|
||||
}
|
||||
addrMap[srv] = addrs
|
||||
numAddrs += len(addrs)
|
||||
@@ -74,13 +79,13 @@ func mockNetLookupIP(
|
||||
m mockAddrs,
|
||||
) func(host string) ([]net.IP, error) {
|
||||
return func(host string) ([]net.IP, error) {
|
||||
for srv, tcpAddrs := range m.addrMap {
|
||||
for srv, addrs := range m.addrMap {
|
||||
if srv.Target != host {
|
||||
continue
|
||||
}
|
||||
result := make([]net.IP, len(tcpAddrs))
|
||||
for i, tcpAddr := range tcpAddrs {
|
||||
result[i] = tcpAddr.IP
|
||||
result := make([]net.IP, len(addrs))
|
||||
for i, addr := range addrs {
|
||||
result[i] = addr.TCP.IP
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
package allregions
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
// Region contains cloudflared edge addresses. The edge is partitioned into several regions for
|
||||
// redundancy purposes.
|
||||
type Region struct {
|
||||
connFor map[*net.TCPAddr]UsedBy
|
||||
connFor map[*EdgeAddr]UsedBy
|
||||
}
|
||||
|
||||
// NewRegion creates a region with the given addresses, which are all unused.
|
||||
func NewRegion(addrs []*net.TCPAddr) Region {
|
||||
func NewRegion(addrs []*EdgeAddr) Region {
|
||||
// The zero value of UsedBy is Unused(), so we can just initialize the map's values with their
|
||||
// zero values.
|
||||
m := make(map[*net.TCPAddr]UsedBy)
|
||||
connFor := make(map[*EdgeAddr]UsedBy)
|
||||
for _, addr := range addrs {
|
||||
m[addr] = Unused()
|
||||
connFor[addr] = Unused()
|
||||
}
|
||||
return Region{
|
||||
connFor: connFor,
|
||||
}
|
||||
return Region{connFor: m}
|
||||
}
|
||||
|
||||
// AddrUsedBy finds the address used by the given connection in this region.
|
||||
// Returns nil if the connection isn't using any IP.
|
||||
func (r *Region) AddrUsedBy(connID int) *net.TCPAddr {
|
||||
func (r *Region) AddrUsedBy(connID int) *EdgeAddr {
|
||||
for addr, used := range r.connFor {
|
||||
if used.Used && used.ConnID == connID {
|
||||
return addr
|
||||
@@ -45,7 +43,7 @@ func (r Region) AvailableAddrs() int {
|
||||
|
||||
// GetUnusedIP returns a random unused address in this region.
|
||||
// Returns nil if all addresses are in use.
|
||||
func (r Region) GetUnusedIP(excluding *net.TCPAddr) *net.TCPAddr {
|
||||
func (r Region) GetUnusedIP(excluding *EdgeAddr) *EdgeAddr {
|
||||
for addr, usedby := range r.connFor {
|
||||
if !usedby.Used && addr != excluding {
|
||||
return addr
|
||||
@@ -55,7 +53,7 @@ func (r Region) GetUnusedIP(excluding *net.TCPAddr) *net.TCPAddr {
|
||||
}
|
||||
|
||||
// Use the address, assigning it to a proxy connection.
|
||||
func (r Region) Use(addr *net.TCPAddr, connID int) {
|
||||
func (r Region) Use(addr *EdgeAddr, connID int) {
|
||||
if addr == nil {
|
||||
return
|
||||
}
|
||||
@@ -63,7 +61,7 @@ func (r Region) Use(addr *net.TCPAddr, connID int) {
|
||||
}
|
||||
|
||||
// GetAnyAddress returns an arbitrary address from the region.
|
||||
func (r Region) GetAnyAddress() *net.TCPAddr {
|
||||
func (r Region) GetAnyAddress() *EdgeAddr {
|
||||
for addr := range r.connFor {
|
||||
return addr
|
||||
}
|
||||
@@ -72,7 +70,7 @@ func (r Region) GetAnyAddress() *net.TCPAddr {
|
||||
|
||||
// GiveBack the address, ensuring it is no longer assigned to an IP.
|
||||
// Returns true if the address is in this region.
|
||||
func (r Region) GiveBack(addr *net.TCPAddr) (ok bool) {
|
||||
func (r Region) GiveBack(addr *EdgeAddr) (ok bool) {
|
||||
if _, ok := r.connFor[addr]; !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
package allregions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRegion_New(t *testing.T) {
|
||||
r := NewRegion([]*net.TCPAddr{&addr0, &addr1, &addr2})
|
||||
fmt.Println(r.connFor)
|
||||
r := NewRegion([]*EdgeAddr{&addr0, &addr1, &addr2})
|
||||
if r.AvailableAddrs() != 3 {
|
||||
t.Errorf("r.AvailableAddrs() == %v but want 3", r.AvailableAddrs())
|
||||
}
|
||||
@@ -17,7 +14,7 @@ func TestRegion_New(t *testing.T) {
|
||||
|
||||
func TestRegion_AddrUsedBy(t *testing.T) {
|
||||
type fields struct {
|
||||
connFor map[*net.TCPAddr]UsedBy
|
||||
connFor map[*EdgeAddr]UsedBy
|
||||
}
|
||||
type args struct {
|
||||
connID int
|
||||
@@ -26,11 +23,11 @@ func TestRegion_AddrUsedBy(t *testing.T) {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *net.TCPAddr
|
||||
want *EdgeAddr
|
||||
}{
|
||||
{
|
||||
name: "happy trivial test",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: InUse(0),
|
||||
}},
|
||||
args: args{connID: 0},
|
||||
@@ -38,7 +35,7 @@ func TestRegion_AddrUsedBy(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "sad trivial test",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: InUse(0),
|
||||
}},
|
||||
args: args{connID: 1},
|
||||
@@ -46,7 +43,7 @@ func TestRegion_AddrUsedBy(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "sad test",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: InUse(0),
|
||||
&addr1: InUse(1),
|
||||
&addr2: InUse(2),
|
||||
@@ -56,7 +53,7 @@ func TestRegion_AddrUsedBy(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "happy test",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: InUse(0),
|
||||
&addr1: InUse(1),
|
||||
&addr2: InUse(2),
|
||||
@@ -79,7 +76,7 @@ func TestRegion_AddrUsedBy(t *testing.T) {
|
||||
|
||||
func TestRegion_AvailableAddrs(t *testing.T) {
|
||||
type fields struct {
|
||||
connFor map[*net.TCPAddr]UsedBy
|
||||
connFor map[*EdgeAddr]UsedBy
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -88,7 +85,7 @@ func TestRegion_AvailableAddrs(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "contains addresses",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: InUse(0),
|
||||
&addr1: Unused(),
|
||||
&addr2: InUse(2),
|
||||
@@ -97,7 +94,7 @@ func TestRegion_AvailableAddrs(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "all free",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: Unused(),
|
||||
&addr1: Unused(),
|
||||
&addr2: Unused(),
|
||||
@@ -106,7 +103,7 @@ func TestRegion_AvailableAddrs(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "all used",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: InUse(0),
|
||||
&addr1: InUse(1),
|
||||
&addr2: InUse(2),
|
||||
@@ -115,7 +112,7 @@ func TestRegion_AvailableAddrs(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{}},
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{}},
|
||||
want: 0,
|
||||
},
|
||||
}
|
||||
@@ -133,20 +130,20 @@ func TestRegion_AvailableAddrs(t *testing.T) {
|
||||
|
||||
func TestRegion_GetUnusedIP(t *testing.T) {
|
||||
type fields struct {
|
||||
connFor map[*net.TCPAddr]UsedBy
|
||||
connFor map[*EdgeAddr]UsedBy
|
||||
}
|
||||
type args struct {
|
||||
excluding *net.TCPAddr
|
||||
excluding *EdgeAddr
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *net.TCPAddr
|
||||
want *EdgeAddr
|
||||
}{
|
||||
{
|
||||
name: "happy test with excluding set",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: Unused(),
|
||||
&addr1: Unused(),
|
||||
&addr2: InUse(2),
|
||||
@@ -156,7 +153,7 @@ func TestRegion_GetUnusedIP(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "happy test with no excluding",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: InUse(0),
|
||||
&addr1: Unused(),
|
||||
&addr2: InUse(2),
|
||||
@@ -166,7 +163,7 @@ func TestRegion_GetUnusedIP(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "sad test with no excluding",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: InUse(0),
|
||||
&addr1: InUse(1),
|
||||
&addr2: InUse(2),
|
||||
@@ -176,7 +173,7 @@ func TestRegion_GetUnusedIP(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "sad test with excluding",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: Unused(),
|
||||
&addr1: InUse(1),
|
||||
&addr2: InUse(2),
|
||||
@@ -199,10 +196,10 @@ func TestRegion_GetUnusedIP(t *testing.T) {
|
||||
|
||||
func TestRegion_GiveBack(t *testing.T) {
|
||||
type fields struct {
|
||||
connFor map[*net.TCPAddr]UsedBy
|
||||
connFor map[*EdgeAddr]UsedBy
|
||||
}
|
||||
type args struct {
|
||||
addr *net.TCPAddr
|
||||
addr *EdgeAddr
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -213,7 +210,7 @@ func TestRegion_GiveBack(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "sad test with excluding",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr1: InUse(1),
|
||||
}},
|
||||
args: args{addr: &addr1},
|
||||
@@ -222,7 +219,7 @@ func TestRegion_GiveBack(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "sad test with excluding",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr1: InUse(1),
|
||||
}},
|
||||
args: args{addr: &addr2},
|
||||
@@ -247,7 +244,7 @@ func TestRegion_GiveBack(t *testing.T) {
|
||||
|
||||
func TestRegion_GetAnyAddress(t *testing.T) {
|
||||
type fields struct {
|
||||
connFor map[*net.TCPAddr]UsedBy
|
||||
connFor map[*EdgeAddr]UsedBy
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -256,19 +253,19 @@ func TestRegion_GetAnyAddress(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "Sad test -- GetAnyAddress should only fail if the region is empty",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{}},
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{}},
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "Happy test (all addresses unused)",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: Unused(),
|
||||
}},
|
||||
wantNil: false,
|
||||
},
|
||||
{
|
||||
name: "Happy test (GetAnyAddress can still return addresses used by proxy conns)",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: InUse(2),
|
||||
}},
|
||||
wantNil: false,
|
||||
|
||||
@@ -2,7 +2,6 @@ package allregions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
@@ -19,17 +18,17 @@ type Regions struct {
|
||||
// ------------------------------------
|
||||
|
||||
// ResolveEdge resolves the Cloudflare edge, returning all regions discovered.
|
||||
func ResolveEdge(log *zerolog.Logger) (*Regions, error) {
|
||||
addrLists, err := edgeDiscovery(log)
|
||||
func ResolveEdge(log *zerolog.Logger, region string) (*Regions, error) {
|
||||
edgeAddrs, err := edgeDiscovery(log, getRegionalServiceName(region))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(addrLists) < 2 {
|
||||
return nil, fmt.Errorf("expected at least 2 Cloudflare Regions regions, but SRV only returned %v", len(addrLists))
|
||||
if len(edgeAddrs) < 2 {
|
||||
return nil, fmt.Errorf("expected at least 2 Cloudflare Regions regions, but SRV only returned %v", len(edgeAddrs))
|
||||
}
|
||||
return &Regions{
|
||||
region1: NewRegion(addrLists[0]),
|
||||
region2: NewRegion(addrLists[1]),
|
||||
region1: NewRegion(edgeAddrs[0]),
|
||||
region2: NewRegion(edgeAddrs[1]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -45,9 +44,9 @@ func StaticEdge(hostnames []string, log *zerolog.Logger) (*Regions, error) {
|
||||
|
||||
// NewNoResolve doesn't resolve the edge. Instead it just uses the given addresses.
|
||||
// You probably only need this for testing.
|
||||
func NewNoResolve(addrs []*net.TCPAddr) *Regions {
|
||||
region1 := make([]*net.TCPAddr, 0)
|
||||
region2 := make([]*net.TCPAddr, 0)
|
||||
func NewNoResolve(addrs []*EdgeAddr) *Regions {
|
||||
region1 := make([]*EdgeAddr, 0)
|
||||
region2 := make([]*EdgeAddr, 0)
|
||||
for i, v := range addrs {
|
||||
if i%2 == 0 {
|
||||
region1 = append(region1, v)
|
||||
@@ -67,7 +66,7 @@ func NewNoResolve(addrs []*net.TCPAddr) *Regions {
|
||||
// ------------------------------------
|
||||
|
||||
// GetAnyAddress returns an arbitrary address from the larger region.
|
||||
func (rs *Regions) GetAnyAddress() *net.TCPAddr {
|
||||
func (rs *Regions) GetAnyAddress() *EdgeAddr {
|
||||
if addr := rs.region1.GetAnyAddress(); addr != nil {
|
||||
return addr
|
||||
}
|
||||
@@ -76,7 +75,7 @@ func (rs *Regions) GetAnyAddress() *net.TCPAddr {
|
||||
|
||||
// AddrUsedBy finds the address used by the given connection.
|
||||
// Returns nil if the connection isn't using an address.
|
||||
func (rs *Regions) AddrUsedBy(connID int) *net.TCPAddr {
|
||||
func (rs *Regions) AddrUsedBy(connID int) *EdgeAddr {
|
||||
if addr := rs.region1.AddrUsedBy(connID); addr != nil {
|
||||
return addr
|
||||
}
|
||||
@@ -85,7 +84,7 @@ func (rs *Regions) AddrUsedBy(connID int) *net.TCPAddr {
|
||||
|
||||
// GetUnusedAddr gets an unused addr from the edge, excluding the given addr. Prefer to use addresses
|
||||
// evenly across both regions.
|
||||
func (rs *Regions) GetUnusedAddr(excluding *net.TCPAddr, connID int) *net.TCPAddr {
|
||||
func (rs *Regions) GetUnusedAddr(excluding *EdgeAddr, connID int) *EdgeAddr {
|
||||
if rs.region1.AvailableAddrs() > rs.region2.AvailableAddrs() {
|
||||
return getAddrs(excluding, connID, &rs.region1, &rs.region2)
|
||||
}
|
||||
@@ -95,7 +94,7 @@ func (rs *Regions) GetUnusedAddr(excluding *net.TCPAddr, connID int) *net.TCPAdd
|
||||
|
||||
// getAddrs tries to grab address form `first` region, then `second` region
|
||||
// this is an unrolled loop over 2 element array
|
||||
func getAddrs(excluding *net.TCPAddr, connID int, first *Region, second *Region) *net.TCPAddr {
|
||||
func getAddrs(excluding *EdgeAddr, connID int, first *Region, second *Region) *EdgeAddr {
|
||||
addr := first.GetUnusedIP(excluding)
|
||||
if addr != nil {
|
||||
first.Use(addr, connID)
|
||||
@@ -117,9 +116,18 @@ func (rs *Regions) AvailableAddrs() int {
|
||||
|
||||
// GiveBack the address so that other connections can use it.
|
||||
// Returns true if the address is in this edge.
|
||||
func (rs *Regions) GiveBack(addr *net.TCPAddr) bool {
|
||||
func (rs *Regions) GiveBack(addr *EdgeAddr) bool {
|
||||
if found := rs.region1.GiveBack(addr); found {
|
||||
return found
|
||||
}
|
||||
return rs.region2.GiveBack(addr)
|
||||
}
|
||||
|
||||
// Return regionalized service name if `region` isn't empty, otherwise return the global service name for origintunneld
|
||||
func getRegionalServiceName(region string) string {
|
||||
if region != "" {
|
||||
return region + "-" + srvService // Example: `us-origintunneld`
|
||||
}
|
||||
|
||||
return srvService // Global service is just `origintunneld`
|
||||
}
|
||||
|
||||
@@ -8,31 +8,59 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
addr0 = net.TCPAddr{
|
||||
IP: net.ParseIP("123.4.5.0"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
addr0 = EdgeAddr{
|
||||
TCP: &net.TCPAddr{
|
||||
IP: net.ParseIP("123.4.5.0"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
UDP: &net.UDPAddr{
|
||||
IP: net.ParseIP("123.4.5.0"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
}
|
||||
addr1 = net.TCPAddr{
|
||||
IP: net.ParseIP("123.4.5.1"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
addr1 = EdgeAddr{
|
||||
TCP: &net.TCPAddr{
|
||||
IP: net.ParseIP("123.4.5.1"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
UDP: &net.UDPAddr{
|
||||
IP: net.ParseIP("123.4.5.1"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
}
|
||||
addr2 = net.TCPAddr{
|
||||
IP: net.ParseIP("123.4.5.2"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
addr2 = EdgeAddr{
|
||||
TCP: &net.TCPAddr{
|
||||
IP: net.ParseIP("123.4.5.2"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
UDP: &net.UDPAddr{
|
||||
IP: net.ParseIP("123.4.5.2"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
}
|
||||
addr3 = net.TCPAddr{
|
||||
IP: net.ParseIP("123.4.5.3"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
addr3 = EdgeAddr{
|
||||
TCP: &net.TCPAddr{
|
||||
IP: net.ParseIP("123.4.5.3"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
UDP: &net.UDPAddr{
|
||||
IP: net.ParseIP("123.4.5.3"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func makeRegions() Regions {
|
||||
r1 := NewRegion([]*net.TCPAddr{&addr0, &addr1})
|
||||
r2 := NewRegion([]*net.TCPAddr{&addr2, &addr3})
|
||||
r1 := NewRegion([]*EdgeAddr{&addr0, &addr1})
|
||||
r2 := NewRegion([]*EdgeAddr{&addr2, &addr3})
|
||||
return Regions{region1: r1, region2: r2}
|
||||
}
|
||||
|
||||
@@ -105,7 +133,7 @@ func TestRegions_GetUnusedAddr_Excluding_Region2(t *testing.T) {
|
||||
|
||||
func TestNewNoResolveBalancesRegions(t *testing.T) {
|
||||
type args struct {
|
||||
addrs []*net.TCPAddr
|
||||
addrs []*EdgeAddr
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -113,11 +141,11 @@ func TestNewNoResolveBalancesRegions(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "one address",
|
||||
args: args{addrs: []*net.TCPAddr{&addr0}},
|
||||
args: args{addrs: []*EdgeAddr{&addr0}},
|
||||
},
|
||||
{
|
||||
name: "two addresses",
|
||||
args: args{addrs: []*net.TCPAddr{&addr0, &addr1}},
|
||||
args: args{addrs: []*EdgeAddr{&addr0, &addr1}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -128,6 +156,18 @@ func TestNewNoResolveBalancesRegions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRegionalServiceName(t *testing.T) {
|
||||
// Empty region should just go to origintunneld
|
||||
globalServiceName := getRegionalServiceName("")
|
||||
assert.Equal(t, srvService, globalServiceName)
|
||||
|
||||
// Non-empty region should go to the regional origintunneld variant
|
||||
for _, region := range []string{"us", "pt", "am"} {
|
||||
regionalServiceName := getRegionalServiceName(region)
|
||||
assert.Equal(t, region+"-"+srvService, regionalServiceName)
|
||||
}
|
||||
}
|
||||
|
||||
func RegionsIsBalanced(t *testing.T, rs *Regions) {
|
||||
delta := rs.region1.AvailableAddrs() - rs.region2.AvailableAddrs()
|
||||
assert.True(t, abs(delta) <= 1)
|
||||
|
||||
@@ -2,15 +2,14 @@ package edgediscovery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
)
|
||||
|
||||
const (
|
||||
LogFieldAddress = "address"
|
||||
LogFieldConnIndex = "connIndex"
|
||||
)
|
||||
|
||||
@@ -29,8 +28,8 @@ type Edge struct {
|
||||
|
||||
// ResolveEdge runs the initial discovery of the Cloudflare edge, finding Addrs that can be allocated
|
||||
// to connections.
|
||||
func ResolveEdge(log *zerolog.Logger) (*Edge, error) {
|
||||
regions, err := allregions.ResolveEdge(log)
|
||||
func ResolveEdge(log *zerolog.Logger, region string) (*Edge, error) {
|
||||
regions, err := allregions.ResolveEdge(log, region)
|
||||
if err != nil {
|
||||
return new(Edge), err
|
||||
}
|
||||
@@ -53,7 +52,7 @@ func StaticEdge(log *zerolog.Logger, hostnames []string) (*Edge, error) {
|
||||
}
|
||||
|
||||
// MockEdge creates a Cloudflare Edge from arbitrary TCP addresses. Used for testing.
|
||||
func MockEdge(log *zerolog.Logger, addrs []*net.TCPAddr) *Edge {
|
||||
func MockEdge(log *zerolog.Logger, addrs []*allregions.EdgeAddr) *Edge {
|
||||
regions := allregions.NewNoResolve(addrs)
|
||||
return &Edge{
|
||||
log: log,
|
||||
@@ -66,7 +65,7 @@ func MockEdge(log *zerolog.Logger, addrs []*net.TCPAddr) *Edge {
|
||||
// ------------------------------------
|
||||
|
||||
// GetAddrForRPC gives this connection an edge Addr.
|
||||
func (ed *Edge) GetAddrForRPC() (*net.TCPAddr, error) {
|
||||
func (ed *Edge) GetAddrForRPC() (*allregions.EdgeAddr, error) {
|
||||
ed.Lock()
|
||||
defer ed.Unlock()
|
||||
addr := ed.regions.GetAnyAddress()
|
||||
@@ -77,9 +76,8 @@ func (ed *Edge) GetAddrForRPC() (*net.TCPAddr, error) {
|
||||
}
|
||||
|
||||
// GetAddr gives this proxy connection an edge Addr. Prefer Addrs this connection has already used.
|
||||
func (ed *Edge) GetAddr(connIndex int) (*net.TCPAddr, error) {
|
||||
func (ed *Edge) GetAddr(connIndex int) (*allregions.EdgeAddr, error) {
|
||||
log := ed.log.With().Int(LogFieldConnIndex, connIndex).Logger()
|
||||
|
||||
ed.Lock()
|
||||
defer ed.Unlock()
|
||||
|
||||
@@ -95,12 +93,12 @@ func (ed *Edge) GetAddr(connIndex int) (*net.TCPAddr, error) {
|
||||
log.Debug().Msg("edgediscovery - GetAddr: No addresses left to give proxy connection")
|
||||
return nil, errNoAddressesLeft
|
||||
}
|
||||
log.Debug().Str(LogFieldAddress, addr.String()).Msg("edgediscovery - GetAddr: Giving connection its new address")
|
||||
log.Debug().Msg("edgediscovery - GetAddr: Giving connection its new address")
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// GetDifferentAddr gives back the proxy connection's edge Addr and uses a new one.
|
||||
func (ed *Edge) GetDifferentAddr(connIndex int) (*net.TCPAddr, error) {
|
||||
func (ed *Edge) GetDifferentAddr(connIndex int) (*allregions.EdgeAddr, error) {
|
||||
log := ed.log.With().Int(LogFieldConnIndex, connIndex).Logger()
|
||||
|
||||
ed.Lock()
|
||||
@@ -116,7 +114,7 @@ func (ed *Edge) GetDifferentAddr(connIndex int) (*net.TCPAddr, error) {
|
||||
// note: if oldAddr were not nil, it will become available on the next iteration
|
||||
return nil, errNoAddressesLeft
|
||||
}
|
||||
log.Debug().Str(LogFieldAddress, addr.String()).Msg("edgediscovery - GetDifferentAddr: Giving connection its new address")
|
||||
log.Debug().Msg("edgediscovery - GetDifferentAddr: Giving connection its new address")
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
@@ -129,7 +127,7 @@ func (ed *Edge) AvailableAddrs() int {
|
||||
|
||||
// GiveBack the address so that other connections can use it.
|
||||
// Returns true if the address is in this edge.
|
||||
func (ed *Edge) GiveBack(addr *net.TCPAddr) bool {
|
||||
func (ed *Edge) GiveBack(addr *allregions.EdgeAddr) bool {
|
||||
ed.Lock()
|
||||
defer ed.Unlock()
|
||||
ed.log.Debug().Msg("edgediscovery - GiveBack: Address now unused")
|
||||
|
||||
@@ -6,35 +6,65 @@ import (
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
)
|
||||
|
||||
var (
|
||||
addr0 = net.TCPAddr{
|
||||
IP: net.ParseIP("123.0.0.0"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
addr0 = allregions.EdgeAddr{
|
||||
TCP: &net.TCPAddr{
|
||||
IP: net.ParseIP("123.0.0.0"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
UDP: &net.UDPAddr{
|
||||
IP: net.ParseIP("123.0.0.0"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
}
|
||||
addr1 = net.TCPAddr{
|
||||
IP: net.ParseIP("123.0.0.1"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
addr1 = allregions.EdgeAddr{
|
||||
TCP: &net.TCPAddr{
|
||||
IP: net.ParseIP("123.0.0.1"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
UDP: &net.UDPAddr{
|
||||
IP: net.ParseIP("123.0.0.1"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
}
|
||||
addr2 = net.TCPAddr{
|
||||
IP: net.ParseIP("123.0.0.2"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
addr2 = allregions.EdgeAddr{
|
||||
TCP: &net.TCPAddr{
|
||||
IP: net.ParseIP("123.0.0.2"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
UDP: &net.UDPAddr{
|
||||
IP: net.ParseIP("123.0.0.2"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
}
|
||||
addr3 = net.TCPAddr{
|
||||
IP: net.ParseIP("123.0.0.3"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
addr3 = allregions.EdgeAddr{
|
||||
TCP: &net.TCPAddr{
|
||||
IP: net.ParseIP("123.0.0.3"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
UDP: &net.UDPAddr{
|
||||
IP: net.ParseIP("123.0.0.3"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
}
|
||||
|
||||
log = zerolog.Nop()
|
||||
testLogger = zerolog.Nop()
|
||||
)
|
||||
|
||||
func TestGiveBack(t *testing.T) {
|
||||
edge := MockEdge(&log, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
edge := MockEdge(&testLogger, []*allregions.EdgeAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
|
||||
// Give this connection an address
|
||||
assert.Equal(t, 4, edge.AvailableAddrs())
|
||||
@@ -51,7 +81,7 @@ func TestGiveBack(t *testing.T) {
|
||||
|
||||
func TestRPCAndProxyShareSingleEdgeIP(t *testing.T) {
|
||||
// Make an edge with a single IP
|
||||
edge := MockEdge(&log, []*net.TCPAddr{&addr0})
|
||||
edge := MockEdge(&testLogger, []*allregions.EdgeAddr{&addr0})
|
||||
tunnelConnID := 0
|
||||
|
||||
// Use the IP for a tunnel
|
||||
@@ -65,7 +95,7 @@ func TestRPCAndProxyShareSingleEdgeIP(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetAddrForRPC(t *testing.T) {
|
||||
edge := MockEdge(&log, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
edge := MockEdge(&testLogger, []*allregions.EdgeAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
|
||||
// Get a connection
|
||||
assert.Equal(t, 4, edge.AvailableAddrs())
|
||||
@@ -83,7 +113,7 @@ func TestGetAddrForRPC(t *testing.T) {
|
||||
|
||||
func TestOnePerRegion(t *testing.T) {
|
||||
// Make an edge with only one address
|
||||
edge := MockEdge(&log, []*net.TCPAddr{&addr0, &addr1})
|
||||
edge := MockEdge(&testLogger, []*allregions.EdgeAddr{&addr0, &addr1})
|
||||
|
||||
// Use the only address
|
||||
const connID = 0
|
||||
@@ -105,7 +135,7 @@ func TestOnePerRegion(t *testing.T) {
|
||||
|
||||
func TestOnlyOneAddrLeft(t *testing.T) {
|
||||
// Make an edge with only one address
|
||||
edge := MockEdge(&log, []*net.TCPAddr{&addr0})
|
||||
edge := MockEdge(&testLogger, []*allregions.EdgeAddr{&addr0})
|
||||
|
||||
// Use the only address
|
||||
const connID = 0
|
||||
@@ -125,7 +155,7 @@ func TestOnlyOneAddrLeft(t *testing.T) {
|
||||
|
||||
func TestNoAddrsLeft(t *testing.T) {
|
||||
// Make an edge with no addresses
|
||||
edge := MockEdge(&log, []*net.TCPAddr{})
|
||||
edge := MockEdge(&testLogger, []*allregions.EdgeAddr{})
|
||||
|
||||
_, err := edge.GetAddr(2)
|
||||
assert.Error(t, err)
|
||||
@@ -134,7 +164,7 @@ func TestNoAddrsLeft(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetAddr(t *testing.T) {
|
||||
edge := MockEdge(&log, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
edge := MockEdge(&testLogger, []*allregions.EdgeAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
|
||||
// Give this connection an address
|
||||
const connID = 0
|
||||
@@ -149,7 +179,7 @@ func TestGetAddr(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetDifferentAddr(t *testing.T) {
|
||||
edge := MockEdge(&log, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
edge := MockEdge(&testLogger, []*allregions.EdgeAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
|
||||
// Give this connection an address
|
||||
assert.Equal(t, 4, edge.AvailableAddrs())
|
||||
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e -o pipefail
|
||||
|
||||
OUTPUT=$(for d in $(go list -mod=vendor -f '{{.Dir}}' -a ./... | fgrep -v tunnelrpc) ; do goimports -format-only -local github.com/cloudflare/cloudflared -d $d ; done)
|
||||
|
||||
if [ -n "$OUTPUT" ] ; then
|
||||
PAGER=$(which colordiff || echo cat)
|
||||
echo
|
||||
echo "Code formatting issues found, use 'goimports -format-only -local github.com/cloudflare/cloudflared' to correct them"
|
||||
echo
|
||||
echo "$OUTPUT" | $PAGER
|
||||
exit 1
|
||||
fi
|
||||
+45
-32
@@ -10,6 +10,9 @@ import shutil
|
||||
import hashlib
|
||||
import requests
|
||||
import tarfile
|
||||
from os import listdir
|
||||
from os.path import isfile, join
|
||||
import re
|
||||
|
||||
from github import Github, GithubException, UnknownObjectException
|
||||
|
||||
@@ -135,7 +138,7 @@ def parse_args():
|
||||
logging.error("Missing asset path")
|
||||
is_valid = False
|
||||
|
||||
if not args.name:
|
||||
if not args.name and not os.path.isdir(args.path):
|
||||
logging.error("Missing asset name")
|
||||
is_valid = False
|
||||
|
||||
@@ -161,6 +164,38 @@ def parse_args():
|
||||
parser.print_usage()
|
||||
exit(1)
|
||||
|
||||
def upload_asset(release, filepath, filename, release_version, kv_account_id, namespace_id, kv_api_token):
|
||||
logging.info("Uploading asset: %s", filename)
|
||||
release.upload_asset(filepath, name=filename)
|
||||
|
||||
# check and extract if the file is a tar and gzipped file (as is the case with the macos builds)
|
||||
binary_path = filepath
|
||||
if binary_path.endswith("tgz"):
|
||||
try:
|
||||
shutil.rmtree('cfd')
|
||||
except OSError:
|
||||
pass
|
||||
zipfile = tarfile.open(binary_path, "r:gz")
|
||||
zipfile.extractall('cfd') # specify which folder to extract to
|
||||
zipfile.close()
|
||||
|
||||
binary_path = os.path.join(os.getcwd(), 'cfd', 'cloudflared')
|
||||
|
||||
# send the sha256 (the checksum) to workers kv
|
||||
pkg_hash = get_sha256(binary_path)
|
||||
send_hash(pkg_hash, filename, release_version, kv_account_id, namespace_id, kv_api_token)
|
||||
|
||||
# create the artifacts directory if it doesn't exist
|
||||
artifact_path = os.path.join(os.getcwd(), 'artifacts')
|
||||
if not os.path.isdir(artifact_path):
|
||||
os.mkdir(artifact_path)
|
||||
|
||||
# copy the binary to the path
|
||||
copy_path = os.path.join(artifact_path, filename)
|
||||
try:
|
||||
shutil.copy(filepath, copy_path)
|
||||
except shutil.SameFileError:
|
||||
pass # the macOS release copy fails with being the same file (already in the artifacts directory)
|
||||
|
||||
def main():
|
||||
""" Attempts to upload Asset to Github Release. Creates Release if it doesnt exist """
|
||||
@@ -174,40 +209,18 @@ def main():
|
||||
logging.info("Skipping asset upload because of dry-run")
|
||||
return
|
||||
|
||||
release.upload_asset(args.path, name=args.name)
|
||||
|
||||
# check and extract if the file is a tar and gzipped file (as is the case with the macos builds)
|
||||
binary_path = args.path
|
||||
if binary_path.endswith("tgz"):
|
||||
try:
|
||||
shutil.rmtree('cfd')
|
||||
except OSError as e:
|
||||
pass
|
||||
zipfile = tarfile.open(binary_path, "r:gz")
|
||||
zipfile.extractall('cfd') # specify which folder to extract to
|
||||
zipfile.close()
|
||||
|
||||
binary_path = os.path.join(os.getcwd(), 'cfd', 'cloudflared')
|
||||
|
||||
# send the sha256 (the checksum) to workers kv
|
||||
pkg_hash = get_sha256(binary_path)
|
||||
send_hash(pkg_hash, args.name, args.release_version, args.kv_account_id, args.namespace_id, args.kv_api_token)
|
||||
|
||||
# create the artifacts directory if it doesn't exist
|
||||
artifact_path = os.path.join(os.getcwd(), 'artifacts')
|
||||
if not os.path.isdir(artifact_path):
|
||||
os.mkdir(artifact_path)
|
||||
|
||||
# copy the binary to the path
|
||||
copy_path = os.path.join(artifact_path, args.name)
|
||||
try:
|
||||
shutil.copy(args.path, copy_path)
|
||||
except shutil.SameFileError:
|
||||
pass # the macOS release copy fails with being the same file (already in the artifacts directory). Catching to ignore.
|
||||
if os.path.isdir(args.path):
|
||||
onlyfiles = [f for f in listdir(args.path) if isfile(join(args.path, f))]
|
||||
for filename in onlyfiles:
|
||||
binary_path = os.path.join(args.path, filename)
|
||||
upload_asset(release, binary_path, filename, args.release_version, args.kv_account_id, args.namespace_id,
|
||||
args.kv_api_token)
|
||||
else:
|
||||
upload_asset(release, args.path, args.name, args.release_version, args.kv_account_id, args.namespace_id,
|
||||
args.kv_api_token)
|
||||
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
exit(1)
|
||||
|
||||
|
||||
main()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/cloudflare/cloudflared
|
||||
|
||||
go 1.15
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect
|
||||
@@ -9,6 +9,7 @@ require (
|
||||
github.com/coredns/coredns v1.7.0
|
||||
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect
|
||||
github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9 // indirect
|
||||
github.com/facebookgo/grace v0.0.0-20180706040059-75cf19382434
|
||||
@@ -21,13 +22,13 @@ require (
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.0.4
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
|
||||
github.com/google/go-cmp v0.5.2 // indirect
|
||||
github.com/google/uuid v1.1.2
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/json-iterator/go v1.1.10
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/lucas-clemente/quic-go v0.23.0
|
||||
github.com/mattn/go-colorable v0.1.8
|
||||
github.com/miekg/dns v1.1.31
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
@@ -36,16 +37,19 @@ require (
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
||||
github.com/prometheus/client_golang v1.7.1
|
||||
github.com/prometheus/client_model v0.2.0
|
||||
github.com/prometheus/common v0.13.0 // indirect
|
||||
github.com/rivo/tview v0.0.0-20200712113419-c65badfc3d92
|
||||
github.com/rs/zerolog v1.20.0
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/stretchr/testify v1.6.0
|
||||
github.com/urfave/cli/v2 v2.2.0
|
||||
go.uber.org/automaxprocs v1.4.0
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
|
||||
golang.org/x/net v0.0.0-20200904194848-62affa334b73
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 // indirect
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d // indirect
|
||||
google.golang.org/grpc v1.32.0 // indirect
|
||||
@@ -53,9 +57,43 @@ require (
|
||||
gopkg.in/coreos/go-oidc.v2 v2.1.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
gopkg.in/square/go-jose.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.3.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
|
||||
zombiezen.com/go/capnproto2 v2.18.0+incompatible
|
||||
)
|
||||
|
||||
replace github.com/urfave/cli/v2 => github.com/ipostelnik/cli/v2 v2.3.1-0.20210223162147-c8b4f97735e4
|
||||
require (
|
||||
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/caddyserver/caddy v1.0.5 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||
github.com/cheekybits/genny v1.0.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 // indirect
|
||||
github.com/marten-seemann/qtls-go1-16 v0.1.4 // indirect
|
||||
github.com/marten-seemann/qtls-go1-17 v0.1.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.12 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.8 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/nxadm/tail v1.4.8 // indirect
|
||||
github.com/onsi/ginkgo v1.16.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/procfs v0.1.3 // indirect
|
||||
github.com/rivo/uniseg v0.1.0 // indirect
|
||||
golang.org/x/mod v0.4.2 // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
golang.org/x/tools v0.1.1 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
google.golang.org/appengine v1.6.6 // indirect
|
||||
google.golang.org/protobuf v1.26.0 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
)
|
||||
|
||||
replace github.com/urfave/cli/v2 => github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
@@ -31,7 +33,12 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA=
|
||||
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
|
||||
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
|
||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/Azure/azure-sdk-for-go v32.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/azure-sdk-for-go v40.6.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/go-autorest/autorest v0.1.0/go.mod h1:AKyIcETwSUFxIcs/Wnq/C+kwCtlEYGUVd7FPNb2slmg=
|
||||
@@ -84,6 +91,7 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190808125512-07798873deee/go.mod h1:myCDvQSzCW+wB1WAlocEru4wMGJxy+vlxHdhegi1CDQ=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
@@ -101,6 +109,8 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
||||
github.com/caddyserver/caddy v1.0.5 h1:5B1Hs0UF2x2tggr2X9jL2qOZtDXbIWQb9YLbmlxHSuM=
|
||||
github.com/caddyserver/caddy v1.0.5/go.mod h1:AnFHB+/MrgRC+mJAvuAgQ38ePzw+wKeW0wzENpdQQKY=
|
||||
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
|
||||
@@ -113,6 +123,7 @@ github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 h1:JLaf/iINcLyjw
|
||||
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
|
||||
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
@@ -133,6 +144,7 @@ github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73 h1:7CNPV0LWRCa1FNmq
|
||||
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
@@ -140,8 +152,9 @@ github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+
|
||||
github.com/coreos/license-bill-of-materials v0.0.0-20190913234955-13baff47494e/go.mod h1:4xMOusJ7xxc84WclVxKT8+lNfGYDwojOUC2OQNCwcj4=
|
||||
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpu/goacmedns v0.0.1/go.mod h1:sesf/pNnCYwUevQEQfEwY0Y3DydlQWSGZbaMElOWxok=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -184,6 +197,7 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
|
||||
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
@@ -197,6 +211,7 @@ github.com/getsentry/raven-go v0.0.0-20180517221441-ed7bcb39ff10 h1:YO10pIIBftO/
|
||||
github.com/getsentry/raven-go v0.0.0-20180517221441-ed7bcb39ff10/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
|
||||
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-acme/lego/v3 v3.1.0/go.mod h1:074uqt+JS6plx+c9Xaiz6+L+GBb+7itGtzfcDM2AhEE=
|
||||
github.com/go-acme/lego/v3 v3.2.0/go.mod h1:074uqt+JS6plx+c9Xaiz6+L+GBb+7itGtzfcDM2AhEE=
|
||||
github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
|
||||
@@ -219,6 +234,8 @@ github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dp
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/gobwas/httphead v0.0.0-20200921212729-da3d93bc3c58 h1:YyrUZvJaU8Q0QsoVo+xLFBgWDTam29PKea6GYmwvSiQ=
|
||||
github.com/gobwas/httphead v0.0.0-20200921212729-da3d93bc3c58/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
@@ -240,6 +257,7 @@ github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4er
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
@@ -247,6 +265,8 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
|
||||
@@ -261,8 +281,10 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
@@ -273,8 +295,9 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@@ -292,6 +315,8 @@ github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||
@@ -311,6 +336,7 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU=
|
||||
@@ -347,8 +373,9 @@ github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||
github.com/infobloxopen/go-trees v0.0.0-20190313150506-2af4e13f9062/go.mod h1:PcNJqIlcX/dj3DTG/+QQnRvSgTMG6CLpRMjWcv4+J6w=
|
||||
github.com/ipostelnik/cli/v2 v2.3.1-0.20210223162147-c8b4f97735e4 h1:U2OxGKZHC5puhKZnGo6wWWjuWusJCM6J/S1eF7Vt+Uk=
|
||||
github.com/ipostelnik/cli/v2 v2.3.1-0.20210223162147-c8b4f97735e4/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d h1:PRDnysJ9dF1vUMmEzBu6aHQeUluSQy4eWH3RsSSy/vI=
|
||||
github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
|
||||
@@ -378,6 +405,7 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
@@ -391,14 +419,24 @@ github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0U
|
||||
github.com/linode/linodego v0.10.0/go.mod h1:cziNP7pbvE3mXIPneHj0oRY8L1WtGEIKlZ8LANE4eXA=
|
||||
github.com/liquidweb/liquidweb-go v1.6.0/go.mod h1:UDcVnAMDkZxpw4Y7NOHkqoeiGacVLEIG/i5J9cyixzQ=
|
||||
github.com/lucas-clemente/quic-go v0.13.1/go.mod h1:Vn3/Fb0/77b02SGhQk36KzOUmXgVpFfizUfW5WMaqyU=
|
||||
github.com/lucas-clemente/quic-go v0.23.0 h1:5vFnKtZ6nHDFsc/F3uuiF4T3y/AXaQdxjUqiVw26GZE=
|
||||
github.com/lucas-clemente/quic-go v0.23.0/go.mod h1:paZuzjXCE5mj6sikVLMvqXk8lJV2AsqtJ6bDhjEfxx0=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
||||
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/marten-seemann/chacha20 v0.2.0/go.mod h1:HSdjFau7GzYRj+ahFNwsO3ouVJr1HFkWoEwNDb4TMtE=
|
||||
github.com/marten-seemann/qpack v0.1.0/go.mod h1:LFt1NU/Ptjip0C2CPkhimBz5CGE3WGDAUWqna+CNTrI=
|
||||
github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
|
||||
github.com/marten-seemann/qtls v0.4.1/go.mod h1:pxVXcHHw1pNIt8Qo0pwSYQEoZ8yYOOPXTCZLQQunvRc=
|
||||
github.com/marten-seemann/qtls-go1-15 v0.1.4/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I=
|
||||
github.com/marten-seemann/qtls-go1-16 v0.1.4 h1:xbHbOGGhrenVtII6Co8akhLEdrawwB2iHl5yhJRpnco=
|
||||
github.com/marten-seemann/qtls-go1-16 v0.1.4/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk=
|
||||
github.com/marten-seemann/qtls-go1-17 v0.1.0 h1:P9ggrs5xtwiqXv/FHNwntmuLMNq3KaSIG93AtAZ48xk=
|
||||
github.com/marten-seemann/qtls-go1-17 v0.1.0/go.mod h1:fz4HIxByo+LlWcreM4CZOYNuz3taBQ8rN2X6FqvaWo8=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
@@ -414,6 +452,7 @@ github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4f
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mholt/certmagic v0.8.3/go.mod h1:91uJzK5K8IWtYQqTi5R2tsxV1pCde+wdGfaRaOZi6aQ=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
@@ -450,11 +489,16 @@ github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi
|
||||
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nrdcg/auroradns v1.0.0/go.mod h1:6JPXKzIRzZzMqtTDgueIhTi6rFf1QvYE/HzqidhOhjw=
|
||||
github.com/nrdcg/goinwx v0.6.1/go.mod h1:XPiut7enlbEdntAqalBIqcYcTEVhpv/dKWgDCX2SwKQ=
|
||||
github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
@@ -463,9 +507,18 @@ github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
|
||||
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak=
|
||||
github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
|
||||
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
|
||||
@@ -475,6 +528,7 @@ github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+
|
||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||
github.com/openzipkin-contrib/zipkin-go-opentracing v0.3.5/go.mod h1:uVHyebswE1cCXr2A73cRM2frx5ld1RJUCJkFNZ90ZiI=
|
||||
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
@@ -499,6 +553,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
@@ -514,6 +569,7 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
|
||||
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
|
||||
@@ -522,6 +578,7 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.13.0 h1:vJlpe9wPgDRM1Z+7Wj3zUUjY1nr6/1jNKyl7llliccg=
|
||||
github.com/prometheus/common v0.13.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
@@ -542,17 +599,41 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs=
|
||||
github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo=
|
||||
github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4 h1:S9YlS71UNJIyS61OqGAmLXv3w5zclSidN+qwr80XxKs=
|
||||
github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sacloud/libsacloud v1.26.1/go.mod h1:79ZwATmHLIFZIMd7sxA3LwzVy/B77uj3LDoToVTxDoQ=
|
||||
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
||||
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
|
||||
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
|
||||
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
|
||||
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
|
||||
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
|
||||
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
|
||||
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
|
||||
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
|
||||
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
|
||||
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
|
||||
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
|
||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
@@ -562,6 +643,8 @@ github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:s
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
@@ -578,6 +661,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho=
|
||||
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/timewasted/linode v0.0.0-20160829202747-37e84520dcf7/go.mod h1:imsgLplxEC/etjIhdr3dNzV3JeT27LbVu5pYWm0JCBY=
|
||||
github.com/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ=
|
||||
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
@@ -587,6 +671,8 @@ github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
|
||||
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
|
||||
github.com/vultr/govultr v0.1.4/go.mod h1:9H008Uxr/C4vFNGLqKx232C206GL0PBHzOP0809bGNA=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
@@ -595,9 +681,12 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
|
||||
go.etcd.io/etcd v0.5.0-alpha.5.0.20200306183522-221f0cc107cb/go.mod h1:VZB9Yx4s43MHItytoe8jcvaEFEgF2QzHDZGfQ/XQjvQ=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
@@ -608,6 +697,8 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/automaxprocs v1.4.0 h1:CpDZl6aOlLhReez+8S3eEotD7Jx0Os++lemPlMULQP0=
|
||||
go.uber.org/automaxprocs v1.4.0/go.mod h1:/mTEdr7LvHhs0v7mjdxDreTz1OG5zdZGqgOnhWiR/+Q=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
@@ -616,11 +707,15 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
|
||||
golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
@@ -632,6 +727,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
|
||||
@@ -648,6 +744,7 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -666,12 +763,16 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180611182652-db08ff08e862/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -680,6 +781,7 @@ golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73r
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@@ -704,19 +806,25 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
|
||||
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -724,8 +832,10 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180622082034-63fc586f45fe/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -733,12 +843,14 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -756,6 +868,7 @@ golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -774,13 +887,19 @@ golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -788,8 +907,9 @@ golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fq
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -799,6 +919,7 @@ golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGm
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@@ -846,11 +967,17 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.1 h1:wGiQel/hW0NnEkJUk8lbzkX2gFJU6PFxf1v5OlCfuOs=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
@@ -871,6 +998,7 @@ google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSr
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
@@ -879,6 +1007,9 @@ google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuh
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
@@ -910,6 +1041,8 @@ google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6D
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d h1:92D1fum1bJLKSdr11OJ+54YeCMCGYIygTA7R/YZxH5M=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
@@ -939,8 +1072,10 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/DataDog/dd-trace-go.v1 v1.24.1/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzwVCaGWylTe3tg=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -967,6 +1102,7 @@ gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/square/go-jose.v2 v2.4.0 h1:0kXPskUMGAXXWJlP05ktEMOV0vmzFQUWw6d+aZJQU8A=
|
||||
gopkg.in/square/go-jose.v2 v2.4.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
@@ -977,11 +1113,13 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
@@ -1007,5 +1145,7 @@ sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnM
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
|
||||
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
|
||||
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
||||
zombiezen.com/go/capnproto2 v2.18.0+incompatible h1:mwfXZniffG5mXokQGHUJWGnqIBggoPfT/CEwon9Yess=
|
||||
zombiezen.com/go/capnproto2 v2.18.0+incompatible/go.mod h1:XO5Pr2SbXgqZwn0m0Ru54QBqpOf4K5AYBO+8LAOBQEQ=
|
||||
|
||||
-234
@@ -1,234 +0,0 @@
|
||||
package h2mux
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Header struct {
|
||||
Name, Value string
|
||||
}
|
||||
|
||||
var headerEncoding = base64.RawStdEncoding
|
||||
|
||||
const (
|
||||
RequestUserHeadersField = "cf-cloudflared-request-headers"
|
||||
ResponseUserHeadersField = "cf-cloudflared-response-headers"
|
||||
|
||||
CFAccessTokenHeader = "cf-access-token"
|
||||
CFJumpDestinationHeader = "CF-Access-Jump-Destination"
|
||||
CFAccessClientIDHeader = "CF-Access-Client-Id"
|
||||
CFAccessClientSecretHeader = "CF-Access-Client-Secret"
|
||||
)
|
||||
|
||||
// H2RequestHeadersToH1Request converts the HTTP/2 headers coming from origintunneld
|
||||
// to an HTTP/1 Request object destined for the local origin web service.
|
||||
// This operation includes conversion of the pseudo-headers into their closest
|
||||
// HTTP/1 equivalents. See https://tools.ietf.org/html/rfc7540#section-8.1.2.3
|
||||
func H2RequestHeadersToH1Request(h2 []Header, h1 *http.Request) error {
|
||||
for _, header := range h2 {
|
||||
name := strings.ToLower(header.Name)
|
||||
if !IsControlHeader(name) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch name {
|
||||
case ":method":
|
||||
h1.Method = header.Value
|
||||
case ":scheme":
|
||||
// noop - use the preexisting scheme from h1.URL
|
||||
case ":authority":
|
||||
// Otherwise the host header will be based on the origin URL
|
||||
h1.Host = header.Value
|
||||
case ":path":
|
||||
// We don't want to be an "opinionated" proxy, so ideally we would use :path as-is.
|
||||
// However, this HTTP/1 Request object belongs to the Go standard library,
|
||||
// whose URL package makes some opinionated decisions about the encoding of
|
||||
// URL characters: see the docs of https://godoc.org/net/url#URL,
|
||||
// in particular the EscapedPath method https://godoc.org/net/url#URL.EscapedPath,
|
||||
// which is always used when computing url.URL.String(), whether we'd like it or not.
|
||||
//
|
||||
// Well, not *always*. We could circumvent this by using url.URL.Opaque. But
|
||||
// that would present unusual difficulties when using an HTTP proxy: url.URL.Opaque
|
||||
// is treated differently when HTTP_PROXY is set!
|
||||
// See https://github.com/golang/go/issues/5684#issuecomment-66080888
|
||||
//
|
||||
// This means we are subject to the behavior of net/url's function `shouldEscape`
|
||||
// (as invoked with mode=encodePath): https://github.com/golang/go/blob/go1.12.7/src/net/url/url.go#L101
|
||||
|
||||
if header.Value == "*" {
|
||||
h1.URL.Path = "*"
|
||||
continue
|
||||
}
|
||||
// Due to the behavior of validation.ValidateUrl, h1.URL may
|
||||
// already have a partial value, with or without a trailing slash.
|
||||
base := h1.URL.String()
|
||||
base = strings.TrimRight(base, "/")
|
||||
// But we know :path begins with '/', because we handled '*' above - see RFC7540
|
||||
requestURL, err := url.Parse(base + header.Value)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("invalid path '%v'", header.Value))
|
||||
}
|
||||
h1.URL = requestURL
|
||||
case "content-length":
|
||||
contentLength, err := strconv.ParseInt(header.Value, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unparseable content length")
|
||||
}
|
||||
h1.ContentLength = contentLength
|
||||
case RequestUserHeadersField:
|
||||
// Do not forward the serialized headers to the origin -- deserialize them, and ditch the serialized version
|
||||
// Find and parse user headers serialized into a single one
|
||||
userHeaders, err := ParseUserHeaders(RequestUserHeadersField, h2)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to parse user headers")
|
||||
}
|
||||
for _, userHeader := range userHeaders {
|
||||
h1.Header.Add(http.CanonicalHeaderKey(userHeader.Name), userHeader.Value)
|
||||
}
|
||||
default:
|
||||
// All other control headers shall just be proxied transparently
|
||||
h1.Header.Add(http.CanonicalHeaderKey(header.Name), header.Value)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseUserHeaders(headerNameToParseFrom string, headers []Header) ([]Header, error) {
|
||||
for _, header := range headers {
|
||||
if header.Name == headerNameToParseFrom {
|
||||
return DeserializeHeaders(header.Value)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%v header not found", RequestUserHeadersField)
|
||||
}
|
||||
|
||||
func IsControlHeader(headerName string) bool {
|
||||
return headerName == "content-length" ||
|
||||
headerName == "connection" || headerName == "upgrade" || // Websocket headers
|
||||
strings.HasPrefix(headerName, ":") ||
|
||||
strings.HasPrefix(headerName, "cf-")
|
||||
}
|
||||
|
||||
// isWebsocketClientHeader returns true if the header name is required by the client to upgrade properly
|
||||
func IsWebsocketClientHeader(headerName string) bool {
|
||||
return headerName == "sec-websocket-accept" ||
|
||||
headerName == "connection" ||
|
||||
headerName == "upgrade"
|
||||
}
|
||||
|
||||
func H1ResponseToH2ResponseHeaders(status int, h1 http.Header) (h2 []Header) {
|
||||
h2 = []Header{
|
||||
{Name: ":status", Value: strconv.Itoa(status)},
|
||||
}
|
||||
userHeaders := make(http.Header, len(h1))
|
||||
for header, values := range h1 {
|
||||
h2name := strings.ToLower(header)
|
||||
if h2name == "content-length" {
|
||||
// This header has meaning in HTTP/2 and will be used by the edge,
|
||||
// so it should be sent as an HTTP/2 response header.
|
||||
|
||||
// Since these are http2 headers, they're required to be lowercase
|
||||
h2 = append(h2, Header{Name: "content-length", Value: values[0]})
|
||||
} else if !IsControlHeader(h2name) || IsWebsocketClientHeader(h2name) {
|
||||
// User headers, on the other hand, must all be serialized so that
|
||||
// HTTP/2 header validation won't be applied to HTTP/1 header values
|
||||
userHeaders[header] = values
|
||||
}
|
||||
}
|
||||
|
||||
// Perform user header serialization and set them in the single header
|
||||
h2 = append(h2, Header{ResponseUserHeadersField, SerializeHeaders(userHeaders)})
|
||||
return h2
|
||||
}
|
||||
|
||||
// Serialize HTTP1.x headers by base64-encoding each header name and value,
|
||||
// and then joining them in the format of [key:value;]
|
||||
func SerializeHeaders(h1Headers http.Header) string {
|
||||
// compute size of the fully serialized value and largest temp buffer we will need
|
||||
serializedLen := 0
|
||||
maxTempLen := 0
|
||||
for headerName, headerValues := range h1Headers {
|
||||
for _, headerValue := range headerValues {
|
||||
nameLen := headerEncoding.EncodedLen(len(headerName))
|
||||
valueLen := headerEncoding.EncodedLen(len(headerValue))
|
||||
const delims = 2
|
||||
serializedLen += delims + nameLen + valueLen
|
||||
if nameLen > maxTempLen {
|
||||
maxTempLen = nameLen
|
||||
}
|
||||
if valueLen > maxTempLen {
|
||||
maxTempLen = valueLen
|
||||
}
|
||||
}
|
||||
}
|
||||
var buf strings.Builder
|
||||
buf.Grow(serializedLen)
|
||||
|
||||
temp := make([]byte, maxTempLen)
|
||||
writeB64 := func(s string) {
|
||||
n := headerEncoding.EncodedLen(len(s))
|
||||
if n > len(temp) {
|
||||
temp = make([]byte, n)
|
||||
}
|
||||
headerEncoding.Encode(temp[:n], []byte(s))
|
||||
buf.Write(temp[:n])
|
||||
}
|
||||
|
||||
for headerName, headerValues := range h1Headers {
|
||||
for _, headerValue := range headerValues {
|
||||
if buf.Len() > 0 {
|
||||
buf.WriteByte(';')
|
||||
}
|
||||
writeB64(headerName)
|
||||
buf.WriteByte(':')
|
||||
writeB64(headerValue)
|
||||
}
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Deserialize headers serialized by `SerializeHeader`
|
||||
func DeserializeHeaders(serializedHeaders string) ([]Header, error) {
|
||||
const unableToDeserializeErr = "Unable to deserialize headers"
|
||||
|
||||
var deserialized []Header
|
||||
for _, serializedPair := range strings.Split(serializedHeaders, ";") {
|
||||
if len(serializedPair) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
serializedHeaderParts := strings.Split(serializedPair, ":")
|
||||
if len(serializedHeaderParts) != 2 {
|
||||
return nil, errors.New(unableToDeserializeErr)
|
||||
}
|
||||
|
||||
serializedName := serializedHeaderParts[0]
|
||||
serializedValue := serializedHeaderParts[1]
|
||||
deserializedName := make([]byte, headerEncoding.DecodedLen(len(serializedName)))
|
||||
deserializedValue := make([]byte, headerEncoding.DecodedLen(len(serializedValue)))
|
||||
|
||||
if _, err := headerEncoding.Decode(deserializedName, []byte(serializedName)); err != nil {
|
||||
return nil, errors.Wrap(err, unableToDeserializeErr)
|
||||
}
|
||||
if _, err := headerEncoding.Decode(deserializedValue, []byte(serializedValue)); err != nil {
|
||||
return nil, errors.Wrap(err, unableToDeserializeErr)
|
||||
}
|
||||
|
||||
deserialized = append(deserialized, Header{
|
||||
Name: string(deserializedName),
|
||||
Value: string(deserializedValue),
|
||||
})
|
||||
}
|
||||
|
||||
return deserialized, nil
|
||||
}
|
||||
@@ -23,6 +23,10 @@ type MuxedStreamDataSignaller interface {
|
||||
Signal(ID uint32)
|
||||
}
|
||||
|
||||
type Header struct {
|
||||
Name, Value string
|
||||
}
|
||||
|
||||
// MuxedStream is logically an HTTP/2 stream, with an additional buffer for outgoing data.
|
||||
type MuxedStream struct {
|
||||
streamID uint32
|
||||
@@ -74,8 +78,6 @@ type MuxedStream struct {
|
||||
sentEOF bool
|
||||
// true if the peer sent us an EOF
|
||||
receivedEOF bool
|
||||
// If valid, tunnelHostname is used to identify which origin service is the intended recipient of the request
|
||||
tunnelHostname TunnelHostname
|
||||
// Compression-related fields
|
||||
receivedUseDict bool
|
||||
method string
|
||||
@@ -252,10 +254,6 @@ func (s *MuxedStream) IsRPCStream() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *MuxedStream) TunnelHostname() TunnelHostname {
|
||||
return s.tunnelHostname
|
||||
}
|
||||
|
||||
// Block until a value is sent on writeBufferHasSpace.
|
||||
// Must be called while holding writeLock
|
||||
func (s *MuxedStream) awaitWriteBufferHasSpace() {
|
||||
|
||||
+3
-8
@@ -12,10 +12,6 @@ import (
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
const (
|
||||
CloudflaredProxyTunnelHostnameHeader = "cf-cloudflared-proxy-tunnel-hostname"
|
||||
)
|
||||
|
||||
type MuxReader struct {
|
||||
// f is used to read HTTP2 frames.
|
||||
f *http2.Framer
|
||||
@@ -73,12 +69,13 @@ func (r *MuxReader) run(log *zerolog.Logger) error {
|
||||
|
||||
// routine to periodically update bytesRead
|
||||
go func() {
|
||||
tickC := time.Tick(updateFreq)
|
||||
ticker := time.NewTicker(updateFreq)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-r.abortChan:
|
||||
return
|
||||
case <-tickC:
|
||||
case <-ticker.C:
|
||||
r.metricsUpdater.updateInBoundBytes(r.bytesRead.Count())
|
||||
}
|
||||
}
|
||||
@@ -252,8 +249,6 @@ func (r *MuxReader) receiveHeaderData(frame *http2.MetaHeadersFrame) error {
|
||||
if r.dictionaries.write != nil {
|
||||
continue
|
||||
}
|
||||
case CloudflaredProxyTunnelHostnameHeader:
|
||||
stream.tunnelHostname = TunnelHostname(header.Value)
|
||||
}
|
||||
headers = append(headers, Header{Name: header.Name, Value: header.Value})
|
||||
}
|
||||
|
||||
@@ -21,10 +21,6 @@ var (
|
||||
Name: ":path",
|
||||
Value: "/api/tunnels",
|
||||
}
|
||||
tunnelHostnameHeader = Header{
|
||||
Name: CloudflaredProxyTunnelHostnameHeader,
|
||||
Value: "tunnel.example.com",
|
||||
}
|
||||
respStatusHeader = Header{
|
||||
Name: ":status",
|
||||
Value: "200",
|
||||
@@ -42,15 +38,6 @@ func (mosh *mockOriginStreamHandler) ServeStream(stream *MuxedStream) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getCloudflaredProxyTunnelHostnameHeader(stream *MuxedStream) string {
|
||||
for _, header := range stream.Headers {
|
||||
if header.Name == CloudflaredProxyTunnelHostnameHeader {
|
||||
return header.Value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func assertOpenStreamSucceed(t *testing.T, stream *MuxedStream, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, stream.Headers, 1)
|
||||
@@ -72,13 +59,11 @@ func TestMissingHeaders(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
// Request doesn't contain CloudflaredProxyTunnelHostnameHeader
|
||||
stream, err := muxPair.EdgeMux.OpenStream(ctx, reqHeaders, nil)
|
||||
assertOpenStreamSucceed(t, stream, err)
|
||||
|
||||
assert.Empty(t, originHandler.stream.method)
|
||||
assert.Empty(t, originHandler.stream.path)
|
||||
assert.False(t, originHandler.stream.TunnelHostname().IsSet())
|
||||
}
|
||||
|
||||
func TestReceiveHeaderData(t *testing.T) {
|
||||
@@ -90,18 +75,14 @@ func TestReceiveHeaderData(t *testing.T) {
|
||||
methodHeader,
|
||||
schemeHeader,
|
||||
pathHeader,
|
||||
tunnelHostnameHeader,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
reqHeaders = append(reqHeaders, tunnelHostnameHeader)
|
||||
stream, err := muxPair.EdgeMux.OpenStream(ctx, reqHeaders, nil)
|
||||
assertOpenStreamSucceed(t, stream, err)
|
||||
|
||||
assert.Equal(t, methodHeader.Value, originHandler.stream.method)
|
||||
assert.Equal(t, pathHeader.Value, originHandler.stream.path)
|
||||
assert.True(t, originHandler.stream.TunnelHostname().IsSet())
|
||||
assert.Equal(t, tunnelHostnameHeader.Value, originHandler.stream.TunnelHostname().String())
|
||||
}
|
||||
|
||||
+5
-3
@@ -3,10 +3,11 @@ package h2mux
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"github.com/rs/zerolog"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/hpack"
|
||||
)
|
||||
@@ -77,12 +78,13 @@ func (w *MuxWriter) run(log *zerolog.Logger) error {
|
||||
|
||||
// routine to periodically communicate bytesWrote
|
||||
go func() {
|
||||
tickC := time.Tick(updateFreq)
|
||||
ticker := time.NewTicker(updateFreq)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-w.abortChan:
|
||||
return
|
||||
case <-tickC:
|
||||
case <-ticker.C:
|
||||
w.metricsUpdater.updateOutBoundBytes(w.bytesWrote.Count())
|
||||
}
|
||||
}
|
||||
|
||||
+10
-5
@@ -9,12 +9,12 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/ipaccess"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/ipaccess"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -103,7 +103,7 @@ func NewWarpRoutingService() *WarpRoutingService {
|
||||
}
|
||||
|
||||
// Get a single origin service from the CLI/config.
|
||||
func parseSingleOriginService(c *cli.Context, allowURLFromArgs bool) (originService, error) {
|
||||
func parseSingleOriginService(c *cli.Context, allowURLFromArgs bool) (OriginService, error) {
|
||||
if c.IsSet("hello-world") {
|
||||
return new(helloWorld), nil
|
||||
}
|
||||
@@ -138,6 +138,11 @@ func (ing Ingress) IsEmpty() bool {
|
||||
return len(ing.Rules) == 0
|
||||
}
|
||||
|
||||
// IsSingleRule checks if the user only specified a single ingress rule.
|
||||
func (ing Ingress) IsSingleRule() bool {
|
||||
return len(ing.Rules) == 1
|
||||
}
|
||||
|
||||
// StartOrigins will start any origin services managed by cloudflared, e.g. proxy servers or Hello World.
|
||||
func (ing Ingress) StartOrigins(
|
||||
wg *sync.WaitGroup,
|
||||
@@ -162,7 +167,7 @@ func validate(ingress []config.UnvalidatedIngressRule, defaults OriginRequestCon
|
||||
rules := make([]Rule, len(ingress))
|
||||
for i, r := range ingress {
|
||||
cfg := setConfig(defaults, r.OriginRequest)
|
||||
var service originService
|
||||
var service OriginService
|
||||
|
||||
if prefix := "unix:"; strings.HasPrefix(r.Service, prefix) {
|
||||
// No validation necessary for unix socket filepath services
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gopkg.in/yaml.v2"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/ipaccess"
|
||||
@@ -402,7 +402,7 @@ func TestSingleOriginSetsConfig(t *testing.T) {
|
||||
flagSet.Bool("hello-world", true, "")
|
||||
flagSet.Duration(ProxyConnectTimeoutFlag, time.Second, "")
|
||||
flagSet.Duration(ProxyTLSTimeoutFlag, time.Second, "")
|
||||
flagSet.Duration(ProxyTCPKeepAlive, time.Second, "")
|
||||
flagSet.Duration(ProxyTCPKeepAliveFlag, time.Second, "")
|
||||
flagSet.Bool(ProxyNoHappyEyeballsFlag, true, "")
|
||||
flagSet.Int(ProxyKeepAliveConnectionsFlag, 10, "")
|
||||
flagSet.Duration(ProxyKeepAliveTimeoutFlag, time.Second, "")
|
||||
@@ -423,7 +423,7 @@ func TestSingleOriginSetsConfig(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
err = cliCtx.Set(ProxyTLSTimeoutFlag, "1s")
|
||||
require.NoError(t, err)
|
||||
err = cliCtx.Set(ProxyTCPKeepAlive, "1s")
|
||||
err = cliCtx.Set(ProxyTCPKeepAliveFlag, "1s")
|
||||
require.NoError(t, err)
|
||||
err = cliCtx.Set(ProxyNoHappyEyeballsFlag, "true")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -2,16 +2,14 @@ package ingress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/ipaccess"
|
||||
"github.com/cloudflare/cloudflared/socks"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
gws "github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// OriginConnection is a way to stream to a service running on the user's origin.
|
||||
@@ -24,34 +22,10 @@ type OriginConnection interface {
|
||||
|
||||
type streamHandlerFunc func(originConn io.ReadWriter, remoteConn net.Conn, log *zerolog.Logger)
|
||||
|
||||
// Stream copies copy data to & from provided io.ReadWriters.
|
||||
func Stream(conn, backendConn io.ReadWriter, log *zerolog.Logger) {
|
||||
proxyDone := make(chan struct{}, 2)
|
||||
|
||||
go func() {
|
||||
_, err := io.Copy(conn, backendConn)
|
||||
if err != nil {
|
||||
log.Debug().Msgf("conn to backendConn copy: %v", err)
|
||||
}
|
||||
proxyDone <- struct{}{}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
_, err := io.Copy(backendConn, conn)
|
||||
if err != nil {
|
||||
log.Debug().Msgf("backendConn to conn copy: %v", err)
|
||||
}
|
||||
proxyDone <- struct{}{}
|
||||
}()
|
||||
|
||||
// If one side is done, we are done.
|
||||
<-proxyDone
|
||||
}
|
||||
|
||||
// DefaultStreamHandler is an implementation of streamHandlerFunc that
|
||||
// performs a two way io.Copy between originConn and remoteConn.
|
||||
func DefaultStreamHandler(originConn io.ReadWriter, remoteConn net.Conn, log *zerolog.Logger) {
|
||||
Stream(originConn, remoteConn, log)
|
||||
websocket.Stream(originConn, remoteConn, log)
|
||||
}
|
||||
|
||||
// tcpConnection is an OriginConnection that directly streams to raw TCP.
|
||||
@@ -60,7 +34,7 @@ type tcpConnection struct {
|
||||
}
|
||||
|
||||
func (tc *tcpConnection) Stream(ctx context.Context, tunnelConn io.ReadWriter, log *zerolog.Logger) {
|
||||
Stream(tunnelConn, tc.conn, log)
|
||||
websocket.Stream(tunnelConn, tc.conn, log)
|
||||
}
|
||||
|
||||
func (tc *tcpConnection) Close() {
|
||||
@@ -74,42 +48,18 @@ type tcpOverWSConnection struct {
|
||||
}
|
||||
|
||||
func (wc *tcpOverWSConnection) Stream(ctx context.Context, tunnelConn io.ReadWriter, log *zerolog.Logger) {
|
||||
wc.streamHandler(websocket.NewConn(ctx, tunnelConn, log), wc.conn, log)
|
||||
wsCtx, cancel := context.WithCancel(ctx)
|
||||
wsConn := websocket.NewConn(wsCtx, tunnelConn, log)
|
||||
wc.streamHandler(wsConn, wc.conn, log)
|
||||
cancel()
|
||||
// Makes sure wsConn stops sending ping before terminating the stream
|
||||
wsConn.WaitForShutdown()
|
||||
}
|
||||
|
||||
func (wc *tcpOverWSConnection) Close() {
|
||||
wc.conn.Close()
|
||||
}
|
||||
|
||||
// wsConnection is an OriginConnection that streams WS between eyeball and origin.
|
||||
type wsConnection struct {
|
||||
wsConn *gws.Conn
|
||||
resp *http.Response
|
||||
}
|
||||
|
||||
func (wsc *wsConnection) Stream(ctx context.Context, tunnelConn io.ReadWriter, log *zerolog.Logger) {
|
||||
Stream(tunnelConn, wsc.wsConn.UnderlyingConn(), log)
|
||||
}
|
||||
|
||||
func (wsc *wsConnection) Close() {
|
||||
wsc.resp.Body.Close()
|
||||
wsc.wsConn.Close()
|
||||
}
|
||||
|
||||
func newWSConnection(clientTLSConfig *tls.Config, r *http.Request) (OriginConnection, *http.Response, error) {
|
||||
d := &gws.Dialer{
|
||||
TLSClientConfig: clientTLSConfig,
|
||||
}
|
||||
wsConn, resp, err := websocket.ClientConnect(r, d)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &wsConnection{
|
||||
wsConn,
|
||||
resp,
|
||||
}, resp, nil
|
||||
}
|
||||
|
||||
// socksProxyOverWSConnection is an OriginConnection that streams SOCKS connections over WS.
|
||||
// The connection to the origin happens inside the SOCKS code as the client specifies the origin
|
||||
// details in the packet.
|
||||
@@ -118,8 +68,26 @@ type socksProxyOverWSConnection struct {
|
||||
}
|
||||
|
||||
func (sp *socksProxyOverWSConnection) Stream(ctx context.Context, tunnelConn io.ReadWriter, log *zerolog.Logger) {
|
||||
socks.StreamNetHandler(websocket.NewConn(ctx, tunnelConn, log), sp.accessPolicy, log)
|
||||
wsCtx, cancel := context.WithCancel(ctx)
|
||||
wsConn := websocket.NewConn(wsCtx, tunnelConn, log)
|
||||
socks.StreamNetHandler(wsConn, sp.accessPolicy, log)
|
||||
cancel()
|
||||
// Makes sure wsConn stops sending ping before terminating the stream
|
||||
wsConn.WaitForShutdown()
|
||||
}
|
||||
|
||||
func (sp *socksProxyOverWSConnection) Close() {
|
||||
}
|
||||
|
||||
// wsProxyConnection represents a bidirectional stream for a websocket connection to the origin
|
||||
type wsProxyConnection struct {
|
||||
rwc io.ReadWriteCloser
|
||||
}
|
||||
|
||||
func (conn *wsProxyConnection) Stream(ctx context.Context, tunnelConn io.ReadWriter, log *zerolog.Logger) {
|
||||
websocket.Stream(tunnelConn, conn.rwc, log)
|
||||
}
|
||||
|
||||
func (conn *wsProxyConnection) Close() {
|
||||
conn.rwc.Close()
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package ingress
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
@@ -13,14 +12,17 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/socks"
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
gorillaWS "github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/proxy"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/socks"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -156,7 +158,7 @@ func TestSocksStreamWSOverTCPConnection(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
defer wsForwarderInConn.Close()
|
||||
|
||||
Stream(wsForwarderInConn, &wsEyeball{wsForwarderOutConn}, testLogger)
|
||||
websocket.Stream(wsForwarderInConn, &wsEyeball{wsForwarderOutConn}, testLogger)
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -188,37 +190,51 @@ func TestSocksStreamWSOverTCPConnection(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamWSConnection(t *testing.T) {
|
||||
eyeballConn, edgeConn := net.Pipe()
|
||||
func TestWsConnReturnsBeforeStreamReturns(t *testing.T) {
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
eyeballConn, err := connection.NewHTTP2RespWriter(r, w, connection.TypeWebsocket)
|
||||
assert.NoError(t, err)
|
||||
|
||||
origin := echoWSOrigin(t)
|
||||
defer origin.Close()
|
||||
cfdConn, originConn := net.Pipe()
|
||||
tcpOverWSConn := tcpOverWSConnection{
|
||||
conn: cfdConn,
|
||||
streamHandler: DefaultStreamHandler,
|
||||
}
|
||||
go func() {
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
// Simulate losing connection to origin
|
||||
originConn.Close()
|
||||
}()
|
||||
ctx := context.WithValue(r.Context(), websocket.PingPeriodContextKey, time.Microsecond)
|
||||
tcpOverWSConn.Stream(ctx, eyeballConn, testLogger)
|
||||
})
|
||||
server := httptest.NewServer(handler)
|
||||
defer server.Close()
|
||||
client := server.Client()
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, origin.URL, nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Sec-Websocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
|
||||
|
||||
clientTLSConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
wsConn, resp, err := newWSConnection(clientTLSConfig, req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusSwitchingProtocols, resp.StatusCode)
|
||||
require.Equal(t, "Upgrade", resp.Header.Get("Connection"))
|
||||
require.Equal(t, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", resp.Header.Get("Sec-Websocket-Accept"))
|
||||
require.Equal(t, "websocket", resp.Header.Get("Upgrade"))
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testStreamTimeout)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
errGroup, ctx := errgroup.WithContext(ctx)
|
||||
errGroup.Go(func() error {
|
||||
echoWSEyeball(t, eyeballConn)
|
||||
return nil
|
||||
})
|
||||
for i := 0; i < 50; i++ {
|
||||
eyeballConn, edgeConn := net.Pipe()
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodConnect, server.URL, edgeConn)
|
||||
assert.NoError(t, err)
|
||||
|
||||
wsConn.Stream(ctx, edgeConn, testLogger)
|
||||
require.NoError(t, errGroup.Wait())
|
||||
resp, err := client.Transport.RoundTrip(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, resp.StatusCode, http.StatusOK)
|
||||
|
||||
errGroup.Go(func() error {
|
||||
for {
|
||||
if err := wsutil.WriteClientBinary(eyeballConn, testMessage); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
assert.NoError(t, errGroup.Wait())
|
||||
}
|
||||
|
||||
type wsEyeball struct {
|
||||
@@ -239,17 +255,23 @@ func (wse *wsEyeball) Write(p []byte) (int, error) {
|
||||
}
|
||||
|
||||
func echoWSEyeball(t *testing.T, conn net.Conn) {
|
||||
require.NoError(t, wsutil.WriteClientBinary(conn, testMessage))
|
||||
defer func() {
|
||||
assert.NoError(t, conn.Close())
|
||||
}()
|
||||
|
||||
if !assert.NoError(t, wsutil.WriteClientBinary(conn, testMessage)) {
|
||||
return
|
||||
}
|
||||
|
||||
readMsg, err := wsutil.ReadServerBinary(conn)
|
||||
require.NoError(t, err)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
require.Equal(t, testResponse, readMsg)
|
||||
|
||||
require.NoError(t, conn.Close())
|
||||
assert.Equal(t, testResponse, readMsg)
|
||||
}
|
||||
|
||||
func echoWSOrigin(t *testing.T) *httptest.Server {
|
||||
func echoWSOrigin(t *testing.T, expectMessages bool) *httptest.Server {
|
||||
var upgrader = gorillaWS.Upgrader{
|
||||
ReadBufferSize: 10,
|
||||
WriteBufferSize: 10,
|
||||
@@ -266,12 +288,17 @@ func echoWSOrigin(t *testing.T) *httptest.Server {
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
|
||||
sawMessage := false
|
||||
for {
|
||||
messageType, p, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
if expectMessages && !sawMessage {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.Equal(t, testMessage, p)
|
||||
assert.Equal(t, testMessage, p)
|
||||
sawMessage = true
|
||||
if err := conn.WriteMessage(messageType, testResponse); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user