Compare commits

..

89 Commits

Author SHA1 Message Date
Michael Borkenstein e9167f7f58 Release 2021.3.3 2021-03-25 12:41:25 -05:00
Michael Borkenstein 4494a27fab Update changelog 2021.3.3 2021-03-25 11:51:12 -05:00
Michael Borkenstein 63833b07dd AUTH-3455: Generate short-lived ssh cert per hostname 2021-03-25 10:38:43 -05:00
Igor Postelnik da4d0b2bae 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 10:53:29 -05:00
cthuang 027168c23a TUN-4123: Don't capture output in reconnect componet test 2021-03-24 14:14:47 +00:00
Igor Postelnik 50435546c5 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 08:26:22 -05:00
Igor Postelnik 9018ee5d5e TUN-4116: Ingore credentials-file setting in configuration file during tunnel create and delete opeations.
This change has two parts:
1. Update to newer version of the urfave/cli fork that correctly sets flag value along the context hierarchy while respecting config file overide behavior of the most specific instance of the flag.
2. Redefine --credentials-file flag so that create and delete subcommand don't use value from the config file.
2021-03-24 08:15:36 -05:00
cthuang 12447677cf TUN-4112: Skip testing graceful shutdown with SIGINT on Windows 2021-03-23 21:52:10 +00:00
Nuno Diegues 6106737281 TUN-4082: Test logging when running as a service 2021-03-23 20:14:53 +00:00
Nuno Diegues 8250b67a9f TUN-4111: Warn the user if both properties "tunnel" and "hostname" are used 2021-03-23 20:14:29 +00:00
Michael Borkenstein db5c6f2556 Release 2021.3.2 2021-03-23 11:08:54 -05:00
Michael Borkenstein 9dd7898792 Publish changelog for 2021.3.2 2021-03-23 10:31:46 -05:00
cthuang 92b3e8311f TUN-4042: Capture cloudflared output to debug component tests 2021-03-23 13:21:37 +00:00
Nuno Diegues 4a7763e497 TUN-3998: Allow to cleanup the connections of a tunnel limited to a single client 2021-03-23 08:48:54 +00:00
cthuang 9767ba1853 TUN-4096: Reduce tunnel not connected assertion backoff to address flaky termination tests 2021-03-18 08:28:38 +00:00
Michael Borkenstein 2c75326021 AUTH-3394: Ensure scheme on token command 2021-03-17 10:50:03 -05:00
Igor Postelnik 9023daba24 TUN-3715: Apply input source to the correct context 2021-03-17 14:59:39 +00:00
Nuno Diegues 89d0e45d62 TUN-3993: New cloudflared tunnel info to obtain details about the active connectors for a tunnel 2021-03-17 14:08:18 +00:00
Igor Postelnik a34099724e TUN-4094: Don't read configuration file for access commands 2021-03-16 17:36:46 -05:00
Igor Postelnik 8c5498fad1 TUN-3715: Only read config file once, right before invoking the command 2021-03-16 17:22:13 -05:00
Adam Chalmers 2c746b3361 TUN-4081: Update log severities to use Zerolog's levels 2021-03-16 19:04:49 +00:00
cthuang 954cd6adca TUN-4091: Use flaky decorator to rerun reconnect component tests when they fail 2021-03-16 17:10:15 +00:00
Nuno Diegues 8432735867 TUN-4060: Fix Go Vet warnings (new with go 1.16) where t.Fatalf is called from a test goroutine 2021-03-16 16:12:11 +00:00
cthuang d67fbbf94f TUN-4089: Address flakiness in component tests for termination 2021-03-16 11:31:20 +00:00
Nuno Diegues 39901e1d60 Release 2021.3.1 2021-03-15 18:46:26 +00:00
cthuang 9df60276a9 TUN-4052: Add component tests to assert service mode behavior 2021-03-15 17:45:25 +00:00
cthuang 6a9ba61242 TUN-4051: Add component-tests to test graceful shutdown 2021-03-15 14:41:32 +00:00
Nuno Diegues 848c44bd0b Release 2021.3.0 2021-03-15 11:49:44 +00:00
Nuno Diegues 9f84706eae Publish change log for 2021.3.0 2021-03-15 10:28:11 +00:00
Michael Borkenstein 841344f1e7 AUTH-3394: Creates a token per app instead of per path - with fix for
free tunnels
2021-03-12 15:49:47 +00:00
cthuang 25cfbec072 TUN-4050: Add component tests to assert reconnect behavior 2021-03-12 09:29:29 +00:00
cthuang f23e33c082 TUN-4049: Add component tests to assert logging behavior when running from terminal 2021-03-12 09:18:15 +00:00
Nuno Diegues d22b374208 TUN-4066: Set permissions in build agent before 'scp'-ing to machine hosting package repo 2021-03-11 19:02:26 +00:00
Nuno Diegues d6e867d4d1 TUN-4066: Remove unnecessary chmod during package publish to CF_PKG_HOSTS 2021-03-11 11:43:34 +00:00
cthuang a7344435a5 TUN-4062: Read component tests config from yaml file 2021-03-10 21:29:33 +00:00
Lee Valentine 206523344f TUN-4017: Add support for using cloudflared as a full socks proxy.
To use cloudflared as a socks proxy, add an ingress on the server
side with your desired rules. Rules are matched in the order they
are added.  If there are no rules, it is an implicit allow.  If
there are rules, but no rule matches match, the connection is denied.

ingress:
  - hostname: socks.example.com
    service: socks-proxy
    originRequest:
      ipRules:
        - prefix: 1.1.1.1/24
          ports: [80, 443]
          allow: true
        - prefix: 0.0.0.0/0
          allow: false

On the client, run using tcp mode:
cloudflared access tcp --hostname socks.example.com --url 127.0.0.1:8080

Set your socks proxy as 127.0.0.1:8080 and you will now be proxying
all connections to the remote machine.
2021-03-10 21:26:12 +00:00
Adam Chalmers b0e69c4b8a Revert "AUTH-3394: Creates a token per app instead of per path"
This reverts commit 8e340d9598.
2021-03-10 13:54:38 -06:00
Adam Chalmers aa5ebb817a TUN-4075: Dedup test should not compare order of list 2021-03-10 13:48:59 -06:00
Michael Borkenstein 8e340d9598 AUTH-3394: Creates a token per app instead of per path 2021-03-10 17:15:16 +00:00
Nuno Diegues 4296b23087 TUN-4069: Fix regression on support for websocket over proxy 2021-03-09 19:43:10 +00:00
Benjamin Buzbee 452f8cef79 Allow partial reads from a GorillaConn; add SetDeadline (from net.Conn) (#330)
* Allow partial reads from a GorillaConn; add SetDeadline (from net.Conn)

The current implementation of GorillaConn will drop data if the
websocket frame isn't read 100%. For example, if a websocket frame is
size=3, and Read() is called with a []byte of len=1, the 2 other bytes
in the frame are lost forever.

This is currently masked by the fact that this is used primarily in
io.Copy to another socket (in ingress.Stream) - as long as the read buffer
used by io.Copy is big enough (it is 32*1024, so in theory we could see
this today?) then data is copied over to the other socket.

The client then can do partial reads just fine as the kernel will take
care of the buffer from here on out.

I hit this by trying to create my own tunnel and avoiding
ingress.Stream, but this could be a real bug today I think if a
websocket frame bigger than 32*1024 was received, although it is also
possible that we are lucky and the upstream size which I haven't checked
uses a smaller buffer than that always.

The test I added hangs before my change, succeeds after.

Also add SetDeadline so that GorillaConn fully implements net.Conn

* Comment formatting; fast path

* Avoid intermediate buffer for first len(p) bytes; import order
2021-03-09 19:57:04 +04:00
Igor Postelnik 39065377b5 TUN-4063: Cleanup dependencies between packages.
- Move packages the provide generic functionality (such as config) from `cmd` subtree to top level.
- Remove all dependencies on `cmd` subtree from top level packages.
- Consolidate all code dealing with token generation and transfer to a single cohesive package.
2021-03-09 14:02:59 +00:00
Areg Harutyunyan d83d6d54ed TUN-3905: Cannot run go mod vendor in cloudflared due to fips 2021-03-09 17:31:59 +04:00
Nuno Diegues a2b41ea3e6 TUN-4016: Delegate decision to update for Worker service 2021-03-08 19:25:42 +00:00
cthuang 4481b9e46c TUN-4047: Add cfsetup target to run component test 2021-03-08 11:57:18 +00:00
cthuang e5d6f969be TUN-4055: Skeleton for component tests 2021-03-08 11:08:34 +00:00
Adam Chalmers ded9dec4f0 TUN-3819: Remove client-side check that deleted tunnels have no connections 2021-03-05 21:21:10 +00:00
Nuno Diegues 89b738f8fa TUN-4026: Fix regression where HTTP2 edge transport was no longer propagating control plane errors 2021-03-04 18:45:39 +00:00
Adam Chalmers 4f88982584 TUN-3994: Log client_id when running a named tunnel 2021-03-03 17:27:23 +00:00
Nuno Diegues bcd71b56e9 TUN-3989: Check in with Updater service in more situations and convey messages to user 2021-03-03 13:57:04 +00:00
Adam Chalmers 5c7b451e17 TUN-3995: Optional --features flag for tunnel run.
These features will be included in the ClientInfo.Features field when
running a named tunnel.
2021-03-02 16:21:17 -06:00
cthuang b73c039070 TUN-3988: Log why it cannot check if origin cert exists 2021-03-01 21:37:44 +00:00
PaulC 53a69a228a Issue #285 - Makefile does not detect TARGET_ARCH correctly on FreeBSD (#325)
* Issue-285: Detect TARGET_ARCH correctly for FreeBSD amd64 (uname -m returns amd64 not x86_64)

See: https://github.com/cloudflare/cloudflared/issues/285

* Add note not to `go get github.com/BurntSushi/go-sumtype` in build directory as this will cause vendor issues

Co-authored-by: PaulC <paulc@>
2021-03-01 21:43:08 +04:00
Areg Harutyunyan eda3a7a305 TUN-3983: Renew CA certs in cloudflared 2021-03-01 16:30:28 +00:00
Nuno Diegues f1ca2de515 TUN-3978: Unhide teamnet commands and improve their help 2021-03-01 11:59:46 +00:00
Adam Chalmers 27507ab192 TUN-3970: Route ip show has alias route ip list 2021-02-26 17:15:43 +00:00
Igor Postelnik 6db934853d TUN-3963: Repoint urfave/cli/v2 library at patched branch at github.com/ipostelnik/cli/v2@fixed which correctly handles reading flags declared at multiple levels of subcommands. 2021-02-24 20:04:59 +00:00
Nuno Diegues 792520d313 Release 2021.2.5 2021-02-23 18:33:42 +00:00
Nuno Diegues 8b9cfcde78 Publish change notes for 2021.2.5 2021-02-23 17:23:46 +00:00
Nuno Diegues 5ba3b3b309 TUN-3939: Add logging that shows that Warp-routing is enabled 2021-02-23 14:19:47 +00:00
cthuang 63a29f421a TUN-3895: Tests for socks stream handler 2021-02-23 14:19:47 +00:00
Sudarsan Reddy e20c4f8752 TUN-3838: ResponseWriter no longer reads and origin error tests 2021-02-23 14:19:47 +00:00
cthuang ab4dda5427 TUN-3868: Refactor singleTCPService and bridgeService to tcpOverWSService and rawTCPService 2021-02-23 14:19:47 +00:00
cthuang 5943808746 TUN-3889: Move host header override logic to httpService 2021-02-23 14:19:47 +00:00
Sudarsan Reddy ed57ee64e8 TUN-3853: Respond with ws headers from the origin service rather than generating our own 2021-02-23 14:19:47 +00:00
Igor Postelnik 9c298e4851 TUN-3855: Add ability to override target of 'access ssh' command to a different host for testing 2021-02-23 14:19:47 +00:00
Sudarsan Reddy 8b794390e5 TUN-3799: extended the Stream interface to take a logger and added debug logs for io.Copy errors 2021-02-23 14:19:47 +00:00
Sudarsan Reddy a6c2348127 TUN-3817: Adds tests for websocket based streaming regression 2021-02-23 14:19:47 +00:00
Nuno Diegues 6681d179dc TUN-3809: Allow routes ip show to output as JSON or YAML
It also fixes the marshelling of CIDR into JSON since otherwise
it would show garbled characters as the mask.
2021-02-23 14:19:47 +00:00
cthuang 2146f71b45 TUN-3753: Select http2 protocol when warp routing is enabled 2021-02-23 14:19:47 +00:00
cthuang 3b93914612 TUN-3764: Actively flush data for TCP streams 2021-02-23 14:19:47 +00:00
Sudarsan Reddy b4700a52e3 TUN-3725: Warp-routing is independent of ingress
- Changed warp-routing configuration to its own yaml.
    - Ingress Rules host matching is indepedent of warp-routing.
2021-02-23 14:19:47 +00:00
Sudarsan Reddy 368066a966 TUN-3615: added support to proxy tcp streams
added ingress.DefaultStreamHandler and a basic test for tcp stream proxy
moved websocket.Stream to ingress
cloudflared no longer picks tcpstream host from header
2021-02-23 14:19:47 +00:00
cthuang e2262085e5 TUN-3617: Separate service from client, and implement different client for http vs. tcp origins
- extracted ResponseWriter from proxyConnection
 - added bastion tests over websocket
 - removed HTTPResp()
 - added some docstrings
 - Renamed some ingress clients as proxies
 - renamed instances of client to proxy in connection and origin
 - Stream no longer takes a context and logger.Service
2021-02-23 14:19:44 +00:00
cthuang 5e2b43adb5 Release 2021.2.4 2021-02-23 12:47:21 +00:00
cthuang c7dca16300 Publish release notes for 2021.2.4 2021-02-23 12:39:18 +00:00
cthuang 9d5bd256be TUN-3964: Revert "TUN-3922: Repoint urfave/cli/v2 library at patched branch at github.com/ipostelnik/cli/v2@fixed which correctly handles reading flags declared at multiple levels of subcommands."
This reverts commit 1670ee87fb.
2021-02-23 12:32:07 +00:00
Nuno Diegues e9c2afec56 TUN-3948: Log error when retrying connection 2021-02-23 11:40:29 +00:00
cthuang 4e33281337 Release 2021.2.3 2021-02-23 10:28:46 +00:00
cthuang ac559f86c9 Publish release notes for 2021.2.3 2021-02-23 10:17:43 +00:00
Areg Harutyunyan 117766562b TUN-3945: Fix runApp signature for generic service 2021-02-19 22:06:57 +00:00
jmonson 5d76e940c7 Added support for proxy (#318)
Co-authored-by: Jeff Monson <jeff.monson@armis.com>
2021-02-19 21:27:22 +04:00
Igor Postelnik 1670ee87fb TUN-3922: Repoint urfave/cli/v2 library at patched branch at github.com/ipostelnik/cli/v2@fixed which correctly handles reading flags declared at multiple levels of subcommands. 2021-02-18 18:12:20 +00:00
Igor Postelnik a8ae6de213 TUN-3924: Removed db-connect command. Added a placeholder handler for this command that informs users that command is no longer supported. 2021-02-17 20:13:51 -06:00
David Jimenez d7c4a89106 Add max upstream connections dns-proxy option (#290)
* Add max upstream connections dns-proxy option

Allows defining a limit to the number of connections that can be
established with the upstream DNS host.

If left unset, there may be situations where connections fail to
establish, which causes the Transport to create an influx of connections
causing upstream to throttle our requests and triggering a runaway
effect resulting in high CPU usage. See https://github.com/cloudflare/cloudflared/issues/91

* Code review with proposed changes

* Add max upstream connections flag to tunnel flags

* Reduce DNS proxy max upstream connections default value

Reduce the default value of maximum upstream connections on the DNS
proxy to guarantee it works on single-core and other low-end hardware.
Further testing could allow for a safe increase of this value.

* Update dns-proxy flag name

Also remove `MaxUpstreamConnsFlag` const as it's no longer referenced in more than one place and to make things more consistent with how the other flags are referenced.

Co-authored-by: Adam Chalmers <achalmers@cloudflare.com>
2021-02-12 21:32:29 +04:00
Nuno Diegues e7354f4768 Update the TryCloudflare link 2021-02-11 15:19:28 +00:00
Samuel Rhea 18ec338d4c Update README.md
Updates to the new Cloudflare documentation links
2021-02-11 15:19:28 +00:00
Adam Chalmers 7f97e2f030 TUN-3913: Help gives wrong exit code for autoupdate 2021-02-11 08:39:56 -06:00
Adam Chalmers a278753bbf TUN-3902: Add jitter to backoffhandler
Jitter is important to avoid every cloudflared in the world trying to
reconnect at t=1, 2, 4, etc. That could overwhelm the backend. But
if each cloudflared randomly waits for up to 2, then up to 4, then up
to 8 etc, then the retries get spread out evenly across time.

On average, wait times should be the same (e.g. instead of waiting for
exactly 1 second, cloudflared will wait betweeen 0 and 2 seconds).

This is the "Full Jitter" algorithm from https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
2021-02-11 14:36:13 +00:00
495 changed files with 8250 additions and 347324 deletions
+8 -12
View File
@@ -1,21 +1,17 @@
.GOPATH/
bin/
tmp/
guide/public
/.GOPATH
/tmp
/bin
.idea
.build
.vscode
\#*\#
cscope.*
cloudflared
cloudflared.pkg
cloudflared.exe
cloudflared.msi
cloudflared-x86-64*
!cmd/cloudflared/
/cloudflared
/cloudflared.pkg
/cloudflared.exe
/cloudflared.msi
/cloudflared-x86-64*
/packaging
.DS_Store
*-session.log
ssh_server_tests/.env
.cover
/.cover
+106
View File
@@ -0,0 +1,106 @@
**Experimental**: This is a new format for release notes. The format and availability is subject to change.
## 2021.3.3
### Improvements
- Tunnel create command, as well as, running ad-hoc tunnels using `cloudflared tunnel -name NAME`, will not overwrite
existing files when writing tunnel credentials.
### Bug Fixes
- Tunnel create and delete commands no longer use path to credentials from the configuration file.
If you need ot place tunnel credentials file at a specific location, you must use `--credentials-file` flag.
- Access ssh-gen creates properly named keys for SSH short lived certs.
## 2021.3.2
### New Features
- It is now possible to obtain more detailed information about the cloudflared connectors to Cloudflare Edge via
`cloudflared tunnel info <name/uuid>`. It is possible to sort the output as well as output in different formats,
such as: `cloudflared tunnel info --sort-by version --invert-sort --output json <name/uuid>`.
You can obtain more information via `cloudflared tunnel info --help`.
### Bug Fixes
- Don't look for configuration file in default paths when `--config FILE` flag is present after `tunnel` subcommand.
- cloudflared access token command now functions correctly with the new token-per-app change from 2021.3.0.
## 2021.3.0
### New Features
- [Cloudflare One Routing](https://developers.cloudflare.com/cloudflare-one/tutorials/warp-to-tunnel) specific commands
now show up in the `cloudflared tunnel route --help` output.
- There is a new ingress type that allows cloudflared to proxy SOCKS5 as a bastion. You can use it with an ingress
rule by adding `service: socks-proxy`. Traffic is routed to any destination specified by the SOCKS5 packet but only
if allowed by a rule. In the following example we allow proxying to a certain CIDR but explicitly forbid one address
within it:
```
ingress:
- hostname: socks.example.com
service: socks-proxy
originRequest:
ipRules:
- prefix: 192.168.1.8/32
allow: false
- prefix: 192.168.1.0/24
ports: [80, 443]
allow: true
```
### Improvements
- Nested commands, such as `cloudflared tunnel run`, now consider CLI arguments even if they appear earlier on the
command. For instance, `cloudflared --config config.yaml tunnel run` will now behave the same as
`cloudflared tunnel --config config.yaml run`
- Warnings are now shown in the output logs whenever cloudflared is running without the most recent version and
`no-autoupdate` is `true`.
- Access tokens are now stored per Access App instead of per request path. This decreases the number of times that the
user is required to authenticate with an Access policy redundantly.
### Bug Fixes
- GitHub [PR #317](https://github.com/cloudflare/cloudflared/issues/317) was broken in 2021.2.5 and is now fixed again.
## 2021.2.5
### New Features
- We introduce [Cloudflare One Routing](https://developers.cloudflare.com/cloudflare-one/tutorials/warp-to-tunnel) in
beta mode. Cloudflare customer can now connect users and private networks with RFC 1918 IP addresses via the
Cloudflare edge network. Users running Cloudflare WARP client in the same organization can connect to the services
made available by Argo Tunnel IP routes. Please share your feedback in the GitHub issue tracker.
## 2021.2.4
### Bug Fixes
- Reverts the Improvement released in 2021.2.3 for CLI arguments as it introduced a regression where cloudflared failed
to read URLs in configuration files.
- cloudflared now logs the reason for failed connections if the error is recoverable.
## 2021.2.3
### Backward Incompatible Changes
- Removes db-connect. The Cloudflare Workers product will continue to support db-connect implementations with versions
of cloudflared that predate this release and include support for db-connect.
### New Features
- Introduces support for proxy configurations with websockets in arbitrary TCP connections (#318).
### Improvements
- (reverted) Nested command line argument handling.
### Bug Fixes
- The maximum number of upstream connections is now limited by default which should fix reported issues of cloudflared
exhausting CPU usage when faced with connectivity issues.
+19 -3
View File
@@ -34,6 +34,8 @@ ifneq ($(GOARCH),)
TARGET_ARCH ?= $(GOARCH)
else ifeq ($(LOCAL_ARCH),x86_64)
TARGET_ARCH ?= amd64
else ifeq ($(LOCAL_ARCH),amd64)
TARGET_ARCH ?= amd64
else ifeq ($(LOCAL_ARCH),i686)
TARGET_ARCH ?= amd64
else ifeq ($(shell echo $(LOCAL_ARCH) | head -c 5),armv8)
@@ -80,8 +82,18 @@ clean:
.PHONY: cloudflared
cloudflared: tunnel-deps
ifeq ($(FIPS), true)
$(info Building cloudflared with go-fips)
-test -f fips/fips.go && mv fips/fips.go fips/fips.go.linux-amd64
mv fips/fips.go.linux-amd64 fips/fips.go
endif
GOOS=$(TARGET_OS) GOARCH=$(TARGET_ARCH) go build -v -mod=vendor $(GO_BUILD_TAGS) $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/cloudflared
ifeq ($(FIPS), true)
mv fips/fips.go fips/fips.go.linux-amd64
endif
.PHONY: container
container:
docker build --build-arg=TARGET_ARCH=$(TARGET_ARCH) --build-arg=TARGET_OS=$(TARGET_OS) -t cloudflare/cloudflared-$(TARGET_OS)-$(TARGET_ARCH):"$(VERSION)" .
@@ -101,10 +113,10 @@ test-ssh-server:
docker-compose -f ssh_server_tests/docker-compose.yml up
define publish_package
chmod 664 cloudflared*.$(1); \
for HOST in $(CF_PKG_HOSTS); do \
ssh-keyscan -t rsa $$HOST >> ~/.ssh/known_hosts; \
scp -4 cloudflared*.$(1) cfsync@$$HOST:/state/cf-pkg/staging/$(2)/$(TARGET_PUBLIC_REPO)/cloudflared/; \
ssh cfsync@$$HOST 'chmod g+w /state/cf-pkg/staging/$(2)/$(TARGET_PUBLIC_REPO)/cloudflared/*.$(1)'; \
scp -p -4 cloudflared*.$(1) cfsync@$$HOST:/state/cf-pkg/staging/$(2)/$(TARGET_PUBLIC_REPO)/cloudflared/; \
done
endef
@@ -236,9 +248,13 @@ tunnelrpc/tunnelrpc.capnp.go: tunnelrpc/tunnelrpc.capnp
.PHONY: vet
vet:
go vet -mod=vendor ./...
which go-sumtype # go get github.com/BurntSushi/go-sumtype
which go-sumtype # go get github.com/BurntSushi/go-sumtype (don't do this in build directory or this will cause vendor issues)
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
+9 -9
View File
@@ -1,6 +1,6 @@
# Argo Tunnel client
Contains the command-line client for Argo Tunnel, a tunneling daemon that proxies any local webserver through the Cloudflare network. Extensive documentation can be found in the [Argo Tunnel section](https://developers.cloudflare.com/argo-tunnel/) of the Cloudflare Docs.
Contains the command-line client for Argo Tunnel, a tunneling daemon that proxies any local webserver through the Cloudflare network. Extensive documentation can be found in the [Argo Tunnel section](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps) of the Cloudflare Docs.
## Before you get started
@@ -13,29 +13,29 @@ Before you use Argo Tunnel, you'll need to complete a few steps in the Cloudflar
Downloads are available as standalone binaries, a Docker image, and Debian, RPM, and Homebrew packages. You can also find releases here on the `cloudflared` GitHub repository.
* You can [install on macOS](https://developers.cloudflare.com/argo-tunnel/getting-started/installation#macos) via Homebrew or by downloading the [latest Darwin amd64 release](https://github.com/cloudflare/cloudflared/releases)
* Binaries, Debian, and RPM packages for Linux [can be found here](https://developers.cloudflare.com/argo-tunnel/getting-started/installation#linux)
* You can [install on macOS](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#macos) via Homebrew or by downloading the [latest Darwin amd64 release](https://github.com/cloudflare/cloudflared/releases)
* Binaries, Debian, and RPM packages for Linux [can be found here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#linux)
* A Docker image of `cloudflared` is [available on DockerHub](https://hub.docker.com/r/cloudflare/cloudflared)
* You can install on Windows machines with the [steps here](https://developers.cloudflare.com/argo-tunnel/getting-started/installation#windows)
* You can install on Windows machines with the [steps here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#windows)
User documentation for Argo Tunnel can be found at https://developers.cloudflare.com/argo-tunnel/
User documentation for Argo Tunnel can be found at https://developers.cloudflare.com/cloudflare-one/connections/connect-apps
## Creating Tunnels and routing traffic
Once installed, you can authenticate `cloudflared` into your Cloudflare account and begin creating Tunnels that serve traffic for hostnames in your account.
* Create a Tunnel with [these instructions](https://developers.cloudflare.com/argo-tunnel/create-tunnel)
* Route traffic to that Tunnel with [DNS records in Cloudflare](https://developers.cloudflare.com/argo-tunnel/routing-to-tunnel/dns) or with a [Cloudflare Load Balancer](https://developers.cloudflare.com/argo-tunnel/routing-to-tunnel/lb)
* Create a Tunnel with [these instructions](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/create-tunnel)
* Route traffic to that Tunnel with [DNS records in Cloudflare](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/routing-to-tunnel/dns) or with a [Cloudflare Load Balancer](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/routing-to-tunnel/lb)
## 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/argo-tunnel/learning/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).
## 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/argo-tunnel/getting-started/installation#updating-cloudflared).
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).
| Version(s) | Deprecation status |
|---|---|
+96
View File
@@ -1,3 +1,99 @@
2021.3.3
- 2021-03-23 TUN-4111: Warn the user if both properties "tunnel" and "hostname" are used
- 2021-03-23 TUN-4082: Test logging when running as a service
- 2021-03-23 TUN-4112: Skip testing graceful shutdown with SIGINT on Windows
- 2021-03-23 TUN-4116: Ingore credentials-file setting in configuration file during tunnel create and delete opeations.
- 2021-03-23 TUN-4118: Don't overwrite existing file with tunnel credentials. For ad-hoc tunnels, this means tunnel won't start if there's a file in the way.
- 2021-03-24 TUN-4123: Don't capture output in reconnect componet test
- 2021-03-23 TUN-4067: Reformat code for consistent import order, grouping, and fix formatting. Added goimports target to the Makefile to make this easier in the future.
- 2021-03-24 AUTH-3455: Generate short-lived ssh cert per hostname
- 2021-03-25 Update changelog 2021.3.3
2021.3.2
- 2021-03-23 TUN-4042: Capture cloudflared output to debug component tests
- 2021-03-23 Publish changelog for 2021.3.2
- 2021-03-16 TUN-4089: Address flakiness in component tests for termination
- 2021-03-16 TUN-4060: Fix Go Vet warnings (new with go 1.16) where t.Fatalf is called from a test goroutine
- 2021-03-16 TUN-4091: Use flaky decorator to rerun reconnect component tests when they fail
- 2021-03-12 TUN-4081: Update log severities to use Zerolog's levels
- 2021-03-16 TUN-4094: Don't read configuration file for access commands
- 2021-03-15 TUN-3993: New `cloudflared tunnel info` to obtain details about the active connectors for a tunnel
- 2021-03-17 TUN-3715: Apply input source to the correct context
- 2021-03-17 AUTH-3394: Ensure scheme on token command
- 2021-03-18 TUN-4096: Reduce tunnel not connected assertion backoff to address flaky termination tests
- 2021-03-19 TUN-3998: Allow to cleanup the connections of a tunnel limited to a single client
- 2021-02-04 TUN-3715: Only read config file once, right before invoking the command
2021.3.1
- 2021-03-11 TUN-4051: Add component-tests to test graceful shutdown
- 2021-03-12 TUN-4052: Add component tests to assert service mode behavior
2021.3.0
- 2021-03-10 TUN-4075: Dedup test should not compare order of list
- 2021-03-10 Revert "AUTH-3394: Creates a token per app instead of per path"
- 2021-03-11 TUN-4066: Remove unnecessary chmod during package publish to CF_PKG_HOSTS
- 2021-03-11 TUN-4066: Set permissions in build agent before 'scp'-ing to machine hosting package repo
- 2021-03-11 TUN-4050: Add component tests to assert reconnect behavior
- 2021-03-10 AUTH-3394: Creates a token per app instead of per path - with fix for free tunnels
- 2021-03-15 Publish change log for 2021.3.0
- 2021-03-01 Issue #285 - Makefile does not detect TARGET_ARCH correctly on FreeBSD (#325)
- 2021-03-01 TUN-3988: Log why it cannot check if origin cert exists
- 2021-03-02 TUN-3995: Optional --features flag for tunnel run.
- 2021-03-02 TUN-3994: Log client_id when running a named tunnel
- 2021-03-04 TUN-4026: Fix regression where HTTP2 edge transport was no longer propagating control plane errors
- 2021-03-05 TUN-4055: Skeleton for component tests
- 2021-03-08 TUN-4047: Add cfsetup target to run component test
- 2021-03-08 TUN-4016: Delegate decision to update for Worker service
- 2021-03-02 TUN-3905: Cannot run go mod vendor in cloudflared due to fips
- 2021-03-08 TUN-4063: Cleanup dependencies between packages.
- 2021-03-09 Allow partial reads from a GorillaConn; add SetDeadline (from net.Conn) (#330)
- 2021-03-09 TUN-4069: Fix regression on support for websocket over proxy
- 2021-03-02 AUTH-3394: Creates a token per app instead of per path
- 2021-03-01 TUN-4017: Add support for using cloudflared as a full socks proxy.
- 2021-03-08 TUN-4062: Read component tests config from yaml file
- 2021-03-08 TUN-4049: Add component tests to assert logging behavior when running from terminal
- 2021-02-23 TUN-3963: Repoint urfave/cli/v2 library at patched branch at github.com/ipostelnik/cli/v2@fixed which correctly handles reading flags declared at multiple levels of subcommands.
- 2021-02-25 TUN-3970: Route ip show has alias route ip list
- 2021-02-26 TUN-3978: Unhide teamnet commands and improve their help
- 2021-02-26 TUN-3983: Renew CA certs in cloudflared
- 2021-02-28 TUN-3989: Check in with Updater service in more situations and convey messages to user
- 2021-02-11 TUN-3819: Remove client-side check that deleted tunnels have no connections
2021.2.5
- 2021-02-23 Publish change notes for 2021.2.5
- 2021-02-11 TUN-3838: ResponseWriter no longer reads and origin error tests
- 2021-02-10 TUN-3895: Tests for socks stream handler
- 2021-02-19 TUN-3939: Add logging that shows that Warp-routing is enabled
- 2021-02-02 TUN-3817: Adds tests for websocket based streaming regression
- 2021-02-04 TUN-3799: extended the Stream interface to take a logger and added debug logs for io.Copy errors
- 2021-02-03 TUN-3855: Add ability to override target of 'access ssh' command to a different host for testing
- 2021-02-04 TUN-3853: Respond with ws headers from the origin service rather than generating our own
- 2021-02-08 TUN-3889: Move host header override logic to httpService
- 2021-02-05 TUN-3868: Refactor singleTCPService and bridgeService to tcpOverWSService and rawTCPService
- 2021-01-21 TUN-3753: Select http2 protocol when warp routing is enabled
- 2021-01-26 TUN-3809: Allow routes ip show to output as JSON or YAML
- 2021-01-11 TUN-3615: added support to proxy tcp streams
- 2021-01-17 TUN-3725: Warp-routing is independent of ingress
- 2021-01-15 TUN-3764: Actively flush data for TCP streams
- 2020-12-09 TUN-3617: Separate service from client, and implement different client for http vs. tcp origins
2021.2.4
- 2021-02-22 TUN-3948: Log error when retrying connection
- 2021-02-23 TUN-3964: Revert "TUN-3922: Repoint urfave/cli/v2 library at patched branch at github.com/ipostelnik/cli/v2@fixed which correctly handles reading flags declared at multiple levels of subcommands."
- 2021-02-23 Publish release notes for 2021.2.4
2021.2.3
- 2021-02-23 Publish release notes for 2021.2.3
- 2021-02-10 TUN-3902: Add jitter to backoffhandler
- 2021-02-11 TUN-3913: Help gives wrong exit code for autoupdate
- 2021-02-12 Add max upstream connections dns-proxy option (#290)
- 2021-02-16 TUN-3924: Removed db-connect command. Added a placeholder handler for this command that informs users that command is no longer supported.
- 2021-02-12 TUN-3922: Repoint urfave/cli/v2 library at patched branch at github.com/ipostelnik/cli/v2@fixed which correctly handles reading flags declared at multiple levels of subcommands.
- 2021-02-19 Added support for proxy (#318)
- 2021-02-19 TUN-3945: Fix runApp signature for generic service
- 2021-02-09 Update README.md
- 2021-02-09 Update the TryCloudflare link
2021.2.2
- 2021-02-04 TUN-3864: Users can choose where credentials file is written after creating a tunnel
- 2021-02-04 TUN-3869: Improve reliability of graceful shutdown.
+11 -7
View File
@@ -4,24 +4,28 @@
package carrier
import (
"crypto/tls"
"io"
"net"
"net/http"
"os"
"strings"
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/token"
)
const LogFieldOriginURL = "originURL"
type StartOptions struct {
OriginURL string
Headers http.Header
AppInfo *token.AppInfo
OriginURL string
Headers http.Header
Host string
TLSClientConfig *tls.Config
}
// Connection wraps up all the needed functions to forward over the tunnel
@@ -120,7 +124,7 @@ func IsAccessResponse(resp *http.Response) bool {
if err != nil || location == nil {
return false
}
if strings.HasPrefix(location.Path, "/cdn-cgi/access/login") {
if strings.HasPrefix(location.Path, token.AccessLoginWorkerPath) {
return true
}
@@ -134,7 +138,7 @@ func BuildAccessRequest(options *StartOptions, log *zerolog.Logger) (*http.Reque
return nil, err
}
token, err := token.FetchTokenWithRedirect(req.URL, log)
token, err := token.FetchTokenWithRedirect(req.URL, options.AppInfo, log)
if err != nil {
return nil, err
}
+4 -3
View File
@@ -44,7 +44,7 @@ func (s *testStreamer) Write(p []byte) (int, error) {
func TestStartClient(t *testing.T) {
message := "Good morning Austin! Time for another sunny day in the great state of Texas."
log := zerolog.Nop()
wsConn := NewWSConnection(&log, false)
wsConn := NewWSConnection(&log)
ts := newTestWebSocketServer()
defer ts.Close()
@@ -70,7 +70,7 @@ func TestStartServer(t *testing.T) {
message := "Good morning Austin! Time for another sunny day in the great state of Texas."
log := zerolog.Nop()
shutdownC := make(chan struct{})
wsConn := NewWSConnection(&log, false)
wsConn := NewWSConnection(&log)
ts := newTestWebSocketServer()
defer ts.Close()
options := &StartOptions{
@@ -81,7 +81,8 @@ func TestStartServer(t *testing.T) {
go func() {
err := Serve(wsConn, listener, shutdownC, options)
if err != nil {
t.Fatalf("Error running server: %v", err)
t.Errorf("Error running server: %v", err)
return
}
}()
+33 -26
View File
@@ -7,12 +7,13 @@ import (
"net/http"
"net/http/httputil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
"github.com/cloudflare/cloudflared/socks"
cfwebsocket "github.com/cloudflare/cloudflared/websocket"
"github.com/gorilla/websocket"
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/socks"
"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,7 +24,7 @@ type Websocket struct {
}
type wsdialer struct {
conn *cfwebsocket.Conn
conn *cfwebsocket.GorillaConn
}
func (d *wsdialer) Dial(address string) (io.ReadWriteCloser, *socks.AddrSpec, error) {
@@ -37,10 +38,9 @@ func (d *wsdialer) Dial(address string) (io.ReadWriteCloser, *socks.AddrSpec, er
}
// NewWSConnection returns a new connection object
func NewWSConnection(log *zerolog.Logger, isSocks bool) Connection {
func NewWSConnection(log *zerolog.Logger) Connection {
return &Websocket{
log: log,
isSocks: isSocks,
log: log,
}
}
@@ -54,41 +54,52 @@ func (ws *Websocket) ServeStream(options *StartOptions, conn io.ReadWriter) erro
}
defer wsConn.Close()
if ws.isSocks {
dialer := &wsdialer{conn: wsConn}
requestHandler := socks.NewRequestHandler(dialer)
socksServer := socks.NewConnectionHandler(requestHandler)
_ = socksServer.Serve(conn)
} else {
cfwebsocket.Stream(wsConn, conn)
}
ingress.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, cfwebsocket.DefaultStreamHandler)
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
func createWebsocketStream(options *StartOptions, log *zerolog.Logger) (*cfwebsocket.Conn, error) {
func createWebsocketStream(options *StartOptions, log *zerolog.Logger) (*cfwebsocket.GorillaConn, error) {
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
if err != nil {
return nil, err
}
req.Header = options.Headers
if options.Host != "" {
req.Host = options.Host
}
dump, err := httputil.DumpRequest(req, false)
log.Debug().Msgf("Websocket request: %s", string(dump))
wsConn, resp, err := cfwebsocket.ClientConnect(req, nil)
dialer := &websocket.Dialer{
TLSClientConfig: options.TLSClientConfig,
Proxy: http.ProxyFromEnvironment,
}
wsConn, resp, err := cfwebsocket.ClientConnect(req, dialer)
defer closeRespBody(resp)
if err != nil && IsAccessResponse(resp) {
// Only get Access app info if we know the origin is protected by Access
originReq, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
if err != nil {
return nil, err
}
appInfo, err := token.GetAppInfo(originReq.URL)
if err != nil {
return nil, err
}
options.AppInfo = appInfo
wsConn, err = createAccessAuthenticatedStream(options, log)
if err != nil {
return nil, err
@@ -97,7 +108,7 @@ func createWebsocketStream(options *StartOptions, log *zerolog.Logger) (*cfwebso
return nil, err
}
return &cfwebsocket.Conn{Conn: wsConn}, nil
return &cfwebsocket.GorillaConn{Conn: wsConn}, nil
}
// createAccessAuthenticatedStream will try load a token from storage and make
@@ -117,11 +128,7 @@ func createAccessAuthenticatedStream(options *StartOptions, log *zerolog.Logger)
}
// Access Token is invalid for some reason. Go through regen flow
originReq, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
if err != nil {
return nil, err
}
if err := token.RemoveTokenIfExists(originReq.URL); err != nil {
if err := token.RemoveTokenIfExists(options.AppInfo); err != nil {
return nil, err
}
wsConn, resp, err = createAccessWebSocketStream(options, log)
+24 -14
View File
@@ -194,14 +194,35 @@ stretch: &stretch
builddeps:
- *pinned_go_fips
- build-essential
- gotest-to-teamcity
pre-cache:
- go get golang.org/x/tools/cmd/goimports
- go get github.com/BurntSushi/go-sumtype
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"
- make test
- ./fmt-check.sh
- make test | gotest-to-teamcity
component-test:
build_dir: *build_dir
builddeps:
- *pinned_go_fips
- python3.7
- python3-pip
- python3-setuptools
# procps installs the ps command which is needed in test_sysv_service because the init script
# uses ps pid to determine if the agent is running
- procps
pre-cache-copy-paths:
- component-tests/requirements.txt
pre-cache:
- sudo pip3 install --upgrade -r component-tests/requirements.txt
post-cache:
# Constructs config file from env vars
- python3 component-tests/config.py
- pytest component-tests
update-homebrew:
builddeps:
- openssh-client
@@ -275,14 +296,3 @@ centos-7:
- export GOOS=linux
- export GOARCH=amd64
- make cloudflared-rpm
# cfsetup compose
default-stack: test_dbconnect
test_dbconnect:
compose:
up-args:
- --renew-anon-volumes
- --abort-on-container-exit
- --exit-code-from=cloudflared
files:
- dbconnect_tests/dbconnect.yaml
+25 -4
View File
@@ -1,11 +1,13 @@
package access
import (
"crypto/tls"
"fmt"
"net/http"
"strings"
"github.com/cloudflare/cloudflared/carrier"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/validation"
@@ -46,7 +48,7 @@ func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, log *z
}
// we could add a cmd line variable for this bool if we want the SOCK5 server to be on the client side
wsConn := carrier.NewWSConnection(log, false)
wsConn := carrier.NewWSConnection(log)
log.Info().Str(LogFieldHost, validURL.Host).Msg("Start Websocket listener")
return carrier.StartForwarder(wsConn, validURL.Host, shutdown, options)
@@ -84,10 +86,30 @@ func ssh(c *cli.Context) error {
options := &carrier.StartOptions{
OriginURL: originURL,
Headers: headers,
Host: hostname,
}
if connectTo := c.String(sshConnectTo); connectTo != "" {
parts := strings.Split(connectTo, ":")
switch len(parts) {
case 1:
options.OriginURL = fmt.Sprintf("https://%s", parts[0])
case 2:
options.OriginURL = fmt.Sprintf("https://%s:%s", parts[0], parts[1])
case 3:
options.OriginURL = fmt.Sprintf("https://%s:%s", parts[2], parts[1])
options.TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
ServerName: parts[0],
}
log.Warn().Msgf("Using insecure SSL connection because SNI overridden to %s", parts[0])
default:
return fmt.Errorf("invalid connection override: %s", connectTo)
}
}
// we could add a cmd line variable for this bool if we want the SOCK5 server to be on the client side
wsConn := carrier.NewWSConnection(log, false)
wsConn := carrier.NewWSConnection(log)
if c.NArg() > 0 || c.IsSet(sshURLFlag) {
forwarder, err := config.ValidateUrl(c, true)
@@ -95,7 +117,6 @@ func ssh(c *cli.Context) error {
log.Err(err).Msg("Error validating origin URL")
return errors.Wrap(err, "error validating origin URL")
}
log.Info().Str(LogFieldHost, forwarder.Host).Msg("Start Websocket listener")
err = carrier.StartForwarder(wsConn, forwarder.Host, shutdownC, options)
if err != nil {
+83 -41
View File
@@ -2,27 +2,28 @@ package access
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"strings"
"text/template"
"time"
"github.com/cloudflare/cloudflared/carrier"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/sshgen"
"github.com/cloudflare/cloudflared/validation"
"github.com/getsentry/raven-go"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"github.com/urfave/cli/v2"
"golang.org/x/net/idna"
"github.com/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"
)
const (
@@ -33,12 +34,13 @@ const (
sshTokenIDFlag = "service-token-id"
sshTokenSecretFlag = "service-token-secret"
sshGenCertFlag = "short-lived-cert"
sshConnectTo = "connect-to"
sshConfigTemplate = `
Add to your {{.Home}}/.ssh/config:
Host {{.Hostname}}
{{- if .ShortLivedCerts}}
ProxyCommand bash -c '{{.Cloudflared}} access ssh-gen --hostname %h; ssh -tt %r@cfpipe-{{.Hostname}} >&2 <&1'
ProxyCommand bash -c '{{.Cloudflared}} access ssh-gen --hostname %h; ssh -tt %r@cfpipe-{{.Hostname}} >&2 <&1'
Host cfpipe-{{.Hostname}}
HostName {{.Hostname}}
@@ -54,7 +56,7 @@ Host cfpipe-{{.Hostname}}
const sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b@sentry.io/189878"
var (
shutdownC chan struct{}
shutdownC chan struct{}
)
// Init will initialize and store vars from the main program
@@ -75,30 +77,24 @@ func Commands() []*cli.Command {
Aliases: []string{"forward"},
Category: "Access",
Usage: "access <subcommand>",
Description: `Cloudflare Access protects internal resources by securing, authenticating and monitoring access
per-user and by application. With Cloudflare Access, only authenticated users with the required permissions are
able to reach sensitive resources. The commands provided here allow you to interact with Access protected
Description: `Cloudflare Access protects internal resources by securing, authenticating and monitoring access
per-user and by application. With Cloudflare Access, only authenticated users with the required permissions are
able to reach sensitive resources. The commands provided here allow you to interact with Access protected
applications from the command line.`,
Subcommands: []*cli.Command{
{
Name: "login",
Action: cliutil.ErrorHandler(login),
Action: cliutil.Action(login),
Usage: "login <url of access application>",
Description: `The login subcommand initiates an authentication flow with your identity provider.
The subcommand will launch a browser. For headless systems, a url is provided.
Once authenticated with your identity provider, the login command will generate a JSON Web Token (JWT)
scoped to your identity, the application you intend to reach, and valid for a session duration set by your
scoped to your identity, the application you intend to reach, and valid for a session duration set by your
administrator. cloudflared stores the token in local storage.`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "url",
Hidden: true,
},
},
},
{
Name: "curl",
Action: cliutil.ErrorHandler(curl),
Action: cliutil.Action(curl),
Usage: "curl [--allow-request, -ar] <url> [<curl args>...]",
Description: `The curl subcommand wraps curl and automatically injects the JWT into a cf-access-token
header when using curl to reach an application behind Access.`,
@@ -107,7 +103,7 @@ func Commands() []*cli.Command {
},
{
Name: "token",
Action: cliutil.ErrorHandler(generateToken),
Action: cliutil.Action(generateToken),
Usage: "token -app=<url of access application>",
ArgsUsage: "url of Access application",
Description: `The token subcommand produces a JWT which can be used to authenticate requests.`,
@@ -119,7 +115,7 @@ func Commands() []*cli.Command {
},
{
Name: "tcp",
Action: cliutil.ErrorHandler(ssh),
Action: cliutil.Action(ssh),
Aliases: []string{"rdp", "ssh", "smb"},
Usage: "",
ArgsUsage: "",
@@ -162,13 +158,18 @@ func Commands() []*cli.Command {
&cli.StringFlag{
Name: logger.LogSSHLevelFlag,
Aliases: []string{"loglevel"}, //added to match the tunnel side
Usage: "Application logging level {fatal, error, info, debug}. ",
Usage: "Application logging level {debug, info, warn, error, fatal}. ",
},
&cli.StringFlag{
Name: sshConnectTo,
Hidden: true,
Usage: "Connect to alternate location for testing, value is host, host:port, or sni:port:host",
},
},
},
{
Name: "ssh-config",
Action: cliutil.ErrorHandler(sshConfig),
Action: cliutil.Action(sshConfig),
Usage: "",
Description: `Prints an example configuration ~/.ssh/config`,
Flags: []cli.Flag{
@@ -184,7 +185,7 @@ func Commands() []*cli.Command {
},
{
Name: "ssh-gen",
Action: cliutil.ErrorHandler(sshGen),
Action: cliutil.Action(sshGen),
Usage: "",
Description: `Generates a short lived certificate for given hostname`,
Flags: []cli.Flag{
@@ -214,12 +215,18 @@ func login(c *cli.Context) error {
log.Error().Msg("Please provide the url of the Access application")
return err
}
if err := verifyTokenAtEdge(appURL, c, log); err != nil {
appInfo, err := token.GetAppInfo(appURL)
if err != nil {
return err
}
if err := verifyTokenAtEdge(appURL, appInfo, c, log); err != nil {
log.Err(err).Msg("Could not verify token")
return err
}
cfdToken, err := token.GetAppTokenIfExists(appURL)
cfdToken, err := token.GetAppTokenIfExists(appInfo)
if err != nil {
fmt.Fprintln(os.Stderr, "Unable to find token for provided application.")
return err
@@ -261,13 +268,17 @@ func curl(c *cli.Context) error {
return err
}
tok, err := token.GetAppTokenIfExists(appURL)
appInfo, err := token.GetAppInfo(appURL)
if err != nil {
return err
}
tok, err := token.GetAppTokenIfExists(appInfo)
if err != nil || tok == "" {
if allowRequest {
log.Info().Msg("You don't have an Access token set. Please run access token <access application> to fetch one.")
return shell.Run("curl", cmdArgs...)
return run("curl", cmdArgs...)
}
tok, err = token.FetchToken(appURL, log)
tok, err = token.FetchToken(appURL, appInfo, log)
if err != nil {
log.Err(err).Msg("Failed to refresh token")
return err
@@ -276,7 +287,28 @@ func curl(c *cli.Context) error {
cmdArgs = append(cmdArgs, "-H")
cmdArgs = append(cmdArgs, fmt.Sprintf("%s: %s", h2mux.CFAccessTokenHeader, tok))
return shell.Run("curl", cmdArgs...)
return run("curl", cmdArgs...)
}
// run kicks off a shell task and pipe the results to the respective std pipes
func run(cmd string, args ...string) error {
c := exec.Command(cmd, args...)
stderr, err := c.StderrPipe()
if err != nil {
return err
}
go func() {
io.Copy(os.Stderr, stderr)
}()
stdout, err := c.StdoutPipe()
if err != nil {
return err
}
go func() {
io.Copy(os.Stdout, stdout)
}()
return c.Run()
}
// token dumps provided token to stdout
@@ -284,12 +316,17 @@ func generateToken(c *cli.Context) error {
if err := raven.SetDSN(sentryDSN); err != nil {
return err
}
appURL, err := url.Parse(c.String("app"))
appURL, err := url.Parse(ensureURLScheme(c.String("app")))
if err != nil || c.NumFlags() < 1 {
fmt.Fprintln(os.Stderr, "Please provide a url.")
return err
}
tok, err := token.GetAppTokenIfExists(appURL)
appInfo, err := token.GetAppInfo(appURL)
if err != nil {
return err
}
tok, err := token.GetAppTokenIfExists(appInfo)
if err != nil || tok == "" {
fmt.Fprintln(os.Stderr, "Unable to find token for provided application. Please run login command to generate token.")
return err
@@ -340,7 +377,12 @@ func sshGen(c *cli.Context) error {
// this fetchToken function mutates the appURL param. We should refactor that
fetchTokenURL := &url.URL{}
*fetchTokenURL = *originURL
cfdToken, err := token.FetchTokenWithRedirect(fetchTokenURL, log)
appInfo, err := token.GetAppInfo(fetchTokenURL)
if err != nil {
return err
}
cfdToken, err := token.FetchTokenWithRedirect(fetchTokenURL, appInfo, log)
if err != nil {
return err
}
@@ -352,7 +394,7 @@ func sshGen(c *cli.Context) error {
return nil
}
// getAppURL will pull the appURL needed for fetching a user's Access token
// getAppURL will pull the request URL needed for fetching a user's Access token
func getAppURL(cmdArgs []string, log *zerolog.Logger) (*url.URL, error) {
if len(cmdArgs) < 1 {
log.Error().Msg("Please provide a valid URL as the first argument to curl.")
@@ -427,7 +469,7 @@ func isFileThere(candidate string) bool {
// verifyTokenAtEdge checks for a token on disk, or generates a new one.
// Then makes a request to to the origin with the token to ensure it is valid.
// Returns nil if token is valid.
func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context, log *zerolog.Logger) error {
func verifyTokenAtEdge(appUrl *url.URL, appInfo *token.AppInfo, c *cli.Context, log *zerolog.Logger) error {
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
if c.IsSet(sshTokenIDFlag) {
headers.Add(h2mux.CFAccessClientIDHeader, c.String(sshTokenIDFlag))
@@ -435,7 +477,7 @@ func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context, log *zerolog.Logger) err
if c.IsSet(sshTokenSecretFlag) {
headers.Add(h2mux.CFAccessClientSecretHeader, c.String(sshTokenSecretFlag))
}
options := &carrier.StartOptions{OriginURL: appUrl.String(), Headers: headers}
options := &carrier.StartOptions{AppInfo: appInfo, OriginURL: appUrl.String(), Headers: headers}
if valid, err := isTokenValid(options, log); err != nil {
return err
@@ -443,7 +485,7 @@ func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context, log *zerolog.Logger) err
return nil
}
if err := token.RemoveTokenIfExists(appUrl); err != nil {
if err := token.RemoveTokenIfExists(appInfo); err != nil {
return err
}
+3 -3
View File
@@ -1,10 +1,10 @@
package main
import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
"github.com/cloudflare/cloudflared/config"
)
// ForwardServiceType is used to identify what kind of overwatch service this is
+8 -6
View File
@@ -1,18 +1,19 @@
package main
import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/tunneldns"
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/tunneldns"
)
const (
// ResolverServiceType is used to identify what kind of overwatch service this is
ResolverServiceType = "resolver"
LogFieldResolverAddress = "resolverAddress"
LogFieldResolverPort = "resolverPort"
LogFieldResolverAddress = "resolverAddress"
LogFieldResolverPort = "resolverPort"
LogFieldResolverMaxUpstreamConns = "resolverMaxUpstreamConns"
)
// ResolverService is used to wrap the tunneldns package's DNS over HTTP
@@ -57,7 +58,7 @@ func (s *ResolverService) Shutdown() {
func (s *ResolverService) Run() error {
// create a listener
l, err := tunneldns.CreateListener(s.resolver.AddressOrDefault(), s.resolver.PortOrDefault(),
s.resolver.UpstreamsOrDefault(), s.resolver.BootstrapsOrDefault(), s.log)
s.resolver.UpstreamsOrDefault(), s.resolver.BootstrapsOrDefault(), s.resolver.MaxUpstreamConnectionsOrDefault(), s.log)
if err != nil {
return err
}
@@ -74,6 +75,7 @@ func (s *ResolverService) Run() error {
resolverLog := s.log.With().
Str(LogFieldResolverAddress, s.resolver.AddressOrDefault()).
Uint16(LogFieldResolverPort, s.resolver.PortOrDefault()).
Int(LogFieldResolverMaxUpstreamConns, s.resolver.MaxUpstreamConnectionsOrDefault()).
Logger()
resolverLog.Info().Msg("Starting resolver")
+3 -3
View File
@@ -1,10 +1,10 @@
package main
import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/overwatch"
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/overwatch"
)
// AppService is the main service that runs when no command lines flags are passed to cloudflared
+7 -1
View File
@@ -1,8 +1,10 @@
package buildinfo
import (
"github.com/rs/zerolog"
"fmt"
"runtime"
"github.com/rs/zerolog"
)
type BuildInfo struct {
@@ -25,3 +27,7 @@ func (bi *BuildInfo) Log(log *zerolog.Logger) {
log.Info().Msgf("Version %s", bi.CloudflaredVersion)
log.Info().Msgf("GOOS: %s, GOVersion: %s, GoArch: %s", bi.GoOS, bi.GoVersion, bi.GoArch)
}
func (bi *BuildInfo) OSArch() string {
return fmt.Sprintf("%s_%s", bi.GoOS, bi.GoArch)
}
+21
View File
@@ -0,0 +1,21 @@
package cliutil
import (
"fmt"
"github.com/urfave/cli/v2"
)
func RemovedCommand(name string) *cli.Command {
return &cli.Command{
Name: name,
Action: func(context *cli.Context) error {
return cli.Exit(
fmt.Sprintf("%s command is no longer supported by cloudflared. Consult Argo Tunnel documentation for possible alternative solutions.", name),
-1,
)
},
Description: fmt.Sprintf("%s is deprecated", name),
Hidden: true,
}
}
+2 -1
View File
@@ -2,6 +2,7 @@ package cliutil
import (
"fmt"
"github.com/urfave/cli/v2"
)
@@ -21,7 +22,7 @@ func UsageError(format string, args ...interface{}) error {
}
// Ensures exit with error code if actionFunc returns an error
func ErrorHandler(actionFunc cli.ActionFunc) cli.ActionFunc {
func WithErrorHandler(actionFunc cli.ActionFunc) cli.ActionFunc {
return func(ctx *cli.Context) error {
err := actionFunc(ctx)
if err != nil {
+39
View File
@@ -0,0 +1,39 @@
package cliutil
import (
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
)
func Action(actionFunc cli.ActionFunc) cli.ActionFunc {
return WithErrorHandler(actionFunc)
}
func ConfiguredAction(actionFunc cli.ActionFunc) cli.ActionFunc {
return WithErrorHandler(func(c *cli.Context) error {
if err := setFlagsFromConfigFile(c); err != nil {
return err
}
return actionFunc(c)
})
}
func setFlagsFromConfigFile(c *cli.Context) error {
const errorExitCode = 1
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
inputSource, err := config.ReadConfigFile(c, log)
if err != nil {
if err == config.ErrNoConfigFile {
return nil
}
return cli.Exit(err, errorExitCode)
}
if err := altsrc.ApplyInputSource(c, inputSource); err != nil {
return cli.Exit(err, errorExitCode)
}
return nil
}
+1 -1
View File
@@ -8,6 +8,6 @@ import (
cli "github.com/urfave/cli/v2"
)
func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
func runApp(app *cli.App, graceShutdownC chan struct{}) {
app.Run(os.Args)
}
+7 -7
View File
@@ -7,13 +7,13 @@ import (
"os"
"path/filepath"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
"github.com/cloudflare/cloudflared/logger"
"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"
)
func runApp(app *cli.App, graceShutdownC chan struct{}) {
@@ -24,7 +24,7 @@ func runApp(app *cli.App, graceShutdownC chan struct{}) {
{
Name: "install",
Usage: "Install Argo Tunnel as a system service",
Action: cliutil.ErrorHandler(installLinuxService),
Action: cliutil.ConfiguredAction(installLinuxService),
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "legacy",
@@ -35,7 +35,7 @@ func runApp(app *cli.App, graceShutdownC chan struct{}) {
{
Name: "uninstall",
Usage: "Uninstall the Argo Tunnel service",
Action: cliutil.ErrorHandler(uninstallLinuxService),
Action: cliutil.ConfiguredAction(uninstallLinuxService),
},
},
})
+5 -5
View File
@@ -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 (
@@ -25,12 +25,12 @@ func runApp(app *cli.App, graceShutdownC chan struct{}) {
{
Name: "install",
Usage: "Install Argo Tunnel as an user launch agent",
Action: cliutil.ErrorHandler(installLaunchd),
Action: cliutil.ConfiguredAction(installLaunchd),
},
{
Name: "uninstall",
Usage: "Uninstall the Argo Tunnel launch agent",
Action: cliutil.ErrorHandler(uninstallLaunchd),
Action: cliutil.ConfiguredAction(uninstallLaunchd),
},
},
})
+13 -12
View File
@@ -2,24 +2,25 @@ package main
import (
"fmt"
"math/rand"
"strings"
"time"
"github.com/getsentry/raven-go"
homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/cmd/cloudflared/proxydns"
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/metrics"
"github.com/cloudflare/cloudflared/overwatch"
"github.com/cloudflare/cloudflared/tunneldns"
"github.com/cloudflare/cloudflared/watcher"
"github.com/getsentry/raven-go"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
)
const (
@@ -43,6 +44,7 @@ var (
)
func main() {
rand.Seed(time.Now().UnixNano())
metrics.RegisterBuildInfo(BuildTime, Version)
raven.SetRelease(Version)
@@ -75,7 +77,6 @@ func main() {
See https://developers.cloudflare.com/argo-tunnel/ for more in-depth documentation.`
app.Flags = flags()
app.Action = action(graceShutdownC)
app.Before = tunnel.SetFlagsFromConfigFile
app.Commands = commands(cli.ShowVersion)
tunnel.Init(Version, graceShutdownC) // we need this to support the tunnel sub command...
@@ -88,7 +89,7 @@ func commands(version func(c *cli.Context)) []*cli.Command {
cmds := []*cli.Command{
{
Name: "update",
Action: cliutil.ErrorHandler(updater.Update),
Action: cliutil.ConfiguredAction(updater.Update),
Usage: "Update the agent if a new version exists",
Flags: []cli.Flag{
&cli.BoolFlag{
@@ -115,7 +116,7 @@ func commands(version func(c *cli.Context)) []*cli.Command {
If a new version exists, updates the agent binary and quits.
Otherwise, does nothing.
To determine if an update happened in a script, check for error code 64.`,
To determine if an update happened in a script, check for error code 11.`,
},
{
Name: "version",
@@ -128,7 +129,7 @@ To determine if an update happened in a script, check for error code 64.`,
},
}
cmds = append(cmds, tunnel.Commands()...)
cmds = append(cmds, tunneldns.Command(false))
cmds = append(cmds, proxydns.Command(false))
cmds = append(cmds, access.Commands()...)
return cmds
}
@@ -143,7 +144,7 @@ func isEmptyInvocation(c *cli.Context) bool {
}
func action(graceShutdownC chan struct{}) cli.ActionFunc {
return cliutil.ErrorHandler(func(c *cli.Context) (err error) {
return cliutil.ConfiguredAction(func(c *cli.Context) (err error) {
if isEmptyInvocation(c) {
return handleServiceMode(c, graceShutdownC)
}
+114
View File
@@ -0,0 +1,114 @@
package proxydns
import (
"net"
"os"
"os/signal"
"syscall"
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/metrics"
"github.com/cloudflare/cloudflared/tunneldns"
)
func Command(hidden bool) *cli.Command {
return &cli.Command{
Name: "proxy-dns",
Action: cliutil.ConfiguredAction(Run),
Usage: "Run a DNS over HTTPS proxy server.",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "metrics",
Value: "localhost:",
Usage: "Listen address for metrics reporting.",
EnvVars: []string{"TUNNEL_METRICS"},
},
&cli.StringFlag{
Name: "address",
Usage: "Listen address for the DNS over HTTPS proxy server.",
Value: "localhost",
EnvVars: []string{"TUNNEL_DNS_ADDRESS"},
},
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
&cli.IntFlag{
Name: "port",
Usage: "Listen on given port for the DNS over HTTPS proxy server.",
Value: 53,
EnvVars: []string{"TUNNEL_DNS_PORT"},
},
&cli.StringSliceFlag{
Name: "upstream",
Usage: "Upstream endpoint URL, you can specify multiple endpoints for redundancy.",
Value: cli.NewStringSlice("https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"),
EnvVars: []string{"TUNNEL_DNS_UPSTREAM"},
},
&cli.StringSliceFlag{
Name: "bootstrap",
Usage: "bootstrap endpoint URL, you can specify multiple endpoints for redundancy.",
Value: cli.NewStringSlice("https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"),
EnvVars: []string{"TUNNEL_DNS_BOOTSTRAP"},
},
&cli.IntFlag{
Name: "max-upstream-conns",
Usage: "Maximum concurrent connections to upstream. Setting to 0 means unlimited.",
Value: tunneldns.MaxUpstreamConnsDefault,
EnvVars: []string{"TUNNEL_DNS_MAX_UPSTREAM_CONNS"},
},
},
ArgsUsage: " ", // can't be the empty string or we get the default output
Hidden: hidden,
}
}
// Run implements a foreground runner
func Run(c *cli.Context) error {
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
metricsListener, err := net.Listen("tcp", c.String("metrics"))
if err != nil {
log.Fatal().Err(err).Msg("Failed to open the metrics listener")
}
go metrics.ServeMetrics(metricsListener, nil, nil, log)
listener, err := tunneldns.CreateListener(
c.String("address"),
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
uint16(c.Int("port")),
c.StringSlice("upstream"),
c.StringSlice("bootstrap"),
c.Int("max-upstream-conns"),
log,
)
if err != nil {
log.Err(err).Msg("Failed to create the listeners")
return err
}
// Try to start the server
readySignal := make(chan struct{})
err = listener.Start(readySignal)
if err != nil {
log.Err(err).Msg("Failed to start the listeners")
return listener.Stop()
}
<-readySignal
// Wait for signal
signals := make(chan os.Signal, 10)
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
defer signal.Stop(signals)
<-signals
// Shut down server
err = listener.Stop()
if err != nil {
log.Err(err).Msg("failed to stop")
}
return err
}
+2 -2
View File
@@ -10,9 +10,9 @@ import (
"os/exec"
"text/template"
"github.com/mitchellh/go-homedir"
homedir "github.com/mitchellh/go-homedir"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/config"
)
type ServiceTemplate struct {
-33
View File
@@ -1,33 +0,0 @@
package shell
import (
"io"
"os"
"os/exec"
)
// OpenBrowser opens the specified URL in the default browser of the user
func OpenBrowser(url string) error {
return getBrowserCmd(url).Start()
}
// Run will kick off a shell task and pipe the results to the respective std pipes
func Run(cmd string, args ...string) error {
c := exec.Command(cmd, args...)
stderr, err := c.StderrPipe()
if err != nil {
return err
}
go func() {
io.Copy(os.Stderr, stderr)
}()
stdout, err := c.StdoutPipe()
if err != nil {
return err
}
go func() {
io.Copy(os.Stdout, stdout)
}()
return c.Run()
}
+37 -106
View File
@@ -7,19 +7,27 @@ import (
"io/ioutil"
"net/url"
"os"
"reflect"
"runtime/trace"
"strings"
"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/config"
"github.com/cloudflare/cloudflared/cmd/cloudflared/proxydns"
"github.com/cloudflare/cloudflared/cmd/cloudflared/ui"
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/dbconnect"
"github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/metrics"
@@ -28,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 (
@@ -75,8 +74,8 @@ const (
// uiFlag is to enable launching cloudflared in interactive UI mode
uiFlag = "ui"
debugLevelWarning = "At debug level, request URL, method, protocol, content legnth and header will be logged. " +
"Response status, content length and header will also be logged in debug level."
debugLevelWarning = "At debug level cloudflared will log request URL, method, protocol, content length, as well as, all request and response headers. " +
"This can expose sensitive information in your logs."
LogFieldCommand = "command"
LogFieldExpandedPath = "expandedPath"
@@ -101,27 +100,27 @@ func Commands() []*cli.Command {
buildRouteCommand(),
buildRunCommand(),
buildListCommand(),
buildInfoCommand(),
buildIngressSubcommand(),
buildDeleteCommand(),
buildCleanupCommand(),
// for compatibility, allow following as tunnel subcommands
tunneldns.Command(true),
dbConnectCmd(),
proxydns.Command(true),
cliutil.RemovedCommand("db-connect"),
}
return []*cli.Command{
buildTunnelCommand(subcommands),
// for compatibility, allow following as top-level subcommands
buildLoginSubcommand(true),
dbConnectCmd(),
cliutil.RemovedCommand("db-connect"),
}
}
func buildTunnelCommand(subcommands []*cli.Command) *cli.Command {
return &cli.Command{
Name: "tunnel",
Action: cliutil.ErrorHandler(TunnelCommand),
Before: SetFlagsFromConfigFile,
Action: cliutil.ConfiguredAction(TunnelCommand),
Category: "Tunnel",
Usage: "Make a locally-running web service accessible over the internet using Argo Tunnel.",
ArgsUsage: " ",
@@ -287,16 +286,14 @@ func StartServer(
}
// update needs to be after DNS proxy is up to resolve equinox server address
if updater.IsAutoupdateEnabled(c, log) {
autoupdateFreq := c.Duration("autoupdate-freq")
log.Info().Dur("autoupdateFreq", autoupdateFreq).Msg("Autoupdate frequency is set")
wg.Add(1)
go func() {
defer wg.Done()
autoupdater := updater.NewAutoUpdater(c.Duration("autoupdate-freq"), &listeners, log)
errC <- autoupdater.Run(ctx)
}()
}
wg.Add(1)
go func() {
defer wg.Done()
autoupdater := updater.NewAutoUpdater(
c.Bool("no-autoupdate"), c.Duration("autoupdate-freq"), &listeners, log,
)
errC <- autoupdater.Run(ctx)
}()
// Serve DNS proxy stand-alone if no hostname or tag or app is going to run
if dnsProxyStandAlone(c) {
@@ -371,26 +368,6 @@ func StartServer(
return waitToShutdown(&wg, cancel, errC, graceShutdownC, c.Duration("grace-period"), log)
}
func SetFlagsFromConfigFile(c *cli.Context) error {
const exitCode = 1
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
inputSource, err := config.ReadConfigFile(c, log)
if err != nil {
if err == config.ErrNoConfigFile {
return nil
}
return cli.Exit(err, exitCode)
}
targetFlags := c.Command.Flags
if c.Command.Name == "" {
targetFlags = c.App.Flags
}
if err := altsrc.ApplyInputSourceValues(c, inputSource, targetFlags); err != nil {
return cli.Exit(err, exitCode)
}
return nil
}
func waitToShutdown(wg *sync.WaitGroup,
cancelServerContext func(),
errC <-chan error,
@@ -479,60 +456,6 @@ func addPortIfMissing(uri *url.URL, port int) string {
return fmt.Sprintf("%s:%d", uri.Hostname(), port)
}
func dbConnectCmd() *cli.Command {
cmd := dbconnect.Cmd()
// Append the tunnel commands so users can customize the daemon settings.
cmd.Flags = appendFlags(Flags(), cmd.Flags...)
// Override before to run tunnel validation before dbconnect validation.
cmd.Before = func(c *cli.Context) error {
err := SetFlagsFromConfigFile(c)
if err == nil {
err = dbconnect.CmdBefore(c)
}
return err
}
// Override action to setup the Proxy, then if successful, start the tunnel daemon.
cmd.Action = cliutil.ErrorHandler(func(c *cli.Context) error {
err := dbconnect.CmdAction(c)
if err == nil {
err = TunnelCommand(c)
}
return err
})
return cmd
}
// appendFlags will append extra flags to a slice of flags.
//
// The cli package will panic if two flags exist with the same name,
// so if extraFlags contains a flag that was already defined, modify the
// original flags to use the extra version.
func appendFlags(flags []cli.Flag, extraFlags ...cli.Flag) []cli.Flag {
for _, extra := range extraFlags {
var found bool
// Check if an extra flag overrides an existing flag.
for i, flag := range flags {
if reflect.DeepEqual(extra.Names(), flag.Names()) {
flags[i] = extra
found = true
break
}
}
// Append the extra flag if it has nothing to override.
if !found {
flags = append(flags, extra)
}
}
return flags
}
func tunnelFlags(shouldHide bool) []cli.Flag {
flags := configureCloudflaredFlags(shouldHide)
flags = append(flags, configureProxyFlags(shouldHide)...)
@@ -542,7 +465,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
credentialsFileFlag,
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "is-autoupdated",
Usage: "Signal the new process that Argo Tunnel client has been autoupdated",
Usage: "Signal the new process that Argo Tunnel connector has been autoupdated",
Value: false,
Hidden: true,
}),
@@ -681,6 +604,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
Aliases: []string{"n"},
EnvVars: []string{"TUNNEL_NAME"},
Usage: "Stable name to identify the tunnel. Using this flag will create, route and run a tunnel. For production usage, execute each command separately",
Hidden: shouldHide,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: uiFlag,
@@ -949,7 +873,7 @@ func configureLoggingFlags(shouldHide bool) []cli.Flag {
altsrc.NewStringFlag(&cli.StringFlag{
Name: logger.LogLevelFlag,
Value: "info",
Usage: "Application logging level {fatal, error, info, debug}. " + debugLevelWarning,
Usage: "Application logging level {debug, info, warn, error, fatal}. " + debugLevelWarning,
EnvVars: []string{"TUNNEL_LOGLEVEL"},
Hidden: shouldHide,
}),
@@ -957,7 +881,7 @@ func configureLoggingFlags(shouldHide bool) []cli.Flag {
Name: logger.LogTransportLevelFlag,
Aliases: []string{"proto-loglevel"}, // This flag used to be called proto-loglevel
Value: "info",
Usage: "Transport logging level(previously called protocol logging level) {fatal, error, info, debug}",
Usage: "Transport logging level(previously called protocol logging level) {debug, info, warn, error, fatal}",
EnvVars: []string{"TUNNEL_PROTO_LOGLEVEL", "TUNNEL_TRANSPORT_LOGLEVEL"},
Hidden: shouldHide,
}),
@@ -1011,6 +935,13 @@ func configureProxyDNSFlags(shouldHide bool) []cli.Flag {
EnvVars: []string{"TUNNEL_DNS_UPSTREAM"},
Hidden: shouldHide,
}),
altsrc.NewIntFlag(&cli.IntFlag{
Name: "proxy-dns-max-upstream-conns",
Usage: "Maximum concurrent connections to upstream. Setting to 0 means unlimited.",
Value: tunneldns.MaxUpstreamConnsDefault,
Hidden: shouldHide,
EnvVars: []string{"TUNNEL_DNS_MAX_UPSTREAM_CONNS"},
}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: "proxy-dns-bootstrap",
Usage: "bootstrap endpoint URL, you can specify multiple endpoints for redundancy.",
+13
View File
@@ -0,0 +1,13 @@
package tunnel
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestDedup(t *testing.T) {
expected := []string{"a", "b"}
actual := dedup([]string{"a", "b", "a"})
require.ElementsMatch(t, expected, actual)
}
+54 -17
View File
@@ -8,8 +8,15 @@ import (
"path/filepath"
"strings"
"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/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/edgediscovery"
"github.com/cloudflare/cloudflared/h2mux"
@@ -18,13 +25,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"
@@ -109,7 +109,7 @@ func findOriginCert(originCertPath string, log *zerolog.Logger) (string, error)
// Check that the user has acquired a certificate using the login command
ok, err := config.FileExists(originCertPath)
if err != nil {
log.Error().Msgf("Cannot check if origin cert exists at path %s", originCertPath)
log.Error().Err(err).Msgf("Cannot check if origin cert exists at path %s", originCertPath)
return "", fmt.Errorf("cannot check if origin cert exists at path %s", originCertPath)
}
if !ok {
@@ -160,6 +160,10 @@ func prepareTunnelConfig(
if err != nil {
log.Err(err).Str(LogFieldHostname, configHostname).Msg("Invalid hostname")
return nil, ingress.Ingress{}, errors.Wrap(err, "Invalid hostname")
} else if hostname != "" && isNamedTunnel {
log.Warn().Msg("The property `hostname` in your configuration is ignored because you configured a Named Tunnel " +
"in the property `tunnel`. Make sure to provision the routing (e.g. via `cloudflared tunnel route`) or else " +
"your origin will not be reachable. You should remove the `hostname` property to avoid this warning.")
}
isFreeTunnel := hostname == ""
clientID := c.String("id")
@@ -195,18 +199,21 @@ func prepareTunnelConfig(
ingressRules ingress.Ingress
classicTunnel *connection.ClassicTunnelConfig
)
cfg := config.GetConfiguration()
if isNamedTunnel {
clientUUID, err := uuid.NewRandom()
if err != nil {
return nil, ingress.Ingress{}, errors.Wrap(err, "can't generate clientUUID")
return nil, ingress.Ingress{}, errors.Wrap(err, "can't generate connector UUID")
}
log.Info().Msgf("Generated Connector ID: %s", clientUUID)
features := append(c.StringSlice("features"), origin.FeatureSerializedHeaders)
namedTunnel.Client = tunnelpogs.ClientInfo{
ClientID: clientUUID[:],
Features: []string{origin.FeatureSerializedHeaders},
Features: dedup(features),
Version: version,
Arch: fmt.Sprintf("%s_%s", buildInfo.GoOS, buildInfo.GoArch),
Arch: buildInfo.OSArch(),
}
ingressRules, err = ingress.ParseIngress(config.GetConfiguration())
ingressRules, err = ingress.ParseIngress(cfg)
if err != nil && err != ingress.ErrNoIngressRules {
return nil, ingress.Ingress{}, err
}
@@ -230,7 +237,14 @@ func prepareTunnelConfig(
}
}
protocolSelector, err := connection.NewProtocolSelector(c.String("protocol"), namedTunnel, edgediscovery.HTTP2Percentage, origin.ResolveTTL, log)
var warpRoutingService *ingress.WarpRoutingService
warpRoutingEnabled := isWarpRoutingEnabled(cfg.WarpRouting, isNamedTunnel)
if warpRoutingEnabled {
warpRoutingService = ingress.NewWarpRoutingService()
log.Info().Msgf("Warp-routing is enabled")
}
protocolSelector, err := connection.NewProtocolSelector(c.String("protocol"), warpRoutingEnabled, namedTunnel, edgediscovery.HTTP2Percentage, origin.ResolveTTL, log)
if err != nil {
return nil, ingress.Ingress{}, err
}
@@ -245,9 +259,9 @@ func prepareTunnelConfig(
edgeTLSConfigs[p] = edgeTLSConfig
}
originClient := origin.NewClient(ingressRules, tags, log)
originProxy := origin.NewOriginProxy(ingressRules, warpRoutingService, tags, log)
connectionConfig := &connection.Config{
OriginClient: originClient,
OriginProxy: originProxy,
GracePeriod: c.Duration("grace-period"),
ReplaceExisting: c.Bool("force"),
}
@@ -262,7 +276,7 @@ func prepareTunnelConfig(
return &origin.TunnelConfig{
ConnectionConfig: connectionConfig,
BuildInfo: buildInfo,
OSArch: buildInfo.OSArch(),
ClientID: clientID,
EdgeAddrs: c.StringSlice("edge"),
HAConnections: c.Int("ha-connections"),
@@ -286,6 +300,29 @@ func prepareTunnelConfig(
}, ingressRules, nil
}
func isWarpRoutingEnabled(warpConfig config.WarpRoutingConfig, isNamedTunnel bool) bool {
return warpConfig.Enabled && isNamedTunnel
}
func isRunningFromTerminal() bool {
return terminal.IsTerminal(int(os.Stdout.Fd()))
}
// Remove any duplicates from the slice
func dedup(slice []string) []string {
// Convert the slice into a set
set := make(map[string]bool, 0)
for _, str := range slice {
set[str] = true
}
// Convert the set back into a slice
keys := make([]string, len(set))
i := 0
for str := range set {
keys[i] = str
i++
}
return keys
}
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"fmt"
"path/filepath"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/config"
"github.com/google/uuid"
"github.com/rs/zerolog"
+16
View File
@@ -0,0 +1,16 @@
package tunnel
import (
"time"
"github.com/google/uuid"
"github.com/cloudflare/cloudflared/tunnelstore"
)
type Info struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"createdAt"`
Connectors []*tunnelstore.ActiveClient `json:"conns"`
}
@@ -5,7 +5,7 @@ import (
"net/url"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/ingress"
"github.com/pkg/errors"
@@ -45,7 +45,7 @@ func buildIngressSubcommand() *cli.Command {
func buildValidateIngressCommand() *cli.Command {
return &cli.Command{
Name: "validate",
Action: cliutil.ErrorHandler(validateIngressCommand),
Action: cliutil.ConfiguredAction(validateIngressCommand),
Usage: "Validate the ingress configuration ",
UsageText: "cloudflared tunnel [--config FILEPATH] ingress validate",
Description: "Validates the configuration file, ensuring your ingress rules are OK.",
@@ -55,7 +55,7 @@ func buildValidateIngressCommand() *cli.Command {
func buildTestURLCommand() *cli.Command {
return &cli.Command{
Name: "rule",
Action: cliutil.ErrorHandler(testURLCommand),
Action: cliutil.ConfiguredAction(testURLCommand),
Usage: "Check which ingress rule matches a given request URL",
UsageText: "cloudflared tunnel [--config FILEPATH] ingress rule URL",
ArgsUsage: "URL",
+6 -12
View File
@@ -8,14 +8,14 @@ import (
"path/filepath"
"syscall"
"github.com/mitchellh/go-homedir"
homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/cmd/cloudflared/transfer"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/token"
)
const (
@@ -26,16 +26,10 @@ const (
func buildLoginSubcommand(hidden bool) *cli.Command {
return &cli.Command{
Name: "login",
Action: cliutil.ErrorHandler(login),
Action: cliutil.ConfiguredAction(login),
Usage: "Generate a configuration file with your login details",
ArgsUsage: " ",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "url",
Hidden: true,
},
},
Hidden: hidden,
Hidden: hidden,
}
}
@@ -56,7 +50,7 @@ func login(c *cli.Context) error {
return err
}
resourceData, err := transfer.Run(
resourceData, err := token.RunTransfer(
loginURL,
"cert",
"callback",
+7 -1
View File
@@ -1,6 +1,8 @@
package tunnel
import (
"fmt"
"github.com/cloudflare/cloudflared/tunneldns"
"github.com/pkg/errors"
@@ -13,7 +15,11 @@ func runDNSProxyServer(c *cli.Context, dnsReadySignal chan struct{}, shutdownC <
if port <= 0 || port > 65535 {
return errors.New("The 'proxy-dns-port' must be a valid port number in <1, 65535> range.")
}
listener, err := tunneldns.CreateListener(c.String("proxy-dns-address"), uint16(port), c.StringSlice("proxy-dns-upstream"), c.StringSlice("proxy-dns-bootstrap"), log)
maxUpstreamConnections := c.Int("proxy-dns-max-upstream-conns")
if maxUpstreamConnections < 0 {
return fmt.Errorf("'%s' must be 0 or higher", "proxy-dns-max-upstream-conns")
}
listener, err := tunneldns.CreateListener(c.String("proxy-dns-address"), uint16(port), c.StringSlice("proxy-dns-upstream"), c.StringSlice("proxy-dns-bootstrap"), maxUpstreamConnections, log)
if err != nil {
close(dnsReadySignal)
listener.Stop()
+1 -1
View File
@@ -20,4 +20,4 @@ func waitForSignal(graceShutdownC chan struct{}, logger *zerolog.Logger) {
close(graceShutdownC)
case <-graceShutdownC:
}
}
}
-1
View File
@@ -101,4 +101,3 @@ func TestWaitForShutdown(t *testing.T) {
assert.True(t, contextCancelled)
assert.True(t, time.Now().Sub(startTime) < time.Second) // check that wait ended early
}
+45 -16
View File
@@ -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
}
@@ -223,13 +237,8 @@ func (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error {
if !tunnel.DeletedAt.IsZero() {
return fmt.Errorf("Tunnel %s has already been deleted", tunnel.ID)
}
// Check if tunnel has existing connections and if force flag is set, cleanup connections
if len(tunnel.Connections) > 0 {
if !forceFlagSet {
return fmt.Errorf("You can not delete tunnel %s because it has active connections. To see connections run the 'list' command. If you believe the tunnel is not active, you can use a -f / --force flag with this command.", id)
}
if err := client.CleanupConnections(tunnel.ID); err != nil {
if forceFlagSet {
if err := client.CleanupConnections(tunnel.ID, tunnelstore.NewCleanupParams()); err != nil {
return errors.Wrapf(err, "Error cleaning up connections for tunnel %s", tunnel.ID)
}
}
@@ -281,13 +290,24 @@ func (sc *subcommandContext) run(tunnelID uuid.UUID) error {
}
func (sc *subcommandContext) cleanupConnections(tunnelIDs []uuid.UUID) error {
params := tunnelstore.NewCleanupParams()
extraLog := ""
if connector := sc.c.String("connector-id"); connector != "" {
connectorID, err := uuid.Parse(connector)
if err != nil {
return errors.Wrapf(err, "%s is not a valid client ID (must be a UUID)", connector)
}
params.ForClient(connectorID)
extraLog = fmt.Sprintf(" for connector-id %s", connectorID.String())
}
client, err := sc.client()
if err != nil {
return err
}
for _, tunnelID := range tunnelIDs {
sc.log.Info().Msgf("Cleanup connection for tunnel %s", tunnelID)
if err := client.CleanupConnections(tunnelID); err != nil {
sc.log.Info().Msgf("Cleanup connection for tunnel %s%s", tunnelID, extraLog)
if err := client.CleanupConnections(tunnelID, params); err != nil {
sc.log.Error().Msgf("Error cleaning up connections for tunnel %v, error :%v", tunnelID, err)
}
}
@@ -348,6 +368,12 @@ func (sc *subcommandContext) findID(input string) (uuid.UUID, error) {
// one Tunnelstore API call.
func (sc *subcommandContext) findIDs(inputs []string) ([]uuid.UUID, error) {
// Shortcut without Tunnelstore call if we find that all inputs are already UUIDs.
uuids, err := convertNamesToUuids(inputs, make(map[string]uuid.UUID))
if err == nil {
return uuids, nil
}
// First, look up all tunnels the user has
filter := tunnelstore.NewFilter()
filter.NoDeleted()
@@ -367,7 +393,10 @@ func findIDs(tunnels []*tunnelstore.Tunnel, inputs []string) ([]uuid.UUID, error
nameToID[tunnel.Name] = tunnel.ID
}
// For each input, try to find the tunnel ID.
return convertNamesToUuids(inputs, nameToID)
}
func convertNamesToUuids(inputs []string, nameToID map[string]uuid.UUID) ([]uuid.UUID, error) {
tunnelIDs := make([]uuid.UUID, len(inputs))
var badInputs []string
for i, input := range inputs {
@@ -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"
@@ -4,16 +4,17 @@ import (
"encoding/base64"
"flag"
"fmt"
"github.com/rs/zerolog"
"reflect"
"testing"
"time"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/tunnelstore"
"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"
)
func Test_findIDs(t *testing.T) {
@@ -260,7 +261,7 @@ func (d *deleteMockTunnelStore) DeleteTunnel(tunnelID uuid.UUID) error {
return nil
}
func (d *deleteMockTunnelStore) CleanupConnections(tunnelID uuid.UUID) error {
func (d *deleteMockTunnelStore) CleanupConnections(tunnelID uuid.UUID, _ *tunnelstore.CleanupParams) error {
tunnel, ok := d.mockTunnels[tunnelID]
if !ok {
return fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
+233 -54
View File
@@ -14,23 +14,25 @@ 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/config"
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/tunnelstore"
)
const (
allSortByOptions = "name, id, createdAt, deletedAt, numConnections"
CredFileFlagAlias = "cred-file"
CredFileFlag = "credentials-file"
allSortByOptions = "name, id, createdAt, deletedAt, numConnections"
connsSortByOptions = "id, startedAt, numConnections, version"
CredFileFlagAlias = "cred-file"
CredFileFlag = "credentials-file"
LogFieldTunnelID = "tunnelID"
)
@@ -63,11 +65,11 @@ var (
Aliases: []string{"rd"},
Usage: "Include connections that have recently disconnected in the list",
}
outputFormatFlag = altsrc.NewStringFlag(&cli.StringFlag{
outputFormatFlag = &cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "Render output using given `FORMAT`. Valid options are 'json' or 'yaml'",
})
}
sortByFlag = &cli.StringFlag{
Name: "sort-by",
Value: "name",
@@ -87,16 +89,23 @@ var (
"overwrite the previous tunnel. If you want to use a single hostname with multiple " +
"tunnels, you can do so with Cloudflare's Load Balancer product.",
})
credentialsFileFlag = altsrc.NewStringFlag(&cli.StringFlag{
featuresFlag = altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: "features",
Aliases: []string{"F"},
Usage: "Opt into various features that are still being developed or tested.",
})
credentialsFileFlagCLIOnly = &cli.StringFlag{
Name: CredFileFlag,
Aliases: []string{CredFileFlagAlias},
Usage: "Filepath at which to read/write the tunnel credentials",
EnvVars: []string{"TUNNEL_CRED_FILE"},
})
forceDeleteFlag = &cli.BoolFlag{
}
credentialsFileFlag = altsrc.NewStringFlag(credentialsFileFlagCLIOnly)
forceDeleteFlag = &cli.BoolFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "Allows you to delete a tunnel, even if it has active connections.",
Usage: "Cleans up any stale connections before the tunnel is deleted. cloudflared will not " +
"delete a tunnel with connections without this flag.",
EnvVars: []string{"TUNNEL_RUN_FORCE_OVERWRITE"},
}
selectProtocolFlag = altsrc.NewStringFlag(&cli.StringFlag{
@@ -107,12 +116,29 @@ var (
EnvVars: []string{"TUNNEL_TRANSPORT_PROTOCOL"},
Hidden: true,
})
sortInfoByFlag = &cli.StringFlag{
Name: "sort-by",
Value: "createdAt",
Usage: fmt.Sprintf("Sorts the list of connections of a tunnel by the given field. Valid options are {%s}", connsSortByOptions),
EnvVars: []string{"TUNNEL_INFO_SORT_BY"},
}
invertInfoSortFlag = &cli.BoolFlag{
Name: "invert-sort",
Usage: "Inverts the sort order of the tunnel info.",
EnvVars: []string{"TUNNEL_INFO_INVERT_SORT"},
}
cleanupClientFlag = &cli.StringFlag{
Name: "connector-id",
Aliases: []string{"c"},
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"},
}
)
func buildCreateCommand() *cli.Command {
return &cli.Command{
Name: "create",
Action: cliutil.ErrorHandler(createCommand),
Action: cliutil.ConfiguredAction(createCommand),
Usage: "Create a new tunnel with given name",
UsageText: "cloudflared tunnel [tunnel command options] create [subcommand options] NAME",
Description: `Creates a tunnel, registers it with Cloudflare edge and generates credential file used to run this tunnel.
@@ -121,7 +147,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(),
}
}
@@ -144,6 +170,9 @@ func createCommand(c *cli.Context) error {
}
name := c.Args().First()
warningChecker := updater.StartWarningCheck(c)
defer warningChecker.LogWarningIfAny(sc.log)
_, err = sc.create(name, c.String(CredFileFlag))
return errors.Wrap(err, "failed to create tunnel")
}
@@ -154,33 +183,26 @@ 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 {
return &cli.Command{
Name: "list",
Action: cliutil.ErrorHandler(listCommand),
Action: cliutil.ConfiguredAction(listCommand),
Usage: "List existing tunnels",
UsageText: "cloudflared tunnel [tunnel command options] list [subcommand options]",
Description: "cloudflared tunnel list will display all active tunnels, their created time and associated connections. Use -d flag to include deleted tunnels. See the list of options to filter the list",
@@ -204,6 +226,9 @@ func listCommand(c *cli.Context) error {
return err
}
warningChecker := updater.StartWarningCheck(c)
defer warningChecker.LogWarningIfAny(sc.log)
filter := tunnelstore.NewFilter()
if !c.Bool("show-deleted") {
filter.NoDeleted()
@@ -266,21 +291,16 @@ func listCommand(c *cli.Context) error {
} else {
fmt.Println("You have no tunnels, use 'cloudflared tunnel create' to define a new tunnel")
}
return nil
}
func formatAndPrintTunnelList(tunnels []*tunnelstore.Tunnel, showRecentlyDisconnected bool) {
const (
minWidth = 0
tabWidth = 8
padding = 1
padChar = ' '
flags = 0
)
writer := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, padChar, flags)
writer := tabWriter()
defer writer.Flush()
_, _ = fmt.Fprintln(writer, "You can obtain more detailed information for each tunnel with `cloudflared tunnel info <name/uuid>`")
// Print column headers with tabbed columns
_, _ = fmt.Fprintln(writer, "ID\tNAME\tCREATED\tCONNECTIONS\t")
@@ -322,14 +342,160 @@ func fmtConnections(connections []tunnelstore.Connection, showRecentlyDisconnect
return strings.Join(output, ", ")
}
func buildInfoCommand() *cli.Command {
return &cli.Command{
Name: "info",
Action: cliutil.ConfiguredAction(tunnelInfo),
Usage: "List details about the active connectors for a tunnel",
UsageText: "cloudflared tunnel [tunnel command options] info [subcommand options] [TUNNEL]",
Description: "cloudflared tunnel info displays details about the active connectors for a given tunnel (identified by name or uuid).",
Flags: []cli.Flag{
outputFormatFlag,
showRecentlyDisconnected,
sortInfoByFlag,
invertInfoSortFlag,
},
CustomHelpTemplate: commandHelpTemplate(),
}
}
func tunnelInfo(c *cli.Context) error {
sc, err := newSubcommandContext(c)
if err != nil {
return err
}
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.`)
}
tunnelID, err := sc.findID(c.Args().First())
if err != nil {
return errors.Wrap(err, "error parsing tunnel ID")
}
client, err := sc.client()
if err != nil {
return err
}
clients, err := client.ListActiveClients(tunnelID)
if err != nil {
return err
}
sortBy := c.String("sort-by")
invalidSortField := false
sort.Slice(clients, func(i, j int) bool {
cmp := func() bool {
switch sortBy {
case "id":
return clients[i].ID.String() < clients[j].ID.String()
case "createdAt":
return clients[i].RunAt.Unix() < clients[j].RunAt.Unix()
case "numConnections":
return len(clients[i].Connections) < len(clients[j].Connections)
case "version":
return clients[i].Version < clients[j].Version
default:
invalidSortField = true
return clients[i].RunAt.Unix() < clients[j].RunAt.Unix()
}
}()
if c.Bool("invert-sort") {
return !cmp
}
return cmp
})
if invalidSortField {
sc.log.Error().Msgf("%s is not a valid sort field. Valid sort fields are %s. Defaulting to 'name'.", sortBy, connsSortByOptions)
}
tunnel, err := getTunnel(sc, tunnelID)
if err != nil {
return err
}
info := Info{
tunnel.ID,
tunnel.Name,
tunnel.CreatedAt,
clients,
}
if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" {
return renderOutput(outputFormat, info)
}
if len(clients) > 0 {
formatAndPrintConnectionsList(info, c.Bool("show-recently-disconnected"))
} else {
fmt.Printf("Your tunnel %s does not have any active connection.\n", tunnelID)
}
return nil
}
func getTunnel(sc *subcommandContext, tunnelID uuid.UUID) (*tunnelstore.Tunnel, error) {
filter := tunnelstore.NewFilter()
filter.ByTunnelID(tunnelID)
tunnels, err := sc.list(filter)
if err != nil {
return nil, err
}
if len(tunnels) != 1 {
return nil, errors.Errorf("Expected to find a single tunnel with uuid %v but found %d tunnels.", tunnelID, len(tunnels))
}
return tunnels[0], nil
}
func formatAndPrintConnectionsList(tunnelInfo Info, showRecentlyDisconnected bool) {
writer := tabWriter()
defer writer.Flush()
_, _ = fmt.Fprintf(writer, "NAME: %s\nID: %s\nCREATED: %s\n\n", tunnelInfo.Name, tunnelInfo.ID, tunnelInfo.CreatedAt)
_, _ = 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()
}
formattedStr := fmt.Sprintf(
"%s\t%s\t%s\t%s\t%s\t%s\t",
c.ID,
c.RunAt.Format(time.RFC3339),
c.Arch,
c.Version,
originIp,
fmtConnections(c.Connections, showRecentlyDisconnected),
)
_, _ = fmt.Fprintln(writer, formattedStr)
}
}
func tabWriter() *tabwriter.Writer {
const (
minWidth = 0
tabWidth = 8
padding = 1
padChar = ' '
flags = 0
)
writer := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, padChar, flags)
return writer
}
func buildDeleteCommand() *cli.Command {
return &cli.Command{
Name: "delete",
Action: cliutil.ErrorHandler(deleteCommand),
Action: cliutil.ConfiguredAction(deleteCommand),
Usage: "Delete existing tunnel by UUID or name",
UsageText: "cloudflared tunnel [tunnel command options] delete [subcommand options] TUNNEL",
Description: "cloudflared tunnel delete will delete tunnels with the given tunnel UUIDs or names. A tunnel cannot be deleted if it has active connections. To delete the tunnel unconditionally, use -f flag.",
Flags: []cli.Flag{credentialsFileFlag, forceDeleteFlag},
Flags: []cli.Flag{credentialsFileFlagCLIOnly, forceDeleteFlag},
CustomHelpTemplate: commandHelpTemplate(),
}
}
@@ -344,6 +510,9 @@ func deleteCommand(c *cli.Context) error {
return cliutil.UsageError(`"cloudflared tunnel delete" requires at least 1 argument, the ID or name of the tunnel to delete.`)
}
warningChecker := updater.StartWarningCheck(c)
defer warningChecker.LogWarningIfAny(sc.log)
tunnelIDs, err := sc.findIDs(c.Args().Slice())
if err != nil {
return err
@@ -370,12 +539,12 @@ func buildRunCommand() *cli.Command {
forceFlag,
credentialsFileFlag,
selectProtocolFlag,
featuresFlag,
}
flags = append(flags, configureProxyFlags(false)...)
return &cli.Command{
Name: "run",
Action: cliutil.ErrorHandler(runCommand),
Before: SetFlagsFromConfigFile,
Action: cliutil.ConfiguredAction(runCommand),
Usage: "Proxy a local web server by running the given tunnel",
UsageText: "cloudflared tunnel [tunnel command options] run [subcommand options] [TUNNEL]",
Description: `Runs the tunnel identified by name or UUUD, creating highly available connections
@@ -427,10 +596,11 @@ func runNamedTunnel(sc *subcommandContext, tunnelRef string) error {
func buildCleanupCommand() *cli.Command {
return &cli.Command{
Name: "cleanup",
Action: cliutil.ErrorHandler(cleanupCommand),
Action: cliutil.ConfiguredAction(cleanupCommand),
Usage: "Cleanup tunnel connections",
UsageText: "cloudflared tunnel [tunnel command options] cleanup [subcommand options] TUNNEL",
Description: "Delete connections for tunnels with the given UUIDs or names.",
Flags: []cli.Flag{cleanupClientFlag},
CustomHelpTemplate: commandHelpTemplate(),
}
}
@@ -456,15 +626,24 @@ func cleanupCommand(c *cli.Context) error {
func buildRouteCommand() *cli.Command {
return &cli.Command{
Name: "route",
Action: cliutil.ErrorHandler(routeCommand),
Usage: "Define what hostname or load balancer can route to this tunnel",
UsageText: "cloudflared tunnel [tunnel command options] route [subcommand options] dns|lb TUNNEL HOSTNAME [LB-POOL]",
Description: `The route defines what hostname or load balancer will proxy requests to this tunnel.
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.
To route a hostname by creating a CNAME to tunnel's address:
cloudflared tunnel route dns <tunnel ID> <hostname>
To use this tunnel as a load balancer origin, creating pool and load balancer if necessary:
cloudflared tunnel route lb <tunnel ID> <load balancer name> <load balancer pool>`,
To route a hostname by creating a DNS CNAME record to a tunnel:
cloudflared tunnel route dns <tunnel ID or name> <hostname>
You can read more at: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/routing-to-tunnel/dns
To use this tunnel as a load balancer origin, creating pool and load balancer if necessary:
cloudflared tunnel route lb <tunnel ID or name> <hostname> <load balancer pool>
You can read more at: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/routing-to-tunnel/lb
For Cloudflare WARP traffic to be routed to your private network, reachable from this tunnel as origins, use:
cloudflared tunnel route ip <network CIDR> <tunnel ID or name>
Further information about managing Cloudflare WARP traffic to your tunnel is available at:
cloudflared tunnel route ip --help
`,
CustomHelpTemplate: commandHelpTemplate(),
Subcommands: []*cli.Command{
buildRouteIPSubcommand(),
+3 -2
View File
@@ -5,10 +5,11 @@ import (
"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) {
+39 -25
View File
@@ -6,54 +6,57 @@ import (
"os"
"text/tabwriter"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/teamnet"
"github.com/pkg/errors"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
"github.com/cloudflare/cloudflared/teamnet"
"github.com/urfave/cli/v2"
)
func buildRouteIPSubcommand() *cli.Command {
return &cli.Command{
Name: "ip",
Category: "Tunnel",
Usage: "Configure and query private routes",
Usage: "Configure and query Cloudflare WARP routing to services or private networks available through this tunnel.",
UsageText: "cloudflared tunnel [--config FILEPATH] route COMMAND [arguments...]",
Hidden: true,
Description: `cloudflared can provision private routes from your private IP space to origins
in your corporate network. Users enrolled in your Cloudflare for Teams organization can reach
those routes through the Cloudflare Warp client. You can also build rules to determine who
can reach certain routes.
`,
Description: `cloudflared can provision private routes from any IP space to origins in your corporate network.
Users enrolled in your Cloudflare for Teams organization can reach those routes through the
Cloudflare WARP client. You can also build rules to determine who can reach certain routes.`,
Subcommands: []*cli.Command{
{
Name: "add",
Action: cliutil.ErrorHandler(addRouteCommand),
Usage: "Add a new Teamnet route to the table",
Action: cliutil.ConfiguredAction(addRouteCommand),
Usage: "Add any new network to the routing table reachable via the tunnel",
UsageText: "cloudflared tunnel [--config FILEPATH] route ip add [CIDR] [TUNNEL] [COMMENT?]",
Description: `Adds a private route to a CIDR in your private IP space. Requests will
be sent through the Cloudflare Warp client running on a user's machine, proxied
through the specified tunnel, and reach an IP in the given CIDR.`,
Description: `Adds any network route space (represented as a CIDR) to your routing table.
That network space becomes reachable for requests egressing from a user's machine
as long as it is using Cloudflare WARP client and is enrolled in the same account
that is running the tunnel chosen here. Further, those requests will be proxied to
the specified tunnel, and reach an IP in the given CIDR, as long as that IP is
reachable from the tunnel.`,
},
{
Name: "show",
Action: cliutil.ErrorHandler(showRoutesCommand),
Aliases: []string{"list"},
Action: cliutil.ConfiguredAction(showRoutesCommand),
Usage: "Show the routing table",
UsageText: "cloudflared tunnel [--config FILEPATH] route ip show [flags]",
Description: `Shows your organization's private route table. You can use flags to filter the results.`,
Flags: teamnet.FilterFlags,
Description: `Shows your organization private routing table. You can use flags to filter the results.`,
Flags: showRoutesFlags(),
},
{
Name: "delete",
Action: cliutil.ErrorHandler(deleteRouteCommand),
Usage: "Delete a row from your organization's private routing table",
UsageText: "cloudflared tunnel [--config FILEPATH] route ip delete [CIDR]",
Description: `Deletes the row for a given CIDR from your routing table`,
Name: "delete",
Action: cliutil.ConfiguredAction(deleteRouteCommand),
Usage: "Delete a row from your organization's private routing table",
UsageText: "cloudflared tunnel [--config FILEPATH] route ip delete [CIDR]",
Description: `Deletes the row for a given CIDR from your routing table. That portion
of your network will no longer be reachable by the WARP clients.`,
},
{
Name: "get",
Action: cliutil.ErrorHandler(getRouteByIPCommand),
Usage: "Check which row of the routing table matches a given IP",
Action: cliutil.ConfiguredAction(getRouteByIPCommand),
Usage: "Check which row of the routing table matches a given IP.",
UsageText: "cloudflared tunnel [--config FILEPATH] route ip get [IP]",
Description: `Checks which row of the routing table will be used to proxy a given IP.
This helps check and validate your config.`,
@@ -62,6 +65,13 @@ func buildRouteIPSubcommand() *cli.Command {
}
}
func showRoutesFlags() []cli.Flag {
flags := make([]cli.Flag, 0)
flags = append(flags, teamnet.FilterFlags...)
flags = append(flags, outputFormatFlag)
return flags
}
func showRoutesCommand(c *cli.Context) error {
sc, err := newSubcommandContext(c)
if err != nil {
@@ -73,6 +83,9 @@ func showRoutesCommand(c *cli.Context) error {
return errors.Wrap(err, "invalid config for routing filters")
}
warningChecker := updater.StartWarningCheck(c)
defer warningChecker.LogWarningIfAny(sc.log)
routes, err := sc.listRoutes(filter)
if err != nil {
return err
@@ -87,6 +100,7 @@ func showRoutesCommand(c *cli.Context) error {
} else {
fmt.Println("You have no routes, use 'cloudflared tunnel route ip add' to add a route")
}
return nil
}
+3 -3
View File
@@ -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 {
+49
View File
@@ -0,0 +1,49 @@
package updater
import (
"github.com/rs/zerolog"
"github.com/urfave/cli/v2"
)
type VersionWarningChecker struct {
warningChan chan string
}
func StartWarningCheck(c *cli.Context) VersionWarningChecker {
checker := VersionWarningChecker{
warningChan: make(chan string),
}
go func() {
options := updateOptions{
updateDisabled: true,
isBeta: c.Bool("beta"),
isStaging: c.Bool("staging"),
isForced: false,
intendedVersion: "",
}
checkResult, err := CheckForUpdate(options)
if err == nil {
checker.warningChan <- checkResult.UserMessage()
}
close(checker.warningChan)
}()
return checker
}
func (checker VersionWarningChecker) getWarning() string {
select {
case message := <-checker.warningChan:
return message
default:
// No feedback on time, we don't wait for it, since this is best-effort.
return ""
}
}
func (checker VersionWarningChecker) LogWarningIfAny(log *zerolog.Logger) {
if warning := checker.getWarning(); warning != "" {
log.Warn().Msg(warning)
}
}
+8 -4
View File
@@ -1,14 +1,15 @@
package updater
// Version is the functions needed to perform an update
type Version interface {
// CheckResult is the behaviour resulting from checking in with the Update Service
type CheckResult interface {
Apply() error
String() string
Version() string
UserMessage() string
}
// Service is the functions to get check for new updates
type Service interface {
Check() (Version, error)
Check() (CheckResult, error)
}
const (
@@ -23,4 +24,7 @@ const (
// VersionKeyName is the url parameter key to send to the checkin API to specific what version to upgrade or downgrade to
VersionKeyName = "version"
// ClientVersionName is the url parameter key to send the version that this cloudflared is currently running with
ClientVersionName = "clientVersion"
)
+80 -68
View File
@@ -3,18 +3,18 @@ package updater
import (
"context"
"fmt"
"github.com/rs/zerolog"
"os"
"path/filepath"
"runtime"
"time"
"github.com/facebookgo/grace/gracenet"
"github.com/rs/zerolog"
"github.com/urfave/cli/v2"
"golang.org/x/crypto/ssh/terminal"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
"github.com/facebookgo/grace/gracenet"
)
const (
@@ -61,16 +61,18 @@ func (e *statusErr) ExitCode() int {
}
type updateOptions struct {
isBeta bool
isStaging bool
isForced bool
version string
updateDisabled bool
isBeta bool
isStaging bool
isForced bool
intendedVersion string
}
type UpdateOutcome struct {
Updated bool
Version string
Error error
Updated bool
Version string
UserMessage string
Error error
}
func (uo *UpdateOutcome) noUpdate() bool {
@@ -81,10 +83,10 @@ func Init(v string) {
version = v
}
func checkForUpdateAndApply(options updateOptions) UpdateOutcome {
func CheckForUpdate(options updateOptions) (CheckResult, error) {
cfdPath, err := os.Executable()
if err != nil {
return UpdateOutcome{Error: err}
return nil, err
}
url := UpdateURL
@@ -93,24 +95,22 @@ func checkForUpdateAndApply(options updateOptions) UpdateOutcome {
}
s := NewWorkersService(version, url, cfdPath, Options{IsBeta: options.isBeta,
IsForced: options.isForced, RequestedVersion: options.version})
IsForced: options.isForced, RequestedVersion: options.intendedVersion})
v, err := s.Check()
return s.Check()
}
func applyUpdate(options updateOptions, update CheckResult) UpdateOutcome {
if update.Version() == "" || options.updateDisabled {
return UpdateOutcome{UserMessage: update.UserMessage()}
}
err := update.Apply()
if err != nil {
return UpdateOutcome{Error: err}
}
//already on the latest version
if v == nil {
return UpdateOutcome{}
}
err = v.Apply()
if err != nil {
return UpdateOutcome{Error: err}
}
return UpdateOutcome{Updated: true, Version: v.String()}
return UpdateOutcome{Updated: true, Version: update.Version(), UserMessage: update.UserMessage()}
}
// Update is the handler for the update command from the command line
@@ -137,7 +137,13 @@ func Update(c *cli.Context) error {
log.Info().Msg("cloudflared is set to upgrade to the latest publish version regardless of the current version")
}
updateOutcome := loggedUpdate(log, updateOptions{isBeta: isBeta, isStaging: isStaging, isForced: isForced, version: c.String("version")})
updateOutcome := loggedUpdate(log, updateOptions{
updateDisabled: false,
isBeta: isBeta,
isStaging: isStaging,
isForced: isForced,
intendedVersion: c.String("version"),
})
if updateOutcome.Error != nil {
return &statusErr{updateOutcome.Error}
}
@@ -152,12 +158,18 @@ func Update(c *cli.Context) error {
// Checks for an update and applies it if one is available
func loggedUpdate(log *zerolog.Logger, options updateOptions) UpdateOutcome {
updateOutcome := checkForUpdateAndApply(options)
checkResult, err := CheckForUpdate(options)
if err != nil {
log.Err(err).Msg("update check failed")
return UpdateOutcome{Error: err}
}
updateOutcome := applyUpdate(options, checkResult)
if updateOutcome.Updated {
log.Info().Str(LogFieldVersion, updateOutcome.Version).Msg("cloudflared has been updated")
}
if updateOutcome.Error != nil {
log.Err(updateOutcome.Error).Msg("update check failed: %s")
log.Err(updateOutcome.Error).Msg("update failed to apply")
}
return updateOutcome
@@ -177,44 +189,53 @@ type configurable struct {
freq time.Duration
}
func NewAutoUpdater(freq time.Duration, listeners *gracenet.Net, log *zerolog.Logger) *AutoUpdater {
updaterConfigurable := &configurable{
enabled: true,
freq: freq,
}
if freq == 0 {
updaterConfigurable.enabled = false
updaterConfigurable.freq = DefaultCheckUpdateFreq
}
func NewAutoUpdater(updateDisabled bool, freq time.Duration, listeners *gracenet.Net, log *zerolog.Logger) *AutoUpdater {
return &AutoUpdater{
configurable: updaterConfigurable,
configurable: createUpdateConfig(updateDisabled, freq, log),
listeners: listeners,
updateConfigChan: make(chan *configurable),
log: log,
}
}
func createUpdateConfig(updateDisabled bool, freq time.Duration, log *zerolog.Logger) *configurable {
if isAutoupdateEnabled(log, updateDisabled, freq) {
log.Info().Dur("autoupdateFreq", freq).Msg("Autoupdate frequency is set")
return &configurable{
enabled: true,
freq: freq,
}
} else {
return &configurable{
enabled: false,
freq: DefaultCheckUpdateFreq,
}
}
}
func (a *AutoUpdater) Run(ctx context.Context) error {
ticker := time.NewTicker(a.configurable.freq)
for {
if a.configurable.enabled {
updateOutcome := loggedUpdate(a.log, updateOptions{})
if updateOutcome.Updated {
if IsSysV() {
// SysV doesn't have a mechanism to keep service alive, we have to restart the process
a.log.Info().Msg("Restarting service managed by SysV...")
pid, err := a.listeners.StartProcess()
if err != nil {
a.log.Err(err).Msg("Unable to restart server automatically")
return &statusErr{err: err}
}
// stop old process after autoupdate. Otherwise we create a new process
// after each update
a.log.Info().Msgf("PID of the new process is %d", pid)
updateOutcome := loggedUpdate(a.log, updateOptions{updateDisabled: !a.configurable.enabled})
if updateOutcome.Updated {
Init(updateOutcome.Version)
if IsSysV() {
// SysV doesn't have a mechanism to keep service alive, we have to restart the process
a.log.Info().Msg("Restarting service managed by SysV...")
pid, err := a.listeners.StartProcess()
if err != nil {
a.log.Err(err).Msg("Unable to restart server automatically")
return &statusErr{err: err}
}
return &statusSuccess{newVersion: updateOutcome.Version}
// stop old process after autoupdate. Otherwise we create a new process
// after each update
a.log.Info().Msgf("PID of the new process is %d", pid)
}
return &statusSuccess{newVersion: updateOutcome.Version}
} else if updateOutcome.UserMessage != "" {
a.log.Warn().Msg(updateOutcome.UserMessage)
}
select {
case <-ctx.Done():
return ctx.Err()
@@ -229,27 +250,18 @@ func (a *AutoUpdater) Run(ctx context.Context) error {
}
// Update is the method to pass new AutoUpdaterConfigurable to a running AutoUpdater. It is safe to be called concurrently
func (a *AutoUpdater) Update(newFreq time.Duration) {
newConfigurable := &configurable{
enabled: true,
freq: newFreq,
}
// A ero duration means autoupdate is disabled
if newFreq == 0 {
newConfigurable.enabled = false
newConfigurable.freq = DefaultCheckUpdateFreq
}
a.updateConfigChan <- newConfigurable
func (a *AutoUpdater) Update(updateDisabled bool, newFreq time.Duration) {
a.updateConfigChan <- createUpdateConfig(updateDisabled, newFreq, a.log)
}
func IsAutoupdateEnabled(c *cli.Context, log *zerolog.Logger) bool {
if !SupportAutoUpdate(log) {
func isAutoupdateEnabled(log *zerolog.Logger, updateDisabled bool, updateFreq time.Duration) bool {
if !supportAutoUpdate(log) {
return false
}
return !c.Bool("no-autoupdate") && c.Duration("autoupdate-freq") != 0
return !updateDisabled && updateFreq != 0
}
func SupportAutoUpdate(log *zerolog.Logger) bool {
func supportAutoUpdate(log *zerolog.Logger) bool {
if runtime.GOOS == "windows" {
log.Info().Msg(noUpdateOnWindowsMessage)
return false
+13 -1
View File
@@ -2,17 +2,19 @@ package updater
import (
"context"
"flag"
"testing"
"github.com/facebookgo/grace/gracenet"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2"
)
func TestDisabledAutoUpdater(t *testing.T) {
listeners := &gracenet.Net{}
log := zerolog.Nop()
autoupdater := NewAutoUpdater(0, listeners, &log)
autoupdater := NewAutoUpdater(false, 0, listeners, &log)
ctx, cancel := context.WithCancel(context.Background())
errC := make(chan error)
go func() {
@@ -26,3 +28,13 @@ func TestDisabledAutoUpdater(t *testing.T) {
// Make sure that autoupdater terminates after canceling the context
assert.Equal(t, context.Canceled, <-errC)
}
func TestCheckInWithUpdater(t *testing.T) {
flagSet := flag.NewFlagSet(t.Name(), flag.PanicOnError)
cliCtx := cli.NewContext(cli.NewApp(), flagSet, nil)
warningChecker := StartWarningCheck(cliCtx)
warning := warningChecker.getWarning()
// Assuming this runs either on a release or development version, then the Worker will never have anything to tell us.
assert.Empty(t, warning)
}
+8 -67
View File
@@ -5,8 +5,6 @@ import (
"errors"
"net/http"
"runtime"
"strconv"
"strings"
)
// Options are the update options supported by the
@@ -27,6 +25,8 @@ type VersionResponse struct {
Version string `json:"version"`
Checksum string `json:"checksum"`
IsCompressed bool `json:"compressed"`
UserMessage string `json:"userMessage"`
ShouldUpdate bool `json:"shouldUpdate"`
Error string `json:"error"`
}
@@ -50,7 +50,7 @@ func NewWorkersService(currentVersion, url, targetPath string, opts Options) Ser
}
// Check does a check in with the Workers API to get a new version update
func (s *WorkersService) Check() (Version, error) {
func (s *WorkersService) Check() (CheckResult, error) {
client := &http.Client{
Timeout: clientTimeout,
}
@@ -59,6 +59,7 @@ func (s *WorkersService) Check() (Version, error) {
q := req.URL.Query()
q.Add(OSKeyName, runtime.GOOS)
q.Add(ArchitectureKeyName, runtime.GOARCH)
q.Add(ClientVersionName, s.currentVersion)
if s.opts.IsBeta {
q.Add(BetaKeyName, "true")
@@ -84,70 +85,10 @@ func (s *WorkersService) Check() (Version, error) {
return nil, errors.New(v.Error)
}
if !s.opts.IsForced && !IsNewerVersion(s.currentVersion, v.Version) {
return nil, nil
versionToUpdate := ""
if v.ShouldUpdate {
versionToUpdate = v.Version
}
return NewWorkersVersion(v.URL, v.Version, v.Checksum, s.targetPath, v.IsCompressed), nil
}
// IsNewerVersion checks semantic versioning for the latest version
// cloudflared tagging is more of a date than a semantic version,
// but the same comparision logic still holds for major.minor.patch
// e.g. 2020.8.2 is newer than 2020.8.1.
func IsNewerVersion(current string, check string) bool {
if strings.Contains(strings.ToLower(current), "dev") {
return false // dev builds shouldn't update
}
cMajor, cMinor, cPatch, err := SemanticParts(current)
if err != nil {
return false
}
nMajor, nMinor, nPatch, err := SemanticParts(check)
if err != nil {
return false
}
if nMajor > cMajor {
return true
}
if nMajor == cMajor && nMinor > cMinor {
return true
}
if nMajor == cMajor && nMinor == cMinor && nPatch > cPatch {
return true
}
return false
}
// SemanticParts gets the major, minor, and patch version of a semantic version string
// e.g. 3.1.2 would return 3, 1, 2, nil
func SemanticParts(version string) (major int, minor int, patch int, err error) {
major = 0
minor = 0
patch = 0
parts := strings.Split(version, ".")
if len(parts) != 3 {
err = errors.New("invalid version")
return
}
major, err = strconv.Atoi(parts[0])
if err != nil {
return
}
minor, err = strconv.Atoi(parts[1])
if err != nil {
return
}
patch, err = strconv.Atoi(parts[2])
if err != nil {
return
}
return
return NewWorkersVersion(v.URL, versionToUpdate, v.Checksum, s.targetPath, v.UserMessage, v.IsCompressed), nil
}
+105 -27
View File
@@ -6,6 +6,7 @@ import (
"compress/gzip"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
@@ -13,6 +14,8 @@ import (
"net/http/httptest"
"os"
"runtime"
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/require"
@@ -32,15 +35,20 @@ func respondWithData(w http.ResponseWriter, b []byte, status int) {
w.Write(b)
}
const mostRecentVersion = "2021.2.5"
const mostRecentBetaVersion = "2021.3.0"
const knownBuggyVersion = "2020.12.0"
const expectedUserMsg = "This message is expected when running a known buggy version"
func updateHandler(w http.ResponseWriter, r *http.Request) {
version := "2020.08.05"
version := mostRecentVersion
host := fmt.Sprintf("http://%s", r.Host)
url := host + "/download"
query := r.URL.Query()
if query.Get(BetaKeyName) == "true" {
version = "2020.08.06"
version = mostRecentBetaVersion
url = host + "/beta"
}
@@ -59,7 +67,15 @@ func updateHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(h, version)
checksum := fmt.Sprintf("%x", h.Sum(nil))
v := VersionResponse{URL: url, Version: version, Checksum: checksum}
var userMessage = ""
if query.Get(ClientVersionName) == knownBuggyVersion {
userMessage = expectedUserMsg
}
shouldUpdate := requestedVersion != "" || IsNewerVersion(query.Get(ClientVersionName), version)
v := VersionResponse{
URL: url, Version: version, Checksum: checksum, UserMessage: userMessage, ShouldUpdate: shouldUpdate,
}
respondWithJSON(w, v, http.StatusOK)
}
@@ -71,7 +87,7 @@ func gzipUpdateHandler(w http.ResponseWriter, r *http.Request) {
checksum := fmt.Sprintf("%x", h.Sum(nil))
url := fmt.Sprintf("http://%s/gzip-download.tgz", r.Host)
v := VersionResponse{URL: url, Version: version, Checksum: checksum}
v := VersionResponse{URL: url, Version: version, Checksum: checksum, ShouldUpdate: true}
respondWithJSON(w, v, http.StatusOK)
}
@@ -96,7 +112,7 @@ func compressedDownloadHandler(w http.ResponseWriter, r *http.Request) {
}
func downloadHandler(w http.ResponseWriter, r *http.Request) {
version := "2020.08.05"
version := mostRecentVersion
requestedVersion := r.URL.Query().Get(VersionKeyName)
if requestedVersion != "" {
version = requestedVersion
@@ -105,13 +121,71 @@ func downloadHandler(w http.ResponseWriter, r *http.Request) {
}
func betaHandler(w http.ResponseWriter, r *http.Request) {
respondWithData(w, []byte("2020.08.06"), http.StatusOK)
respondWithData(w, []byte(mostRecentBetaVersion), http.StatusOK)
}
func failureHandler(w http.ResponseWriter, r *http.Request) {
respondWithJSON(w, VersionResponse{Error: "unsupported os and architecture"}, http.StatusBadRequest)
}
func IsNewerVersion(current string, check string) bool {
if current == "" || check == "" {
return false
}
if strings.Contains(strings.ToLower(current), "dev") {
return false // dev builds shouldn't update
}
cMajor, cMinor, cPatch, err := SemanticParts(current)
if err != nil {
return false
}
nMajor, nMinor, nPatch, err := SemanticParts(check)
if err != nil {
return false
}
if nMajor > cMajor {
return true
}
if nMajor == cMajor && nMinor > cMinor {
return true
}
if nMajor == cMajor && nMinor == cMinor && nPatch > cPatch {
return true
}
return false
}
func SemanticParts(version string) (major int, minor int, patch int, err error) {
major = 0
minor = 0
patch = 0
parts := strings.Split(version, ".")
if len(parts) != 3 {
err = errors.New("invalid version")
return
}
major, err = strconv.Atoi(parts[0])
if err != nil {
return
}
minor, err = strconv.Atoi(parts[1])
if err != nil {
return
}
patch, err = strconv.Atoi(parts[2])
if err != nil {
return
}
return
}
func createServer() *httptest.Server {
mux := http.NewServeMux()
mux.HandleFunc("/updater", updateHandler)
@@ -124,7 +198,7 @@ func createServer() *httptest.Server {
}
func createTestFile(t *testing.T, path string) {
f, err := os.Create("tmpfile")
f, err := os.Create(path)
require.NoError(t, err)
fmt.Fprint(f, "2020.08.04")
f.Close()
@@ -142,13 +216,13 @@ func TestUpdateService(t *testing.T) {
s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{})
v, err := s.Check()
require.NoError(t, err)
require.Equal(t, v.String(), "2020.08.05")
require.Equal(t, v.Version(), mostRecentVersion)
require.NoError(t, v.Apply())
dat, err := ioutil.ReadFile(testFilePath)
require.NoError(t, err)
require.Equal(t, string(dat), "2020.08.05")
require.Equal(t, string(dat), mostRecentVersion)
}
func TestBetaUpdateService(t *testing.T) {
@@ -162,13 +236,13 @@ func TestBetaUpdateService(t *testing.T) {
s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{IsBeta: true})
v, err := s.Check()
require.NoError(t, err)
require.Equal(t, v.String(), "2020.08.06")
require.Equal(t, v.Version(), mostRecentBetaVersion)
require.NoError(t, v.Apply())
dat, err := ioutil.ReadFile(testFilePath)
require.NoError(t, err)
require.Equal(t, string(dat), "2020.08.06")
require.Equal(t, string(dat), mostRecentBetaVersion)
}
func TestFailUpdateService(t *testing.T) {
@@ -193,10 +267,11 @@ func TestNoUpdateService(t *testing.T) {
createTestFile(t, testFilePath)
defer os.Remove(testFilePath)
s := NewWorkersService("2020.8.5", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{})
s := NewWorkersService(mostRecentVersion, fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{})
v, err := s.Check()
require.NoError(t, err)
require.Nil(t, v)
require.NotNil(t, v)
require.Empty(t, v.Version())
}
func TestForcedUpdateService(t *testing.T) {
@@ -210,13 +285,13 @@ func TestForcedUpdateService(t *testing.T) {
s := NewWorkersService("2020.8.5", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{IsForced: true})
v, err := s.Check()
require.NoError(t, err)
require.Equal(t, v.String(), "2020.08.05")
require.Equal(t, v.Version(), mostRecentVersion)
require.NoError(t, v.Apply())
dat, err := ioutil.ReadFile(testFilePath)
require.NoError(t, err)
require.Equal(t, string(dat), "2020.08.05")
require.Equal(t, string(dat), mostRecentVersion)
}
func TestUpdateSpecificVersionService(t *testing.T) {
@@ -231,7 +306,7 @@ func TestUpdateSpecificVersionService(t *testing.T) {
s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{RequestedVersion: reqVersion})
v, err := s.Check()
require.NoError(t, err)
require.Equal(t, reqVersion, v.String())
require.Equal(t, reqVersion, v.Version())
require.NoError(t, v.Apply())
dat, err := ioutil.ReadFile(testFilePath)
@@ -251,7 +326,7 @@ func TestCompressedUpdateService(t *testing.T) {
s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/compressed", ts.URL), testFilePath, Options{})
v, err := s.Check()
require.NoError(t, err)
require.Equal(t, "2020.09.02", v.String())
require.Equal(t, "2020.09.02", v.Version())
require.NoError(t, v.Apply())
dat, err := ioutil.ReadFile(testFilePath)
@@ -260,14 +335,17 @@ func TestCompressedUpdateService(t *testing.T) {
require.Equal(t, "2020.09.02", string(dat))
}
func TestVersionParsing(t *testing.T) {
require.False(t, IsNewerVersion("2020.8.2", "2020.8.2"))
require.True(t, IsNewerVersion("2020.8.2", "2020.8.3"))
require.True(t, IsNewerVersion("2020.8.2", "2021.1.2"))
require.True(t, IsNewerVersion("2020.8.2", "2020.9.1"))
require.True(t, IsNewerVersion("2020.8.2", "2020.12.45"))
require.False(t, IsNewerVersion("2020.8.2", "2020.6.3"))
require.False(t, IsNewerVersion("DEV", "2020.8.5"))
require.False(t, IsNewerVersion("2020.8.2", "asdlkfjasdf"))
require.True(t, IsNewerVersion("3.0.1", "4.2.1"))
func TestUpdateWhenRunningKnownBuggyVersion(t *testing.T) {
ts := createServer()
defer ts.Close()
testFilePath := "tmpfile"
createTestFile(t, testFilePath)
defer os.Remove(testFilePath)
s := NewWorkersService(knownBuggyVersion, fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{})
v, err := s.Check()
require.NoError(t, err)
require.Equal(t, v.Version(), mostRecentVersion)
require.Equal(t, v.UserMessage(), expectedUserMsg)
}
+19 -3
View File
@@ -54,6 +54,7 @@ type WorkersVersion struct {
version string
targetPath string
isCompressed bool
userMessage string
}
// NewWorkersVersion creates a new Version object. This is normally created by a WorkersService JSON checkin response
@@ -61,8 +62,17 @@ type WorkersVersion struct {
// version is the version of this update
// checksum is the expected checksum of the downloaded file
// target path is where the file should be replace. Normally this the running cloudflared's path
func NewWorkersVersion(url, version, checksum, targetPath string, isCompressed bool) Version {
return &WorkersVersion{downloadURL: url, version: version, checksum: checksum, targetPath: targetPath, isCompressed: isCompressed}
// userMessage is a possible message to convey back to the user after having checked in with the Updater Service
// isCompressed tells whether the asset to update cloudflared is compressed or not
func NewWorkersVersion(url, version, checksum, targetPath, userMessage string, isCompressed bool) CheckResult {
return &WorkersVersion{
downloadURL: url,
version: version,
checksum: checksum,
targetPath: targetPath,
isCompressed: isCompressed,
userMessage: userMessage,
}
}
// Apply does the actual verification and update logic.
@@ -114,10 +124,16 @@ func (v *WorkersVersion) Apply() error {
}
// String returns the version number of this update/release (e.g. 2020.08.05)
func (v *WorkersVersion) String() string {
func (v *WorkersVersion) Version() string {
return v.version
}
// String returns a possible message to convey back to user after having checked in with the Updater Service. E.g.
// it can warn about the need to update the version currently running.
func (v *WorkersVersion) UserMessage() string {
return v.userMessage
}
// download the file from the link in the json
func download(url, filepath string, isCompressed bool) error {
client := &http.Client{
+17 -24
View File
@@ -12,13 +12,15 @@ import (
"time"
"unsafe"
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
"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 (
@@ -48,13 +50,13 @@ func runApp(app *cli.App, graceShutdownC chan struct{}) {
{
Name: "install",
Usage: "Install Argo Tunnel as a Windows service",
Action: installWindowsService,
},
Action: cliutil.ConfiguredAction(installWindowsService),
},
{
Name: "uninstall",
Usage: "Uninstall the Argo Tunnel service",
Action: uninstallWindowsService,
},
Action: cliutil.ConfiguredAction(uninstallWindowsService),
},
},
})
@@ -177,35 +179,30 @@ func installWindowsService(c *cli.Context) error {
zeroLogger.Info().Msg("Installing Argo Tunnel Windows service")
exepath, err := os.Executable()
if err != nil {
zeroLogger.Err(err).Msg("Cannot find path name that start the process")
return err
return errors.Wrap(err, "Cannot find path name that start the process")
}
m, err := mgr.Connect()
if err != nil {
zeroLogger.Err(err).Msg("Cannot establish a connection to the service control manager")
return err
return errors.Wrap(err, "Cannot establish a connection to the service control manager")
}
defer m.Disconnect()
s, err := m.OpenService(windowsServiceName)
log := zeroLogger.With().Str(LogFieldWindowsServiceName, windowsServiceName).Logger()
if err == nil {
s.Close()
log.Err(err).Msg("service already exists")
return fmt.Errorf("service %s already exists", windowsServiceName)
return fmt.Errorf("Service %s already exists", windowsServiceName)
}
config := mgr.Config{StartType: mgr.StartAutomatic, DisplayName: windowsServiceDescription}
s, err = m.CreateService(windowsServiceName, exepath, config)
if err != nil {
log.Error().Msg("Cannot install service")
return err
return errors.Wrap(err, "Cannot install service")
}
defer s.Close()
log.Info().Msg("Argo Tunnel agent service is installed")
err = eventlog.InstallAsEventCreate(windowsServiceName, eventlog.Error|eventlog.Warning|eventlog.Info)
if err != nil {
s.Delete()
log.Err(err).Msg("Cannot install event logger")
return fmt.Errorf("SetupEventLogSource() failed: %s", err)
return errors.Wrap(err, "Cannot install event logger")
}
err = configRecoveryOption(s.Handle)
if err != nil {
@@ -223,26 +220,22 @@ func uninstallWindowsService(c *cli.Context) error {
log.Info().Msg("Uninstalling Argo Tunnel Windows Service")
m, err := mgr.Connect()
if err != nil {
log.Error().Msg("Cannot establish a connection to the service control manager")
return err
return errors.Wrap(err, "Cannot establish a connection to the service control manager")
}
defer m.Disconnect()
s, err := m.OpenService(windowsServiceName)
if err != nil {
log.Error().Msg("service is not installed")
return fmt.Errorf("service %s is not installed", windowsServiceName)
return fmt.Errorf("Service %s is not installed", windowsServiceName)
}
defer s.Close()
err = s.Delete()
if err != nil {
log.Error().Msg("Cannot delete service")
return err
return errors.Wrap(err, "Cannot delete service")
}
log.Info().Msg("Argo Tunnel agent service is uninstalled")
err = eventlog.Remove(windowsServiceName)
if err != nil {
log.Error().Msg("Cannot remove event logger")
return fmt.Errorf("RemoveEventLogSource() failed: %s", err)
return errors.Wrap(err, "Cannot remove event logger")
}
return nil
}
+2
View File
@@ -0,0 +1,2 @@
__pycache__
.pytest_cache
+52
View File
@@ -0,0 +1,52 @@
# Requirements
1. Python 3.7 or later with packages in the given `requirements.txt`
- E.g. with conda:
- `conda create -n component-tests python=3.7`
- `conda activate component-tests`
- `pip3 install -r requirements.txt`
2. Create a config yaml file, for example:
```
cloudflared_binary: "cloudflared"
tunnel: "3d539f97-cd3a-4d8e-c33b-65e9099c7a8d"
credentials_file: "/Users/tunnel/.cloudflared/3d539f97-cd3a-4d8e-c33b-65e9099c7a8d.json"
classic_hostname: "classic-tunnel-component-tests.example.com"
origincert: "/Users/tunnel/.cloudflared/cert.pem"
ingress:
- hostname: named-tunnel-component-tests.example.com
service: http_status:200
- service: http_status:404
```
3. Route hostname to the tunnel. For the example config above, we can do that via
```
cloudflared tunnel route dns 3d539f97-cd3a-4d8e-c33b-65e9099c7a8d named-tunnel-component-tests.example.com
```
4. Turn on linter
If you are using Visual Studio, follow https://code.visualstudio.com/docs/python/linting to turn on linter.
5. Turn on formatter
If you are using Visual Studio, follow https://code.visualstudio.com/docs/python/editing#_formatting
to turn on formatter and https://marketplace.visualstudio.com/items?itemName=cbrevik.toggle-format-on-save
to turn on format on save.
6. If you have cloudflared running as a service on your machine, you can either stop the service or ignore the service tests
via `--ignore test_service.py`
# How to run
Specify path to config file via env var `COMPONENT_TESTS_CONFIG`. This is required.
## All tests
Run `pytest` inside this(component-tests) folder
## Specific files
Run `pytest <file 1 name>.py <file 2 name>.py`
## Specific tests
Run `pytest file.py -k <test 1 name> -k <test 2 name>`
## Live Logging
Running with `-o log_cli=true` outputs logging to CLI as the tests are. By default the log level is WARN.
`--log-cli-level` control logging level.
For example, to log at info level, run `pytest -o log_cli=true --log-cli-level=INFO`.
See https://docs.pytest.org/en/latest/logging.html#live-logs for more documentation on logging.
+132
View File
@@ -0,0 +1,132 @@
#!/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
# frozen=True raises exception when assigning to fields. This emulates immutability
@dataclass(frozen=True)
class BaseConfig:
cloudflared_binary: str
no_autoupdate: bool = True
metrics: str = f'localhost:{METRICS_PORT}'
def merge_config(self, additional):
config = copy.copy(additional)
config['no-autoupdate'] = self.no_autoupdate
config['metrics'] = self.metrics
return config
@dataclass(frozen=True)
class NamedTunnelBaseConfig(BaseConfig):
# The attributes of the parent class are ordered before attributes in this class,
# so we have to use default values here and check if they are set in __post_init__
tunnel: str = None
credentials_file: str = None
ingress: list = None
def __post_init__(self):
if self.tunnel is None:
raise TypeError("Field tunnel is not set")
if self.credentials_file is None:
raise TypeError("Field credentials_file is not set")
if self.ingress is None:
raise TypeError("Field ingress is not set")
def merge_config(self, additional):
config = super(NamedTunnelBaseConfig, self).merge_config(additional)
config['tunnel'] = self.tunnel
config['credentials-file'] = self.credentials_file
# In some cases we want to override default ingress, such as in config tests
if 'ingress' not in config:
config['ingress'] = self.ingress
return config
@dataclass(frozen=True)
class NamedTunnelConfig(NamedTunnelBaseConfig):
full_config: dict = None
additional_config: InitVar[dict] = {}
def __post_init__(self, additional_config):
# Cannot call set self.full_config because the class is frozen, instead, we can use __setattr__
# https://docs.python.org/3/library/dataclasses.html#frozen-instances
object.__setattr__(self, 'full_config',
self.merge_config(additional_config))
def get_url(self):
return "https://" + self.ingress[0]['hostname']
@dataclass(frozen=True)
class ClassicTunnelBaseConfig(BaseConfig):
hostname: str = None
origincert: str = None
def __post_init__(self):
if self.hostname is None:
raise TypeError("Field tunnel is not set")
if self.origincert is None:
raise TypeError("Field credentials_file is not set")
def merge_config(self, additional):
config = super(ClassicTunnelBaseConfig, self).merge_config(additional)
config['hostname'] = self.hostname
config['origincert'] = self.origincert
return config
@dataclass(frozen=True)
class ClassicTunnelConfig(ClassicTunnelBaseConfig):
full_config: dict = None
additional_config: InitVar[dict] = {}
def __post_init__(self, additional_config):
# Cannot call set self.full_config because the class is frozen, instead, we can use __setattr__
# https://docs.python.org/3/library/dataclasses.html#frozen-instances
object.__setattr__(self, 'full_config',
self.merge_config(additional_config))
def get_url(self):
return "https://" + self.hostname
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)
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()
+35
View File
@@ -0,0 +1,35 @@
import os
import pytest
import yaml
from time import sleep
from config import NamedTunnelConfig, ClassicTunnelConfig
from constants import BACKOFF_SECS
from util import LOGGER
@pytest.fixture(scope="session")
def component_tests_config():
config_file = os.getenv("COMPONENT_TESTS_CONFIG")
if config_file is None:
raise Exception(
"Need to provide path to config file in COMPONENT_TESTS_CONFIG")
with open(config_file, 'r') as stream:
config = yaml.safe_load(stream)
LOGGER.info(f"component tests base config {config}")
def _component_tests_config(additional_config={}, named_tunnel=True):
if named_tunnel:
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'])
return _component_tests_config
# This fixture is automatically called before each tests to make sure the previous cloudflared has been shutdown
@pytest.fixture(autouse=True)
def wait_previous_cloudflared():
sleep(BACKOFF_SECS)
+3
View File
@@ -0,0 +1,3 @@
METRICS_PORT = 51000
MAX_RETRIES = 5
BACKOFF_SECS = 7
+5
View File
@@ -0,0 +1,5 @@
flaky==3.7.0
pytest==6.2.2
pyyaml==5.4.1
requests==2.25.1
retrying==1.3.3
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env python
from util import start_cloudflared
class TestConfig:
# tmp_path is a fixture provides a temporary directory unique to the test invocation
def test_validate_ingress_rules(self, tmp_path, component_tests_config):
extra_config = {
'ingress': [
{
"hostname": "example.com",
"service": "https://localhost:8000",
"originRequest": {
"originServerName": "test.example.com",
"caPool": "/etc/certs/ca.pem"
},
},
{
"hostname": "api.example.com",
"path": "login",
"service": "https://localhost:9000",
},
{
"hostname": "wss.example.com",
"service": "wss://localhost:8000",
},
{
"hostname": "ssh.example.com",
"service": "ssh://localhost:8000",
},
{"service": "http_status:404"}
],
}
config = component_tests_config(extra_config)
validate_args = ["ingress", "validate"]
_ = start_cloudflared(tmp_path, config, validate_args)
self.match_rule(tmp_path, config,
"http://example.com/index.html", 1)
self.match_rule(tmp_path, config,
"https://example.com/index.html", 1)
self.match_rule(tmp_path, config,
"https://api.example.com/login", 2)
self.match_rule(tmp_path, config,
"https://wss.example.com", 3)
self.match_rule(tmp_path, config,
"https://ssh.example.com", 4)
self.match_rule(tmp_path, config,
"https://api.example.com", 5)
# This is used to check that the command tunnel ingress url <url> matches rule number <rule_num>. Note that rule number uses 1-based indexing
def match_rule(self, tmp_path, config, url, rule_num):
args = ["ingress", "rule", url]
match_rule = start_cloudflared(tmp_path, config, args)
assert f"Matched rule #{rule_num}" .encode() in match_rule.stdout
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env python
import json
import os
import subprocess
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 tunnel"
def assert_log_to_terminal(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 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(200)
# cloudflared logs the following when it first starts
# {"level":"info","tunnelID":"<tunnel ID>","time":"2021-03-10T12:21:13Z","message":"Starting tunnel"}
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:
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())
assert_log_to_terminal(cloudflared)
def test_logging_to_file(self, tmp_path, component_tests_config):
log_file = tmp_path / default_log_file
extra_config = {
# Convert from pathlib.Path to str
"logfile": str(log_file),
}
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())
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"
extra_config = {
"loglevel": "debug",
# Convert from pathlib.Path to str
"log-directory": str(log_dir),
}
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())
assert_log_to_dir(config, log_dir)
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env python
import copy
from time import sleep
from flaky import flaky
from util import start_cloudflared, wait_tunnel_ready, check_tunnel_not_connected
@flaky(max_runs=3, min_passes=1)
class TestReconnect():
default_ha_conns = 4
default_reconnect_secs = 5
extra_config = {
"stdin-control": True,
}
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, 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, capture_output=False) as cloudflared:
self.assert_reconnect(config, cloudflared, 1)
def send_reconnect(self, cloudflared, secs):
# Although it is recommended to use the Popen.communicate method, we cannot
# use it because it blocks on reading stdout and stderr until EOF is reached
cloudflared.stdin.write(f"reconnect {secs}s\n".encode())
cloudflared.stdin.flush()
def assert_reconnect(self, config, cloudflared, repeat):
wait_tunnel_ready(tunnel_url=config.get_url())
for _ in range(repeat):
for i in range(self.default_ha_conns):
self.send_reconnect(cloudflared, self.default_reconnect_secs)
expect_connections = self.default_ha_conns-i-1
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)
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)
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env python
import os
import platform
import subprocess
from contextlib import contextmanager
from pathlib import Path
import pytest
import test_logging
from util import start_cloudflared, wait_tunnel_ready
def select_platform(plat):
return pytest.mark.skipif(
platform.system() != plat, reason=f"Only runs on {plat}")
def default_config_dir():
return os.path.join(Path.home(), ".cloudflared")
def default_config_file():
return os.path.join(default_config_dir(), "config.yml")
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_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)
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, named_tunnel=False)
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_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)
@contextmanager
def run_service(self, tmp_path, config, root=False):
try:
service = start_cloudflared(
tmp_path, config, cfd_args=["service", "install"], cfd_pre_args=[], capture_output=False, root=root)
yield service
finally:
start_cloudflared(
tmp_path, config, cfd_args=["service", "uninstall"], cfd_pre_args=[], capture_output=False, root=root)
def launchctl_cmd(self, action, success=True):
cmd = subprocess.run(
["launchctl", action, "com.cloudflare.cloudflared"], check=success)
if not success:
assert cmd.returncode != 0, f"Expect {cmd.args} to fail, but it succeed"
def sysv_cmd(self, action, success=True):
cmd = subprocess.run(
["sudo", "service", "cloudflared", action], check=success)
if not success:
assert cmd.returncode != 0, f"Expect {cmd.args} to fail, but it succeed"
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env python
from contextlib import contextmanager
import platform
import signal
import threading
import time
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():
grace_period = 5
timeout = 10
extra_config = {
"grace-period": f"{grace_period}s",
}
sse_endpoint = "/sse?freq=1s"
@pytest.mark.parametrize("signal", supported_signals())
def test_graceful_shutdown(self, tmp_path, component_tests_config, signal):
config = component_tests_config(self.extra_config)
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, False, ))
in_flight_req.start()
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
@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)
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()
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)
@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)
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)
check_tunnel_not_connected()
cloudflared.wait()
def wait_eyeball_thread(self, thread, timeout):
thread.join(timeout)
assert thread.is_alive() == False, "eyeball thread is still alive"
# Using this context asserts logic within the context is executed within grace period
@contextmanager
def within_grace_period(self):
try:
start = time.time()
yield
finally:
duration = time.time() - start
assert duration < self.grace_period
def stream_request(self, config, connected, early_terminate):
expected_terminate_message = "502 Bad Gateway"
url = config.get_url() + self.sse_endpoint
with requests.get(url, timeout=5, stream=True) as resp:
with connected:
connected.notifyAll()
lines = 0
for line in resp.iter_lines():
if expected_terminate_message.encode() == line:
break
lines += 1
if early_terminate and lines == 2:
return
# /sse returns count followed by 2 new lines
assert lines >= (self.grace_period * 2)
+100
View File
@@ -0,0 +1,100 @@
from contextlib import contextmanager
import logging
import requests
from retrying import retry
import subprocess
import yaml
from time import sleep
from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS
LOGGER = logging.getLogger(__name__)
def write_config(directory, config):
config_path = directory / "config.yml"
with open(config_path, 'w') as outfile:
yaml.dump(config, outfile)
return config_path
def start_cloudflared(directory, config, cfd_args=["run"], cfd_pre_args=["tunnel"], new_process=False, allow_input=False, capture_output=True, root=False):
config_path = write_config(directory, config.full_config)
cmd = cloudflared_cmd(config, config_path, cfd_args, cfd_pre_args, root)
if new_process:
return run_cloudflared_background(cmd, allow_input, capture_output)
# By setting check=True, it will raise an exception if the process exits with non-zero exit code
return subprocess.run(cmd, check=True, capture_output=capture_output)
def cloudflared_cmd(config, config_path, cfd_args, cfd_pre_args, root):
cmd = []
if root:
cmd += ["sudo"]
cmd += [config.cloudflared_binary]
cmd += cfd_pre_args
cmd += ["--config", config_path]
cmd += cfd_args
LOGGER.info(f"Run cmd {cmd} with config {config}")
return cmd
@contextmanager
def run_cloudflared_background(cmd, allow_input, capture_output):
output = subprocess.PIPE if capture_output else subprocess.DEVNULL
stdin = subprocess.PIPE if allow_input else None
try:
cfd = subprocess.Popen(cmd, stdin=stdin, stdout=output, stderr=output)
yield cfd
finally:
cfd.terminate()
if capture_output:
LOGGER.info(
f"cloudflared log: {cfd.stderr.read()}")
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
def wait_tunnel_ready(tunnel_url=None, require_min_connections=1):
metrics_url = f'http://localhost:{METRICS_PORT}/ready'
with requests.Session() as s:
resp = send_request(s, metrics_url, True)
assert resp.json()[
"readyConnections"] >= require_min_connections, f"Ready endpoint returned {resp.json()} but we expect at least {require_min_connections} connections"
if tunnel_url is not None:
send_request(s, tunnel_url, True)
@retry(stop_max_attempt_number=MAX_RETRIES * BACKOFF_SECS, wait_fixed=1000)
def check_tunnel_not_connected():
url = f'http://localhost:{METRICS_PORT}/ready'
try:
resp = requests.get(url, timeout=1)
assert resp.status_code == 503, f"Expect {url} returns 503, got {resp.status_code}"
# cloudflared might already terminate
except requests.exceptions.ConnectionError as e:
LOGGER.warning(f"Failed to connect to {url}, error: {e}")
# In some cases we don't need to check response status, such as when sending batch requests to generate logs
def send_requests(url, count, require_ok=True):
errors = 0
with requests.Session() as s:
for _ in range(count):
resp = send_request(s, url, require_ok)
if resp is None:
errors += 1
sleep(0.01)
if errors > 0:
LOGGER.warning(
f"{errors} out of {count} requests to {url} return non-200 status")
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
def send_request(session, url, require_ok):
resp = session.get(url, timeout=BACKOFF_SECS)
if require_ok:
assert resp.status_code == 200, f"{url} returned {resp}"
return resp if resp.status_code == 200 else None
@@ -9,13 +9,13 @@ 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"
"github.com/rs/zerolog"
)
var (
@@ -221,15 +221,28 @@ type OriginRequestConfig struct {
ProxyPort *uint `yaml:"proxyPort"`
// Valid options are 'socks' or empty.
ProxyType *string `yaml:"proxyType"`
// IP rules for the proxy service
IPRules []IngressIPRule `yaml:"ipRules"`
}
type IngressIPRule struct {
Prefix *string `yaml:"prefix"`
Ports []int `yaml:"ports"`
Allow bool `yaml:"allow"`
}
type Configuration struct {
TunnelID string `yaml:"tunnel"`
Ingress []UnvalidatedIngressRule
WarpRouting WarpRoutingConfig `yaml:"warp-routing"`
OriginRequest OriginRequestConfig `yaml:"originRequest"`
sourceFile string
}
type WarpRoutingConfig struct {
Enabled bool `yaml:"enabled"`
}
type configFileSettings struct {
Configuration `yaml:",inline"`
// older settings will be aggregated into the generic map, should be read via cli.Context
@@ -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) {
@@ -20,6 +20,9 @@ func TestConfigFileSettings(t *testing.T) {
Path: "",
Service: "https://localhost:8001",
}
warpRouting = WarpRoutingConfig{
Enabled: true,
}
)
rawYAML := `
tunnel: config-file-test
@@ -29,6 +32,8 @@ ingress:
service: https://localhost:8000
- hostname: "*"
service: https://localhost:8001
warp-routing:
enabled: true
retries: 5
grace-period: 30s
percentage: 3.14
@@ -47,6 +52,7 @@ counters:
assert.Equal(t, "config-file-test", config.TunnelID)
assert.Equal(t, firstIngress, config.Ingress[0])
assert.Equal(t, secondIngress, config.Ingress[1])
assert.Equal(t, warpRouting, config.WarpRouting)
retries, err := config.Int("retries")
assert.NoError(t, err)
@@ -73,4 +79,5 @@ counters:
assert.NoError(t, err)
assert.Equal(t, 123, counters[0])
assert.Equal(t, 456, counters[1])
}
@@ -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 {
@@ -5,6 +5,8 @@ import (
"fmt"
"io"
"strings"
"github.com/cloudflare/cloudflared/tunneldns"
)
// Forwarder represents a client side listener to forward traffic to the edge
@@ -25,11 +27,12 @@ type Tunnel struct {
// DNSResolver represents a client side DNS resolver
type DNSResolver struct {
Enabled bool `json:"enabled"`
Address string `json:"address,omitempty"`
Port uint16 `json:"port,omitempty"`
Upstreams []string `json:"upstreams,omitempty"`
Bootstraps []string `json:"bootstraps,omitempty"`
Enabled bool `json:"enabled"`
Address string `json:"address,omitempty"`
Port uint16 `json:"port,omitempty"`
Upstreams []string `json:"upstreams,omitempty"`
Bootstraps []string `json:"bootstraps,omitempty"`
MaxUpstreamConnections int `json:"max_upstream_connections,omitempty"`
}
// Root is the base options to configure the service
@@ -59,6 +62,7 @@ func (r *DNSResolver) Hash() string {
io.WriteString(h, strings.Join(r.Bootstraps, ","))
io.WriteString(h, strings.Join(r.Upstreams, ","))
io.WriteString(h, fmt.Sprintf("%d", r.Port))
io.WriteString(h, fmt.Sprintf("%d", r.MaxUpstreamConnections))
io.WriteString(h, fmt.Sprintf("%v", r.Enabled))
return fmt.Sprintf("%x", h.Sum(nil))
}
@@ -99,3 +103,11 @@ func (r *DNSResolver) BootstrapsOrDefault() []string {
}
return []string{"https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"}
}
// MaxUpstreamConnectionsOrDefault return the max upstream connections or returns the default if negative
func (r *DNSResolver) MaxUpstreamConnectionsOrDefault() int {
if r.MaxUpstreamConnections >= 0 {
return r.MaxUpstreamConnections
}
return tunneldns.MaxUpstreamConnsDefault
}
+44 -7
View File
@@ -1,20 +1,22 @@
package connection
import (
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/google/uuid"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
const LogFieldConnIndex = "connIndex"
type Config struct {
OriginClient OriginClient
OriginProxy OriginProxy
GracePeriod time.Duration
ReplaceExisting bool
}
@@ -50,14 +52,49 @@ func (c *ClassicTunnelConfig) IsTrialZone() bool {
return c.Hostname == ""
}
type OriginClient interface {
Proxy(w ResponseWriter, req *http.Request, isWebsocket bool) error
// Type indicates the connection type of the connection.
type Type int
const (
TypeWebsocket Type = iota
TypeTCP
TypeControlStream
TypeHTTP
)
// ShouldFlush returns whether this kind of connection should actively flush data
func (t Type) shouldFlush() bool {
switch t {
case TypeWebsocket, TypeTCP, TypeControlStream:
return true
default:
return false
}
}
func (t Type) String() string {
switch t {
case TypeWebsocket:
return "websocket"
case TypeTCP:
return "tcp"
case TypeControlStream:
return "control stream"
case TypeHTTP:
return "http"
default:
return fmt.Sprintf("Unknown Type %d", t)
}
}
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
}
type ResponseWriter interface {
WriteRespHeaders(*http.Response) error
WriteErrorResponse()
io.ReadWriter
WriteRespHeaders(status int, header http.Header) error
io.Writer
}
type ConnectedFuse interface {
+7 -7
View File
@@ -19,8 +19,8 @@ const (
var (
testConfig = &Config{
OriginClient: &mockOriginClient{},
GracePeriod: time.Millisecond * 100,
OriginProxy: &mockOriginProxy{},
GracePeriod: time.Millisecond * 100,
}
log = zerolog.Nop()
testOriginURL = &url.URL{
@@ -38,11 +38,11 @@ type testRequest struct {
isProxyError bool
}
type mockOriginClient struct {
type mockOriginProxy struct {
}
func (moc *mockOriginClient) Proxy(w ResponseWriter, r *http.Request, isWebsocket bool) error {
if isWebsocket {
func (moc *mockOriginProxy) Proxy(w ResponseWriter, r *http.Request, sourceConnectionType Type) error {
if sourceConnectionType == TypeWebsocket {
return wsEndpoint(w, r)
}
switch r.URL.Path {
@@ -74,7 +74,7 @@ func wsEndpoint(w ResponseWriter, r *http.Request) error {
resp := &http.Response{
StatusCode: http.StatusSwitchingProtocols,
}
_ = w.WriteRespHeaders(resp)
_ = w.WriteRespHeaders(resp.StatusCode, resp.Header)
clientReader := nowriter{r.Body}
go func() {
for {
@@ -95,7 +95,7 @@ func originRespEndpoint(w ResponseWriter, status int, data []byte) {
resp := &http.Response{
StatusCode: status,
}
_ = w.WriteRespHeaders(resp)
_ = w.WriteRespHeaders(resp.StatusCode, resp.Header)
_, _ = w.Write(data)
}
+12 -7
View File
@@ -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 (
@@ -216,7 +216,12 @@ func (h *h2muxConnection) ServeStream(stream *h2mux.MuxedStream) error {
return reqErr
}
err := h.config.OriginClient.Proxy(respWriter, req, websocket.IsWebSocketUpgrade(req))
var sourceConnectionType = TypeHTTP
if websocket.IsWebSocketUpgrade(req) {
sourceConnectionType = TypeWebsocket
}
err := h.config.OriginProxy.Proxy(respWriter, req, sourceConnectionType)
if err != nil {
respWriter.WriteErrorResponse()
return err
@@ -240,8 +245,8 @@ type h2muxRespWriter struct {
*h2mux.MuxedStream
}
func (rp *h2muxRespWriter) WriteRespHeaders(resp *http.Response) error {
headers := h2mux.H1ResponseToH2ResponseHeaders(resp)
func (rp *h2muxRespWriter) WriteRespHeaders(status int, header http.Header) error {
headers := h2mux.H1ResponseToH2ResponseHeaders(status, header)
headers = append(headers, h2mux.Header{Name: ResponseMetaHeaderField, Value: responseMetaHeaderOrigin})
return rp.WriteHeaders(headers)
}
+1 -1
View File
@@ -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"},
})
}),
}
+63 -32
View File
@@ -10,15 +10,16 @@ import (
"strings"
"sync"
"github.com/cloudflare/cloudflared/h2mux"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/rs/zerolog"
"golang.org/x/net/http2"
"github.com/cloudflare/cloudflared/h2mux"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
const (
internalUpgradeHeader = "Cf-Cloudflared-Proxy-Connection-Upgrade"
tcpStreamHeader = "Cf-Cloudflared-Proxy-Src"
websocketUpgrade = "websocket"
controlStreamUpgrade = "control-stream"
)
@@ -96,31 +97,25 @@ func (c *http2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c.activeRequestsWG.Add(1)
defer c.activeRequestsWG.Done()
respWriter := &http2RespWriter{
r: r.Body,
w: w,
}
flusher, isFlusher := w.(http.Flusher)
if !isFlusher {
c.observer.log.Error().Msgf("%T doesn't implement http.Flusher", w)
respWriter.WriteErrorResponse()
connType := determineHTTP2Type(r)
respWriter, err := newHTTP2RespWriter(r, w, connType)
if err != nil {
c.observer.log.Error().Msg(err.Error())
return
}
respWriter.flusher = flusher
var err error
if isControlStreamUpgrade(r) {
respWriter.shouldFlush = true
err = c.serveControlStream(r.Context(), respWriter)
c.controlStreamErr = err
} else if isWebsocketUpgrade(r) {
respWriter.shouldFlush = true
stripWebsocketUpgradeHeader(r)
err = c.config.OriginClient.Proxy(respWriter, r, true)
} else {
err = c.config.OriginClient.Proxy(respWriter, r, false)
}
if err != nil {
var proxyErr error
switch connType {
case TypeControlStream:
proxyErr = c.serveControlStream(r.Context(), respWriter)
c.controlStreamErr = proxyErr
case TypeWebsocket:
stripWebsocketUpgradeHeader(r)
proxyErr = c.config.OriginProxy.Proxy(respWriter, r, TypeWebsocket)
default:
proxyErr = c.config.OriginProxy.Proxy(respWriter, r, connType)
}
if proxyErr != nil {
respWriter.WriteErrorResponse()
}
}
@@ -161,10 +156,29 @@ type http2RespWriter struct {
shouldFlush bool
}
func (rp *http2RespWriter) WriteRespHeaders(resp *http.Response) error {
func newHTTP2RespWriter(r *http.Request, w http.ResponseWriter, connType Type) (*http2RespWriter, error) {
flusher, isFlusher := w.(http.Flusher)
if !isFlusher {
respWriter := &http2RespWriter{
r: r.Body,
w: w,
}
respWriter.WriteErrorResponse()
return nil, fmt.Errorf("%T doesn't implement http.Flusher", w)
}
return &http2RespWriter{
r: r.Body,
w: w,
flusher: flusher,
shouldFlush: connType.shouldFlush(),
}, nil
}
func (rp *http2RespWriter) WriteRespHeaders(status int, header http.Header) error {
dest := rp.w.Header()
userHeaders := make(http.Header, len(resp.Header))
for header, values := range resp.Header {
userHeaders := make(http.Header, len(header))
for header, values := range header {
// Since these are http2 headers, they're required to be lowercase
h2name := strings.ToLower(header)
for _, v := range values {
@@ -184,13 +198,12 @@ func (rp *http2RespWriter) WriteRespHeaders(resp *http.Response) error {
// Perform user header serialization and set them in the single header
dest.Set(canonicalResponseUserHeadersField, h2mux.SerializeHeaders(userHeaders))
rp.setResponseMetaHeader(responseMetaHeaderOrigin)
status := resp.StatusCode
// HTTP2 removes support for 101 Switching Protocols https://tools.ietf.org/html/rfc7540#section-8.1.1
if status == http.StatusSwitchingProtocols {
status = http.StatusOK
}
rp.w.WriteHeader(status)
if IsServerSentEvent(resp.Header) {
if IsServerSentEvent(header) {
rp.shouldFlush = true
}
if rp.shouldFlush {
@@ -231,12 +244,30 @@ func (rp *http2RespWriter) Close() error {
return nil
}
func determineHTTP2Type(r *http.Request) Type {
switch {
case isWebsocketUpgrade(r):
return TypeWebsocket
case IsTCPStream(r):
return TypeTCP
case isControlStreamUpgrade(r):
return TypeControlStream
default:
return TypeHTTP
}
}
func isControlStreamUpgrade(r *http.Request) bool {
return strings.ToLower(r.Header.Get(internalUpgradeHeader)) == controlStreamUpgrade
return r.Header.Get(internalUpgradeHeader) == controlStreamUpgrade
}
func isWebsocketUpgrade(r *http.Request) bool {
return strings.ToLower(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) != ""
}
func stripWebsocketUpgradeHeader(r *http.Request) {
+43 -5
View File
@@ -12,15 +12,14 @@ 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 (
@@ -114,6 +113,7 @@ func TestServeHTTP(t *testing.T) {
}
type mockNamedTunnelRPCClient struct {
shouldFail error
registered chan struct{}
unregistered chan struct{}
}
@@ -125,6 +125,9 @@ func (mc mockNamedTunnelRPCClient) RegisterConnection(
connIndex uint8,
observer *Observer,
) error {
if mc.shouldFail != nil {
return mc.shouldFail
}
close(mc.registered)
return nil
}
@@ -136,12 +139,14 @@ func (mc mockNamedTunnelRPCClient) GracefulShutdown(ctx context.Context, gracePe
func (mockNamedTunnelRPCClient) Close() {}
type mockRPCClientFactory struct {
shouldFail error
registered chan struct{}
unregistered chan struct{}
}
func (mf *mockRPCClientFactory) newMockRPCClient(context.Context, io.ReadWriteCloser, *zerolog.Logger) NamedTunnelRPCClient {
return mockNamedTunnelRPCClient{
shouldFail: mf.shouldFail,
registered: mf.registered,
unregistered: mf.unregistered,
}
@@ -249,6 +254,39 @@ func TestServeControlStream(t *testing.T) {
wg.Wait()
}
func TestFailRegistration(t *testing.T) {
http2Conn, edgeConn := newTestHTTP2Connection()
rpcClientFactory := mockRPCClientFactory{
shouldFail: errDuplicationConnection,
registered: make(chan struct{}),
unregistered: make(chan struct{}),
}
http2Conn.newRPCClientFunc = rpcClientFactory.newMockRPCClient
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
http2Conn.Serve(ctx)
}()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/", nil)
require.NoError(t, err)
req.Header.Set(internalUpgradeHeader, controlStreamUpgrade)
edgeHTTP2Conn, err := testTransport.NewClientConn(edgeConn)
require.NoError(t, err)
resp, err := edgeHTTP2Conn.RoundTrip(req)
require.NoError(t, err)
require.Equal(t, http.StatusBadGateway, resp.StatusCode)
assert.NotNil(t, http2Conn.controlStreamErr)
cancel()
wg.Wait()
}
func TestGracefulShutdownHTTP2(t *testing.T) {
http2Conn, edgeConn := newTestHTTP2Connection()
+3 -2
View File
@@ -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
}
}
+2 -2
View File
@@ -5,9 +5,9 @@ import (
"net/url"
"strings"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/rs/zerolog"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
const (
+2 -3
View File
@@ -67,10 +67,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 +82,4 @@ func (s *eventCollectorSink) assertSawEvent(t *testing.T, event Event) {
s.mu.Lock()
defer s.mu.Unlock()
assert.Contains(t, s.observedEvents, event)
}
}
+13
View File
@@ -141,16 +141,29 @@ type PercentageFetcher func() (int32, error)
func NewProtocolSelector(
protocolFlag string,
warpRoutingEnabled bool,
namedTunnel *NamedTunnelConfig,
fetchFunc PercentageFetcher,
ttl time.Duration,
log *zerolog.Logger,
) (ProtocolSelector, error) {
// Classic tunnel is only supported with h2mux
if namedTunnel == nil {
return &staticProtocolSelector{
current: H2mux,
}, nil
}
// warp routing can only be served over http2 connections
if warpRoutingEnabled {
if protocolFlag == H2mux.String() {
log.Warn().Msg("Warp routing is only supported by http2 protocol. Upgrading protocol to http2")
}
return &staticProtocolSelector{
current: HTTP2,
}, nil
}
if protocolFlag == H2mux.String() {
return &staticProtocolSelector{
current: H2mux,
+45 -13
View File
@@ -9,7 +9,8 @@ import (
)
const (
testNoTTL = 0
testNoTTL = 0
noWarpRoutingEnabled = false
)
var (
@@ -48,14 +49,15 @@ func (dmf *dynamicMockFetcher) fetch() PercentageFetcher {
func TestNewProtocolSelector(t *testing.T) {
tests := []struct {
name string
protocol string
expectedProtocol Protocol
hasFallback bool
expectedFallback Protocol
namedTunnelConfig *NamedTunnelConfig
fetchFunc PercentageFetcher
wantErr bool
name string
protocol string
expectedProtocol Protocol
hasFallback bool
expectedFallback Protocol
warpRoutingEnabled bool
namedTunnelConfig *NamedTunnelConfig
fetchFunc PercentageFetcher
wantErr bool
}{
{
name: "classic tunnel",
@@ -108,6 +110,36 @@ func TestNewProtocolSelector(t *testing.T) {
fetchFunc: mockFetcher(100),
namedTunnelConfig: testNamedTunnelConfig,
},
{
name: "warp routing requesting h2mux",
protocol: "h2mux",
expectedProtocol: HTTP2,
hasFallback: false,
expectedFallback: H2mux,
fetchFunc: mockFetcher(100),
warpRoutingEnabled: true,
namedTunnelConfig: testNamedTunnelConfig,
},
{
name: "warp routing http2",
protocol: "http2",
expectedProtocol: HTTP2,
hasFallback: false,
expectedFallback: H2mux,
fetchFunc: mockFetcher(100),
warpRoutingEnabled: true,
namedTunnelConfig: testNamedTunnelConfig,
},
{
name: "warp routing auto",
protocol: "auto",
expectedProtocol: HTTP2,
hasFallback: false,
expectedFallback: H2mux,
fetchFunc: mockFetcher(100),
warpRoutingEnabled: true,
namedTunnelConfig: testNamedTunnelConfig,
},
{
// None named tunnel can only use h2mux, so specifying an unknown protocol is not an error
name: "classic tunnel unknown protocol",
@@ -131,7 +163,7 @@ func TestNewProtocolSelector(t *testing.T) {
}
for _, test := range tests {
selector, err := NewProtocolSelector(test.protocol, test.namedTunnelConfig, test.fetchFunc, testNoTTL, &log)
selector, err := NewProtocolSelector(test.protocol, test.warpRoutingEnabled, test.namedTunnelConfig, test.fetchFunc, testNoTTL, &log)
if test.wantErr {
assert.Error(t, err, fmt.Sprintf("test %s failed", test.name))
} else {
@@ -148,7 +180,7 @@ func TestNewProtocolSelector(t *testing.T) {
func TestAutoProtocolSelectorRefresh(t *testing.T) {
fetcher := dynamicMockFetcher{}
selector, err := NewProtocolSelector("auto", testNamedTunnelConfig, fetcher.fetch(), testNoTTL, &log)
selector, err := NewProtocolSelector("auto", noWarpRoutingEnabled, testNamedTunnelConfig, fetcher.fetch(), testNoTTL, &log)
assert.NoError(t, err)
assert.Equal(t, H2mux, selector.Current())
@@ -177,7 +209,7 @@ func TestAutoProtocolSelectorRefresh(t *testing.T) {
func TestHTTP2ProtocolSelectorRefresh(t *testing.T) {
fetcher := dynamicMockFetcher{}
selector, err := NewProtocolSelector("http2", testNamedTunnelConfig, fetcher.fetch(), testNoTTL, &log)
selector, err := NewProtocolSelector("http2", noWarpRoutingEnabled, testNamedTunnelConfig, fetcher.fetch(), testNoTTL, &log)
assert.NoError(t, err)
assert.Equal(t, HTTP2, selector.Current())
@@ -206,7 +238,7 @@ func TestHTTP2ProtocolSelectorRefresh(t *testing.T) {
func TestProtocolSelectorRefreshTTL(t *testing.T) {
fetcher := dynamicMockFetcher{percentage: 100}
selector, err := NewProtocolSelector("auto", testNamedTunnelConfig, fetcher.fetch(), time.Hour, &log)
selector, err := NewProtocolSelector("auto", noWarpRoutingEnabled, testNamedTunnelConfig, fetcher.fetch(), time.Hour, &log)
assert.NoError(t, err)
assert.Equal(t, HTTP2, selector.Current())
+3 -3
View File
@@ -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 {
-145
View File
@@ -1,145 +0,0 @@
package dbconnect
import (
"context"
"encoding/json"
"fmt"
"net/url"
"strings"
"time"
"unicode"
"unicode/utf8"
)
// Client is an interface to talk to any database.
//
// Currently, the only implementation is SQLClient, but its structure
// should be designed to handle a MongoClient or RedisClient in the future.
type Client interface {
Ping(context.Context) error
Submit(context.Context, *Command) (interface{}, error)
}
// NewClient creates a database client based on its URL scheme.
func NewClient(ctx context.Context, originURL *url.URL) (Client, error) {
return NewSQLClient(ctx, originURL)
}
// Command is a standard, non-vendor format for submitting database commands.
//
// When determining the scope of this struct, refer to the following litmus test:
// Could this (roughly) conform to SQL, Document-based, and Key-value command formats?
type Command struct {
Statement string `json:"statement"`
Arguments Arguments `json:"arguments,omitempty"`
Mode string `json:"mode,omitempty"`
Isolation string `json:"isolation,omitempty"`
Timeout time.Duration `json:"timeout,omitempty"`
}
// Validate enforces the contract of Command: non empty statement (both in length and logic),
// lowercase mode and isolation, non-zero timeout, and valid Arguments.
func (cmd *Command) Validate() error {
if cmd.Statement == "" {
return fmt.Errorf("cannot provide an empty statement")
}
if strings.Map(func(char rune) rune {
if char == ';' || unicode.IsSpace(char) {
return -1
}
return char
}, cmd.Statement) == "" {
return fmt.Errorf("cannot provide a statement with no logic: '%s'", cmd.Statement)
}
cmd.Mode = strings.ToLower(cmd.Mode)
cmd.Isolation = strings.ToLower(cmd.Isolation)
if cmd.Timeout.Nanoseconds() <= 0 {
cmd.Timeout = 24 * time.Hour
}
return cmd.Arguments.Validate()
}
// UnmarshalJSON converts a byte representation of JSON into a Command, which is also validated.
func (cmd *Command) UnmarshalJSON(data []byte) error {
// Alias is required to avoid infinite recursion from the default UnmarshalJSON.
type Alias Command
alias := &struct {
*Alias
}{
Alias: (*Alias)(cmd),
}
err := json.Unmarshal(data, &alias)
if err == nil {
err = cmd.Validate()
}
return err
}
// Arguments is a wrapper for either map-based or array-based Command arguments.
//
// Each field is mutually-exclusive and some Client implementations may not
// support both fields (eg. MySQL does not accept named arguments).
type Arguments struct {
Named map[string]interface{}
Positional []interface{}
}
// Validate enforces the contract of Arguments: non nil, mutually exclusive, and no empty or reserved keys.
func (args *Arguments) Validate() error {
if args.Named == nil {
args.Named = map[string]interface{}{}
}
if args.Positional == nil {
args.Positional = []interface{}{}
}
if len(args.Named) > 0 && len(args.Positional) > 0 {
return fmt.Errorf("both named and positional arguments cannot be specified: %+v and %+v", args.Named, args.Positional)
}
for key := range args.Named {
if key == "" {
return fmt.Errorf("named arguments cannot contain an empty key: %+v", args.Named)
}
if !utf8.ValidString(key) {
return fmt.Errorf("named argument does not conform to UTF-8 encoding: %s", key)
}
if strings.HasPrefix(key, "_") {
return fmt.Errorf("named argument cannot start with a reserved keyword '_': %s", key)
}
if unicode.IsNumber([]rune(key)[0]) {
return fmt.Errorf("named argument cannot start with a number: %s", key)
}
}
return nil
}
// UnmarshalJSON converts a byte representation of JSON into Arguments, which is also validated.
func (args *Arguments) UnmarshalJSON(data []byte) error {
var obj interface{}
err := json.Unmarshal(data, &obj)
if err != nil {
return err
}
named, ok := obj.(map[string]interface{})
if ok {
args.Named = named
} else {
positional, ok := obj.([]interface{})
if ok {
args.Positional = positional
} else {
return fmt.Errorf("arguments must either be an object {\"0\":\"val\"} or an array [\"val\"]: %s", string(data))
}
}
return args.Validate()
}
-183
View File
@@ -1,183 +0,0 @@
package dbconnect
import (
"encoding/json"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestCommandValidateEmpty(t *testing.T) {
stmts := []string{
"",
";",
" \n\t",
";\n;\t;",
}
for _, stmt := range stmts {
cmd := Command{Statement: stmt}
assert.Error(t, cmd.Validate(), stmt)
}
}
func TestCommandValidateMode(t *testing.T) {
modes := []string{
"",
"query",
"ExEc",
"PREPARE",
}
for _, mode := range modes {
cmd := Command{Statement: "Ok", Mode: mode}
assert.NoError(t, cmd.Validate(), mode)
assert.Equal(t, strings.ToLower(mode), cmd.Mode)
}
}
func TestCommandValidateIsolation(t *testing.T) {
isos := []string{
"",
"default",
"read_committed",
"SNAPshot",
}
for _, iso := range isos {
cmd := Command{Statement: "Ok", Isolation: iso}
assert.NoError(t, cmd.Validate(), iso)
assert.Equal(t, strings.ToLower(iso), cmd.Isolation)
}
}
func TestCommandValidateTimeout(t *testing.T) {
cmd := Command{Statement: "Ok", Timeout: 0}
assert.NoError(t, cmd.Validate())
assert.NotZero(t, cmd.Timeout)
cmd = Command{Statement: "Ok", Timeout: 1 * time.Second}
assert.NoError(t, cmd.Validate())
assert.Equal(t, 1*time.Second, cmd.Timeout)
}
func TestCommandValidateArguments(t *testing.T) {
cmd := Command{Statement: "Ok", Arguments: Arguments{
Named: map[string]interface{}{"key": "val"},
Positional: []interface{}{"val"},
}}
assert.Error(t, cmd.Validate())
}
func TestCommandUnmarshalJSON(t *testing.T) {
strs := []string{
"{\"statement\":\"Ok\"}",
"{\"statement\":\"Ok\",\"arguments\":[0, 3.14, \"apple\"],\"mode\":\"query\"}",
"{\"statement\":\"Ok\",\"isolation\":\"read_uncommitted\",\"timeout\":1000}",
}
for _, str := range strs {
var cmd Command
assert.NoError(t, json.Unmarshal([]byte(str), &cmd), str)
}
strs = []string{
"",
"\"",
"{}",
"{\"argument\":{\"key\":\"val\"}}",
"{\"statement\":[\"Ok\"]}",
}
for _, str := range strs {
var cmd Command
assert.Error(t, json.Unmarshal([]byte(str), &cmd), str)
}
}
func TestArgumentsValidateNotNil(t *testing.T) {
args := Arguments{}
assert.NoError(t, args.Validate())
assert.NotNil(t, args.Named)
assert.NotNil(t, args.Positional)
}
func TestArgumentsValidateMutuallyExclusive(t *testing.T) {
args := []Arguments{
Arguments{},
Arguments{Named: map[string]interface{}{"key": "val"}},
Arguments{Positional: []interface{}{"val"}},
}
for _, arg := range args {
assert.NoError(t, arg.Validate())
assert.False(t, len(arg.Named) > 0 && len(arg.Positional) > 0)
}
args = []Arguments{
Arguments{
Named: map[string]interface{}{"key": "val"},
Positional: []interface{}{"val"},
},
}
for _, arg := range args {
assert.Error(t, arg.Validate())
assert.True(t, len(arg.Named) > 0 && len(arg.Positional) > 0)
}
}
func TestArgumentsValidateKeys(t *testing.T) {
keys := []string{
"",
"_",
"_key",
"1",
"1key",
"\xf0\x28\x8c\xbc", // non-utf8
}
for _, key := range keys {
args := Arguments{Named: map[string]interface{}{key: "val"}}
assert.Error(t, args.Validate(), key)
}
}
func TestArgumentsUnmarshalJSON(t *testing.T) {
strs := []string{
"{}",
"{\"key\":\"val\"}",
"{\"key\":[1, 3.14, {\"key\":\"val\"}]}",
"[]",
"[\"key\",\"val\"]",
"[{}]",
}
for _, str := range strs {
var args Arguments
assert.NoError(t, json.Unmarshal([]byte(str), &args), str)
}
strs = []string{
"",
"\"",
"1",
"\"key\"",
"{\"key\",\"val\"}",
}
for _, str := range strs {
var args Arguments
assert.Error(t, json.Unmarshal([]byte(str), &args), str)
}
}
-165
View File
@@ -1,165 +0,0 @@
package dbconnect
import (
"context"
"log"
"net"
"strconv"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
)
// Cmd is the entrypoint command for dbconnect.
//
// The tunnel package is responsible for appending this to tunnel.Commands().
func Cmd() *cli.Command {
return &cli.Command{
Category: "Database Connect (ALPHA) - Deprecated",
Name: "db-connect",
Usage: "deprecated: Access your SQL database from Cloudflare Workers or the browser",
ArgsUsage: " ",
Description: `
This feature has been deprecated.
Please see:
cloudflared access tcp --help
for setting up database connections to the cloudflare edge.
Creates a connection between your database and the Cloudflare edge.
Now you can execute SQL commands anywhere you can send HTTPS requests.
Connect your database with any of the following commands, you can also try the "playground" without a database:
cloudflared db-connect --hostname sql.mysite.com --url postgres://user:pass@localhost?sslmode=disable \
--auth-domain mysite.cloudflareaccess.com --application-aud my-access-policy-tag
cloudflared db-connect --hostname sql-dev.mysite.com --url mysql://localhost --insecure
cloudflared db-connect --playground
Requests should be authenticated using Cloudflare Access, learn more about how to enable it here:
https://developers.cloudflare.com/access/service-auth/service-token/
`,
Flags: []cli.Flag{
altsrc.NewStringFlag(&cli.StringFlag{
Name: "url",
Usage: "URL to the database (eg. postgres://user:pass@localhost?sslmode=disable)",
EnvVars: []string{"TUNNEL_URL"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "hostname",
Usage: "Hostname to accept commands over HTTPS (eg. sql.mysite.com)",
EnvVars: []string{"TUNNEL_HOSTNAME"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "auth-domain",
Usage: "Cloudflare Access authentication domain for your account (eg. mysite.cloudflareaccess.com)",
EnvVars: []string{"TUNNEL_ACCESS_AUTH_DOMAIN"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "application-aud",
Usage: "Cloudflare Access application \"AUD\" to verify JWTs from requests",
EnvVars: []string{"TUNNEL_ACCESS_APPLICATION_AUD"},
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "insecure",
Usage: "Disable authentication, the database will be open to the Internet",
Value: false,
EnvVars: []string{"TUNNEL_ACCESS_INSECURE"},
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "playground",
Usage: "Run a temporary, in-memory SQLite3 database for testing",
Value: false,
EnvVars: []string{"TUNNEL_HELLO_WORLD"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "loglevel",
Value: "debug", // Make it more verbose than the tunnel default 'info'.
EnvVars: []string{"TUNNEL_LOGLEVEL"},
Hidden: true,
}),
},
Before: CmdBefore,
Action: CmdAction,
Hidden: true,
}
}
// CmdBefore runs some validation checks before running the command.
func CmdBefore(c *cli.Context) error {
// Show the help text is no flags are specified.
if c.NumFlags() == 0 {
return cli.ShowSubcommandHelp(c)
}
// Hello-world and playground are synonymous with each other,
// unset hello-world to prevent tunnel from initializing the hello package.
if c.IsSet("hello-world") {
c.Set("playground", "true")
c.Set("hello-world", "false")
}
// Unix-socket database urls are supported, but the logic is the same as url.
if c.IsSet("unix-socket") {
c.Set("url", c.String("unix-socket"))
c.Set("unix-socket", "")
}
// When playground mode is enabled, run with an in-memory database.
if c.IsSet("playground") {
c.Set("url", "sqlite3::memory:?cache=shared")
c.Set("insecure", strconv.FormatBool(!c.IsSet("auth-domain") && !c.IsSet("application-aud")))
}
// At this point, insecure configurations are valid.
if c.Bool("insecure") {
return nil
}
// Ensure that secure configurations specify a hostname, domain, and tag for JWT validation.
if !c.IsSet("hostname") || !c.IsSet("auth-domain") || !c.IsSet("application-aud") {
log.Fatal("must specify --hostname, --auth-domain, and --application-aud unless you want to run in --insecure mode")
}
return nil
}
// CmdAction starts the Proxy and sets the url in cli.Context to point to the Proxy address.
func CmdAction(c *cli.Context) error {
// STOR-612: sync with context in tunnel daemon.
ctx := context.Background()
var proxy *Proxy
var err error
if c.Bool("insecure") {
proxy, err = NewInsecureProxy(ctx, c.String("url"))
} else {
proxy, err = NewSecureProxy(ctx, c.String("url"), c.String("auth-domain"), c.String("application-aud"))
}
if err != nil {
log.Fatal(err)
return err
}
listenerC := make(chan net.Listener)
defer close(listenerC)
// Since the Proxy should only talk to the tunnel daemon, find the next available
// localhost port and start to listen to requests.
go func() {
err := proxy.Start(ctx, "127.0.0.1:", listenerC)
if err != nil {
log.Fatal(err)
}
}()
// Block until the the Proxy is online, retreive its address, and change the url to point to it.
// This is effectively "handing over" control to the tunnel package so it can run the tunnel daemon.
c.Set("url", "https://"+(<-listenerC).Addr().String())
return nil
}
-27
View File
@@ -1,27 +0,0 @@
package dbconnect
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2"
)
func TestCmd(t *testing.T) {
tests := [][]string{
{"cloudflared", "db-connect", "--playground"},
{"cloudflared", "db-connect", "--playground", "--hostname", "sql.mysite.com"},
{"cloudflared", "db-connect", "--url", "sqlite3::memory:?cache=shared", "--insecure"},
{"cloudflared", "db-connect", "--url", "sqlite3::memory:?cache=shared", "--hostname", "sql.mysite.com", "--auth-domain", "mysite.cloudflareaccess.com", "--application-aud", "aud"},
}
app := &cli.App{
Name: "cloudflared",
Commands: []*cli.Command{Cmd()},
}
for _, test := range tests {
assert.NoError(t, app.Run(test))
}
}
-279
View File
@@ -1,279 +0,0 @@
package dbconnect
import (
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"time"
"github.com/cloudflare/cloudflared/hello"
"github.com/cloudflare/cloudflared/validation"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/rs/zerolog"
)
const (
LogFieldCFRay = "cfRay"
)
// Proxy is an HTTP server that proxies requests to a Client.
type Proxy struct {
client Client
accessValidator *validation.Access
log *zerolog.Logger
}
// NewInsecureProxy creates a Proxy that talks to a Client at an origin.
//
// In insecure mode, the Proxy will allow all Command requests.
func NewInsecureProxy(ctx context.Context, origin string) (*Proxy, error) {
originURL, err := url.Parse(origin)
if err != nil {
return nil, errors.Wrap(err, "must provide a valid database url")
}
client, err := NewClient(ctx, originURL)
if err != nil {
return nil, err
}
err = client.Ping(ctx)
if err != nil {
return nil, errors.Wrap(err, "could not connect to the database")
}
log := zerolog.New(os.Stderr).With().Logger() // TODO: Does not obey log configuration
return &Proxy{client, nil, &log}, nil
}
// NewSecureProxy creates a Proxy that talks to a Client at an origin.
//
// In secure mode, the Proxy will reject any Command requests that are
// not authenticated by Cloudflare Access with a valid JWT.
func NewSecureProxy(ctx context.Context, origin, authDomain, applicationAUD string) (*Proxy, error) {
proxy, err := NewInsecureProxy(ctx, origin)
if err != nil {
return nil, err
}
validator, err := validation.NewAccessValidator(ctx, authDomain, authDomain, applicationAUD)
if err != nil {
return nil, err
}
proxy.accessValidator = validator
return proxy, err
}
// IsInsecure gets whether the Proxy will accept a Command from any source.
func (proxy *Proxy) IsInsecure() bool {
return proxy.accessValidator == nil
}
// IsAllowed checks whether a http.Request is allowed to receive data.
//
// By default, requests must pass through Cloudflare Access for authentication.
// If the proxy is explcitly set to insecure mode, all requests will be allowed.
func (proxy *Proxy) IsAllowed(r *http.Request, verbose ...bool) bool {
if proxy.IsInsecure() {
return true
}
// Access and Tunnel should prevent bad JWTs from even reaching the origin,
// but validate tokens anyway as an abundance of caution.
err := proxy.accessValidator.ValidateRequest(r.Context(), r)
if err == nil {
return true
}
// Warn administrators that invalid JWTs are being rejected. This is indicative
// of either a misconfiguration of the CLI or a massive failure of upstream systems.
if len(verbose) > 0 {
proxy.log.Err(err).Str(LogFieldCFRay, proxy.getRayHeader(r)).Msg("dbproxy: Failed JWT authentication")
}
return false
}
// Start the Proxy at a given address and notify the listener channel when the server is online.
func (proxy *Proxy) Start(ctx context.Context, addr string, listenerC chan<- net.Listener) error {
// STOR-611: use a seperate listener and consider web socket support.
httpListener, err := hello.CreateTLSListener(addr)
if err != nil {
return errors.Wrapf(err, "could not create listener at %s", addr)
}
errC := make(chan error)
defer close(errC)
// Starts the HTTP server and begins to serve requests.
go func() {
errC <- proxy.httpListen(ctx, httpListener)
}()
// Continually ping the server until it comes online or 10 attempts fail.
go func() {
var err error
for i := 0; i < 10; i++ {
_, err = http.Get("http://" + httpListener.Addr().String())
// Once no error was detected, notify the listener channel and return.
if err == nil {
listenerC <- httpListener
return
}
// Backoff between requests to ping the server.
<-time.After(1 * time.Second)
}
errC <- errors.Wrap(err, "took too long for the http server to start")
}()
return <-errC
}
// httpListen starts the httpServer and blocks until the context closes.
func (proxy *Proxy) httpListen(ctx context.Context, listener net.Listener) error {
httpServer := &http.Server{
Addr: listener.Addr().String(),
Handler: proxy.httpRouter(),
ReadTimeout: 10 * time.Second,
WriteTimeout: 60 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
<-ctx.Done()
_ = httpServer.Close()
_ = listener.Close()
}()
return httpServer.Serve(listener)
}
// httpRouter creates a mux.Router for the Proxy.
func (proxy *Proxy) httpRouter() *mux.Router {
router := mux.NewRouter()
router.HandleFunc("/ping", proxy.httpPing()).Methods("GET", "HEAD")
router.HandleFunc("/submit", proxy.httpSubmit()).Methods("POST")
return router
}
// httpPing tests the connection to the database.
//
// By default, this endpoint is unauthenticated to allow for health checks.
// To enable authentication, Cloudflare Access must be enabled on this route.
func (proxy *Proxy) httpPing() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
err := proxy.client.Ping(ctx)
if err == nil {
proxy.httpRespond(w, r, http.StatusOK, "")
} else {
proxy.httpRespondErr(w, r, http.StatusInternalServerError, err)
}
}
}
// httpSubmit sends a command to the database and returns its response.
//
// By default, this endpoint will reject requests that do not pass through Cloudflare Access.
// To disable authentication, the --insecure flag must be specified in the command line.
func (proxy *Proxy) httpSubmit() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !proxy.IsAllowed(r, true) {
proxy.httpRespondErr(w, r, http.StatusForbidden, fmt.Errorf(""))
return
}
var cmd Command
err := json.NewDecoder(r.Body).Decode(&cmd)
if err != nil {
proxy.httpRespondErr(w, r, http.StatusBadRequest, err)
return
}
ctx := r.Context()
data, err := proxy.client.Submit(ctx, &cmd)
if err != nil {
proxy.httpRespondErr(w, r, http.StatusUnprocessableEntity, err)
return
}
w.Header().Set("Content-type", "application/json")
err = json.NewEncoder(w).Encode(data)
if err != nil {
proxy.httpRespondErr(w, r, http.StatusInternalServerError, err)
}
}
}
// httpRespond writes a status code and string response to the response writer.
func (proxy *Proxy) httpRespond(w http.ResponseWriter, r *http.Request, status int, message string) {
w.WriteHeader(status)
// Only expose the message detail of the reponse if the request is not HEAD
// and the user is authenticated. For example, this prevents an unauthenticated
// failed health check from accidentally leaking sensitive information about the Client.
if r.Method != http.MethodHead && proxy.IsAllowed(r) {
if message == "" {
message = http.StatusText(status)
}
fmt.Fprint(w, message)
}
}
// httpRespondErr is similar to httpRespond, except it formats errors to be more friendly.
func (proxy *Proxy) httpRespondErr(w http.ResponseWriter, r *http.Request, defaultStatus int, err error) {
status, err := httpError(defaultStatus, err)
proxy.httpRespond(w, r, status, err.Error())
if len(err.Error()) > 0 {
cfRay := proxy.getRayHeader(r)
proxy.log.Err(err).Str(LogFieldCFRay, cfRay).Msg("dbproxy: Database proxy error")
}
}
// getRayHeader returns the request's Cf-ray header.
func (proxy *Proxy) getRayHeader(r *http.Request) string {
return r.Header.Get("Cf-ray")
}
// httpError extracts common errors and returns an status code and friendly error.
func httpError(defaultStatus int, err error) (int, error) {
if err == nil {
return http.StatusNotImplemented, fmt.Errorf("error expected but found none")
}
if err == io.EOF {
return http.StatusBadRequest, fmt.Errorf("request body cannot be empty")
}
if err == context.DeadlineExceeded {
return http.StatusRequestTimeout, err
}
_, ok := err.(net.Error)
if ok {
return http.StatusRequestTimeout, err
}
if err == context.Canceled {
// Does not exist in Golang, but would be: http.StatusClientClosedWithoutResponse
return 444, err
}
return defaultStatus, err
}
-238
View File
@@ -1,238 +0,0 @@
package dbconnect
import (
"context"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
)
func TestNewInsecureProxy(t *testing.T) {
origins := []string{
"",
":/",
"http://localhost",
"tcp://localhost:9000?debug=true",
"mongodb://127.0.0.1",
}
for _, origin := range origins {
proxy, err := NewInsecureProxy(context.Background(), origin)
assert.Error(t, err)
assert.Empty(t, proxy)
}
}
func TestProxyIsAllowed(t *testing.T) {
proxy := helperNewProxy(t)
req := httptest.NewRequest("GET", "https://1.1.1.1/ping", nil)
assert.True(t, proxy.IsAllowed(req))
proxy = helperNewProxy(t, true)
req.Header.Set("Cf-access-jwt-assertion", "xxx")
assert.False(t, proxy.IsAllowed(req))
}
func TestProxyStart(t *testing.T) {
proxy := helperNewProxy(t)
ctx := context.Background()
listenerC := make(chan net.Listener)
err := proxy.Start(ctx, "1.1.1.1:", listenerC)
assert.Error(t, err)
err = proxy.Start(ctx, "127.0.0.1:-1", listenerC)
assert.Error(t, err)
ctx, cancel := context.WithTimeout(ctx, 0)
defer cancel()
err = proxy.Start(ctx, "127.0.0.1:", listenerC)
assert.IsType(t, http.ErrServerClosed, err)
}
func TestProxyHTTPRouter(t *testing.T) {
proxy := helperNewProxy(t)
router := proxy.httpRouter()
tests := []struct {
path string
method string
valid bool
}{
{"", "GET", false},
{"/", "GET", false},
{"/ping", "GET", true},
{"/ping", "HEAD", true},
{"/ping", "POST", false},
{"/submit", "POST", true},
{"/submit", "GET", false},
{"/submit/extra", "POST", false},
}
for _, test := range tests {
match := &mux.RouteMatch{}
ok := router.Match(httptest.NewRequest(test.method, "https://1.1.1.1"+test.path, nil), match)
assert.True(t, ok == test.valid, test.path)
}
}
func TestProxyHTTPPing(t *testing.T) {
proxy := helperNewProxy(t)
server := httptest.NewServer(proxy.httpPing())
defer server.Close()
client := server.Client()
res, err := client.Get(server.URL)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
assert.Equal(t, int64(2), res.ContentLength)
res, err = client.Head(server.URL)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
assert.Equal(t, int64(-1), res.ContentLength)
}
func TestProxyHTTPSubmit(t *testing.T) {
proxy := helperNewProxy(t)
server := httptest.NewServer(proxy.httpSubmit())
defer server.Close()
client := server.Client()
tests := []struct {
input string
status int
output string
}{
{"", http.StatusBadRequest, "request body cannot be empty"},
{"{}", http.StatusBadRequest, "cannot provide an empty statement"},
{"{\"statement\":\"Ok\"}", http.StatusUnprocessableEntity, "cannot provide invalid sql mode: ''"},
{"{\"statement\":\"Ok\",\"mode\":\"query\"}", http.StatusUnprocessableEntity, "near \"Ok\": syntax error"},
{"{\"statement\":\"CREATE TABLE t (a INT);\",\"mode\":\"exec\"}", http.StatusOK, "{\"last_insert_id\":0,\"rows_affected\":0}\n"},
}
for _, test := range tests {
res, err := client.Post(server.URL, "application/json", strings.NewReader(test.input))
assert.NoError(t, err)
assert.Equal(t, test.status, res.StatusCode)
if res.StatusCode > http.StatusOK {
assert.Equal(t, "text/plain; charset=utf-8", res.Header.Get("Content-type"))
} else {
assert.Equal(t, "application/json", res.Header.Get("Content-type"))
}
data, err := ioutil.ReadAll(res.Body)
defer res.Body.Close()
str := string(data)
assert.NoError(t, err)
assert.Equal(t, test.output, str)
}
}
func TestProxyHTTPSubmitForbidden(t *testing.T) {
proxy := helperNewProxy(t, true)
server := httptest.NewServer(proxy.httpSubmit())
defer server.Close()
client := server.Client()
res, err := client.Get(server.URL)
assert.NoError(t, err)
assert.Equal(t, http.StatusForbidden, res.StatusCode)
assert.Zero(t, res.ContentLength)
}
func TestProxyHTTPRespond(t *testing.T) {
proxy := helperNewProxy(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
proxy.httpRespond(w, r, http.StatusAccepted, "Hello")
}))
defer server.Close()
client := server.Client()
res, err := client.Get(server.URL)
assert.NoError(t, err)
assert.Equal(t, http.StatusAccepted, res.StatusCode)
assert.Equal(t, int64(5), res.ContentLength)
data, err := ioutil.ReadAll(res.Body)
defer res.Body.Close()
assert.Equal(t, []byte("Hello"), data)
}
func TestProxyHTTPRespondForbidden(t *testing.T) {
proxy := helperNewProxy(t, true)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
proxy.httpRespond(w, r, http.StatusAccepted, "Hello")
}))
defer server.Close()
client := server.Client()
res, err := client.Get(server.URL)
assert.NoError(t, err)
assert.Equal(t, http.StatusAccepted, res.StatusCode)
assert.Equal(t, int64(0), res.ContentLength)
}
func TestHTTPError(t *testing.T) {
_, errTimeout := net.DialTimeout("tcp", "127.0.0.1", 0)
assert.Error(t, errTimeout)
tests := []struct {
input error
status int
output error
}{
{nil, http.StatusNotImplemented, fmt.Errorf("error expected but found none")},
{io.EOF, http.StatusBadRequest, fmt.Errorf("request body cannot be empty")},
{context.DeadlineExceeded, http.StatusRequestTimeout, nil},
{context.Canceled, 444, nil},
{errTimeout, http.StatusRequestTimeout, nil},
{fmt.Errorf(""), http.StatusInternalServerError, nil},
}
for _, test := range tests {
status, err := httpError(http.StatusInternalServerError, test.input)
assert.Error(t, err)
assert.Equal(t, test.status, status)
if test.output == nil {
test.output = test.input
}
assert.Equal(t, test.output, err)
}
}
func helperNewProxy(t *testing.T, secure ...bool) *Proxy {
t.Helper()
proxy, err := NewSecureProxy(context.Background(), "file::memory:?cache=shared", "test.cloudflareaccess.com", "")
assert.NoError(t, err)
assert.NotNil(t, proxy)
if len(secure) == 0 {
proxy.accessValidator = nil // Mark as insecure
}
return proxy
}
-318
View File
@@ -1,318 +0,0 @@
package dbconnect
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"net/url"
"reflect"
"strings"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"github.com/xo/dburl"
// SQL drivers self-register with the database/sql package.
// https://github.com/golang/go/wiki/SQLDrivers
_ "github.com/denisenkom/go-mssqldb"
_ "github.com/go-sql-driver/mysql"
_ "github.com/mattn/go-sqlite3"
"github.com/kshvakov/clickhouse"
"github.com/lib/pq"
)
// SQLClient is a Client that talks to a SQL database.
type SQLClient struct {
Dialect string
driver *sqlx.DB
}
// NewSQLClient creates a SQL client based on its URL scheme.
func NewSQLClient(ctx context.Context, originURL *url.URL) (Client, error) {
res, err := dburl.Parse(originURL.String())
if err != nil {
helpText := fmt.Sprintf("supported drivers: %+q, see documentation for more details: %s", sql.Drivers(), "https://godoc.org/github.com/xo/dburl")
return nil, fmt.Errorf("could not parse sql database url '%s': %s\n%s", originURL, err.Error(), helpText)
}
// Establishes the driver, but does not test the connection.
driver, err := sqlx.Open(res.Driver, res.DSN)
if err != nil {
return nil, fmt.Errorf("could not open sql driver %s: %s\n%s", res.Driver, err.Error(), res.DSN)
}
// Closes the driver, will occur when the context finishes.
go func() {
<-ctx.Done()
_ = driver.Close()
}()
return &SQLClient{driver.DriverName(), driver}, nil
}
// Ping verifies a connection to the database is still alive.
func (client *SQLClient) Ping(ctx context.Context) error {
return client.driver.PingContext(ctx)
}
// Submit queries or executes a command to the SQL database.
func (client *SQLClient) Submit(ctx context.Context, cmd *Command) (interface{}, error) {
txx, err := cmd.ValidateSQL(client.Dialect)
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(ctx, cmd.Timeout)
defer cancel()
var res interface{}
// Get the next available sql.Conn and submit the Command.
err = sqlConn(ctx, client.driver, txx, func(conn *sql.Conn) error {
stmt := cmd.Statement
args := cmd.Arguments.Positional
if cmd.Mode == "query" {
res, err = sqlQuery(ctx, conn, stmt, args)
} else {
res, err = sqlExec(ctx, conn, stmt, args)
}
return err
})
return res, err
}
// ValidateSQL extends the contract of Command for SQL dialects:
// mode is conformed, arguments are []sql.NamedArg, and isolation is a sql.IsolationLevel.
//
// When the command should not be wrapped in a transaction, *sql.TxOptions and error will both be nil.
func (cmd *Command) ValidateSQL(dialect string) (*sql.TxOptions, error) {
err := cmd.Validate()
if err != nil {
return nil, err
}
mode, err := sqlMode(cmd.Mode)
if err != nil {
return nil, err
}
// Mutates Arguments to only use positional arguments with the type sql.NamedArg.
// This is a required by the sql.Driver before submitting arguments.
cmd.Arguments.sql(dialect)
iso, err := sqlIsolation(cmd.Isolation)
if err != nil {
return nil, err
}
// When isolation is out-of-range, this is indicative that no
// transaction should be executed and sql.TxOptions should be nil.
if iso < sql.LevelDefault {
return nil, nil
}
// In query mode, execute the transaction in read-only, unless it's Microsoft SQL
// which does not support that type of transaction.
readOnly := mode == "query" && dialect != "mssql"
return &sql.TxOptions{Isolation: iso, ReadOnly: readOnly}, nil
}
// sqlConn gets the next available sql.Conn in the connection pool and runs a function to use it.
//
// If the transaction options are nil, run the useIt function outside a transaction.
// This is potentially an unsafe operation if the command does not clean up its state.
func sqlConn(ctx context.Context, driver *sqlx.DB, txx *sql.TxOptions, useIt func(*sql.Conn) error) error {
conn, err := driver.Conn(ctx)
if err != nil {
return err
}
defer conn.Close()
// If transaction options are specified, begin and defer a rollback to catch errors.
var tx *sql.Tx
if txx != nil {
tx, err = conn.BeginTx(ctx, txx)
if err != nil {
return err
}
defer tx.Rollback()
}
err = useIt(conn)
// Check if useIt was successful and a transaction exists before committing.
if err == nil && tx != nil {
err = tx.Commit()
}
return err
}
// sqlQuery queries rows on a sql.Conn and returns an array of result objects.
func sqlQuery(ctx context.Context, conn *sql.Conn, stmt string, args []interface{}) ([]map[string]interface{}, error) {
rows, err := conn.QueryContext(ctx, stmt, args...)
if err == nil {
return sqlRows(rows)
}
return nil, err
}
// sqlExec executes a command on a sql.Conn and returns the result of the operation.
func sqlExec(ctx context.Context, conn *sql.Conn, stmt string, args []interface{}) (sqlResult, error) {
exec, err := conn.ExecContext(ctx, stmt, args...)
if err == nil {
return sqlResultFrom(exec), nil
}
return sqlResult{}, err
}
// sql mutates Arguments to contain a positional []sql.NamedArg.
//
// The actual return type is []interface{} due to the native Golang
// function signatures for sql.Exec and sql.Query being generic.
func (args *Arguments) sql(dialect string) {
result := args.Positional
for i, val := range result {
result[i] = sqlArg("", val, dialect)
}
for key, val := range args.Named {
result = append(result, sqlArg(key, val, dialect))
}
args.Positional = result
args.Named = map[string]interface{}{}
}
// sqlArg creates a sql.NamedArg from a key-value pair and an optional dialect.
//
// Certain dialects will need to wrap objects, such as arrays, to conform its driver requirements.
func sqlArg(key, val interface{}, dialect string) sql.NamedArg {
switch reflect.ValueOf(val).Kind() {
// PostgreSQL and Clickhouse require arrays to be wrapped before
// being inserted into the driver interface.
case reflect.Slice, reflect.Array:
switch dialect {
case "postgres":
val = pq.Array(val)
case "clickhouse":
val = clickhouse.Array(val)
}
}
return sql.Named(fmt.Sprint(key), val)
}
// sqlIsolation tries to match a string to a sql.IsolationLevel.
func sqlIsolation(str string) (sql.IsolationLevel, error) {
if str == "none" {
return sql.IsolationLevel(-1), nil
}
for iso := sql.LevelDefault; ; iso++ {
if iso > sql.LevelLinearizable {
return -1, fmt.Errorf("cannot provide an invalid sql isolation level: '%s'", str)
}
if str == "" || strings.EqualFold(iso.String(), strings.ReplaceAll(str, "_", " ")) {
return iso, nil
}
}
}
// sqlMode tries to match a string to a command mode: 'query' or 'exec' for now.
func sqlMode(str string) (string, error) {
switch str {
case "query", "exec":
return str, nil
default:
return "", fmt.Errorf("cannot provide invalid sql mode: '%s'", str)
}
}
// sqlRows scans through a SQL result set and returns an array of objects.
func sqlRows(rows *sql.Rows) ([]map[string]interface{}, error) {
columns, err := rows.Columns()
if err != nil {
return nil, errors.Wrap(err, "could not extract columns from result")
}
defer rows.Close()
types, err := rows.ColumnTypes()
if err != nil {
// Some drivers do not support type extraction, so fail silently and continue.
types = make([]*sql.ColumnType, len(columns))
}
values := make([]interface{}, len(columns))
pointers := make([]interface{}, len(columns))
var results []map[string]interface{}
for rows.Next() {
for i := range columns {
pointers[i] = &values[i]
}
_ = rows.Scan(pointers...)
// Convert a row, an array of values, into an object where
// each key is the name of its respective column.
entry := make(map[string]interface{})
for i, col := range columns {
entry[col] = sqlValue(values[i], types[i])
}
results = append(results, entry)
}
return results, nil
}
// sqlValue handles special cases where sql.Rows does not return a "human-readable" object.
func sqlValue(val interface{}, col *sql.ColumnType) interface{} {
bytes, ok := val.([]byte)
if ok {
// Opportunistically check for embeded JSON and convert it to a first-class object.
var embeded interface{}
if json.Unmarshal(bytes, &embeded) == nil {
return embeded
}
// STOR-604: investigate a way to coerce PostgreSQL arrays '{a, b, ...}' into JSON.
// Although easy with strings, it becomes more difficult with special types like INET[].
return string(bytes)
}
return val
}
// sqlResult is a thin wrapper around sql.Result.
type sqlResult struct {
LastInsertId int64 `json:"last_insert_id"`
RowsAffected int64 `json:"rows_affected"`
}
// sqlResultFrom converts sql.Result into a JSON-marshable sqlResult.
func sqlResultFrom(res sql.Result) sqlResult {
insertID, errID := res.LastInsertId()
rowsAffected, errRows := res.RowsAffected()
// If an error occurs when extracting the result, it is because the
// driver does not support that specific field. Instead of passing this
// to the user, omit the field in the response.
if errID != nil {
insertID = -1
}
if errRows != nil {
rowsAffected = -1
}
return sqlResult{insertID, rowsAffected}
}
-336
View File
@@ -1,336 +0,0 @@
package dbconnect
import (
"context"
"database/sql"
"fmt"
"net/url"
"strings"
"testing"
"time"
"github.com/kshvakov/clickhouse"
"github.com/lib/pq"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
)
func TestNewSQLClient(t *testing.T) {
originURLs := []string{
"postgres://localhost",
"cockroachdb://localhost:1337",
"postgresql://user:pass@127.0.0.1",
"mysql://localhost",
"clickhouse://127.0.0.1:9000/?debug",
"sqlite3::memory:",
"file:test.db?cache=shared",
}
for _, originURL := range originURLs {
origin, _ := url.Parse(originURL)
_, err := NewSQLClient(context.Background(), origin)
assert.NoError(t, err, originURL)
}
originURLs = []string{
"",
"/",
"http://localhost",
"coolthing://user:pass@127.0.0.1",
}
for _, originURL := range originURLs {
origin, _ := url.Parse(originURL)
_, err := NewSQLClient(context.Background(), origin)
assert.Error(t, err, originURL)
}
}
func TestArgumentsSQL(t *testing.T) {
args := []Arguments{
Arguments{
Positional: []interface{}{
"val", 10, 3.14,
},
},
Arguments{
Named: map[string]interface{}{
"key": time.Unix(0, 0),
},
},
}
var nameType sql.NamedArg
for _, arg := range args {
arg.sql("")
for _, named := range arg.Positional {
assert.IsType(t, nameType, named)
}
}
}
func TestSQLArg(t *testing.T) {
tests := []struct {
key interface{}
val interface{}
dialect string
arg sql.NamedArg
}{
{"key", "val", "mssql", sql.Named("key", "val")},
{0, 1, "sqlite3", sql.Named("0", 1)},
{1, []string{"a", "b", "c"}, "postgres", sql.Named("1", pq.Array([]string{"a", "b", "c"}))},
{"in", []uint{0, 1}, "clickhouse", sql.Named("in", clickhouse.Array([]uint{0, 1}))},
{"", time.Unix(0, 0), "", sql.Named("", time.Unix(0, 0))},
}
for _, test := range tests {
arg := sqlArg(test.key, test.val, test.dialect)
assert.Equal(t, test.arg, arg, test.key)
}
}
func TestSQLIsolation(t *testing.T) {
tests := []struct {
str string
iso sql.IsolationLevel
}{
{"", sql.LevelDefault},
{"DEFAULT", sql.LevelDefault},
{"read_UNcommitted", sql.LevelReadUncommitted},
{"serializable", sql.LevelSerializable},
{"none", sql.IsolationLevel(-1)},
{"SNAP shot", -2},
{"blah", -2},
}
for _, test := range tests {
iso, err := sqlIsolation(test.str)
if test.iso < -1 {
assert.Error(t, err, test.str)
} else {
assert.NoError(t, err)
assert.Equal(t, test.iso, iso, test.str)
}
}
}
func TestSQLMode(t *testing.T) {
modes := []string{
"query",
"exec",
}
for _, mode := range modes {
actual, err := sqlMode(mode)
assert.NoError(t, err)
assert.Equal(t, strings.ToLower(mode), actual, mode)
}
modes = []string{
"",
"blah",
}
for _, mode := range modes {
_, err := sqlMode(mode)
assert.Error(t, err)
}
}
func helperRows(mockRows *sqlmock.Rows) *sql.Rows {
db, mock, _ := sqlmock.New()
mock.ExpectQuery("SELECT").WillReturnRows(mockRows)
rows, _ := db.Query("SELECT")
return rows
}
func TestSQLRows(t *testing.T) {
actual, err := sqlRows(helperRows(sqlmock.NewRows(
[]string{"name", "age", "dept"}).
AddRow("alice", 19, "prod")))
expected := []map[string]interface{}{map[string]interface{}{
"name": "alice",
"age": int64(19),
"dept": "prod"}}
assert.NoError(t, err)
assert.ElementsMatch(t, expected, actual)
}
func TestSQLValue(t *testing.T) {
tests := []struct {
input interface{}
output interface{}
}{
{"hello", "hello"},
{1, 1},
{false, false},
{[]byte("random"), "random"},
{[]byte("{\"json\":true}"), map[string]interface{}{"json": true}},
{[]byte("[]"), []interface{}{}},
}
for _, test := range tests {
assert.Equal(t, test.output, sqlValue(test.input, nil), test.input)
}
}
func TestSQLResultFrom(t *testing.T) {
res := sqlResultFrom(sqlmock.NewResult(1, 2))
assert.Equal(t, sqlResult{1, 2}, res)
res = sqlResultFrom(sqlmock.NewErrorResult(fmt.Errorf("")))
assert.Equal(t, sqlResult{-1, -1}, res)
}
func helperSQLite3(t *testing.T) (context.Context, Client) {
t.Helper()
ctx := context.Background()
url, _ := url.Parse("file::memory:?cache=shared")
sqlite3, err := NewSQLClient(ctx, url)
assert.NoError(t, err)
return ctx, sqlite3
}
func TestPing(t *testing.T) {
ctx, sqlite3 := helperSQLite3(t)
err := sqlite3.Ping(ctx)
assert.NoError(t, err)
}
func TestSubmit(t *testing.T) {
ctx, sqlite3 := helperSQLite3(t)
res, err := sqlite3.Submit(ctx, &Command{
Statement: "CREATE TABLE t (a INTEGER, b FLOAT, c TEXT, d BLOB);",
Mode: "exec",
})
assert.NoError(t, err)
assert.Equal(t, sqlResult{0, 0}, res)
res, err = sqlite3.Submit(ctx, &Command{
Statement: "SELECT * FROM t;",
Mode: "query",
})
assert.NoError(t, err)
assert.Empty(t, res)
res, err = sqlite3.Submit(ctx, &Command{
Statement: "INSERT INTO t VALUES (?, ?, ?, ?);",
Mode: "exec",
Arguments: Arguments{
Positional: []interface{}{
1,
3.14,
"text",
"blob",
},
},
})
assert.NoError(t, err)
assert.Equal(t, sqlResult{1, 1}, res)
res, err = sqlite3.Submit(ctx, &Command{
Statement: "UPDATE t SET c = NULL;",
Mode: "exec",
})
assert.NoError(t, err)
assert.Equal(t, sqlResult{1, 1}, res)
res, err = sqlite3.Submit(ctx, &Command{
Statement: "SELECT * FROM t WHERE a = ?;",
Mode: "query",
Arguments: Arguments{
Positional: []interface{}{1},
},
})
assert.NoError(t, err)
assert.Len(t, res, 1)
resf, ok := res.([]map[string]interface{})
assert.True(t, ok)
assert.EqualValues(t, map[string]interface{}{
"a": int64(1),
"b": 3.14,
"c": nil,
"d": "blob",
}, resf[0])
res, err = sqlite3.Submit(ctx, &Command{
Statement: "DROP TABLE t;",
Mode: "exec",
})
assert.NoError(t, err)
assert.Equal(t, sqlResult{1, 1}, res)
}
func TestSubmitTransaction(t *testing.T) {
ctx, sqlite3 := helperSQLite3(t)
res, err := sqlite3.Submit(ctx, &Command{
Statement: "BEGIN;",
Mode: "exec",
})
assert.Error(t, err)
assert.Empty(t, res)
res, err = sqlite3.Submit(ctx, &Command{
Statement: "BEGIN; CREATE TABLE tt (a INT); COMMIT;",
Mode: "exec",
Isolation: "none",
})
assert.NoError(t, err)
assert.Equal(t, sqlResult{0, 0}, res)
rows, err := sqlite3.Submit(ctx, &Command{
Statement: "SELECT * FROM tt;",
Mode: "query",
Isolation: "repeatable_read",
})
assert.NoError(t, err)
assert.Empty(t, rows)
}
func TestSubmitTimeout(t *testing.T) {
ctx, sqlite3 := helperSQLite3(t)
res, err := sqlite3.Submit(ctx, &Command{
Statement: "SELECT * FROM t;",
Mode: "query",
Timeout: 1 * time.Nanosecond,
})
assert.Error(t, err)
assert.Empty(t, res)
}
func TestSubmitMode(t *testing.T) {
ctx, sqlite3 := helperSQLite3(t)
res, err := sqlite3.Submit(ctx, &Command{
Statement: "SELECT * FROM t;",
Mode: "notanoption",
})
assert.Error(t, err)
assert.Empty(t, res)
}
func TestSubmitEmpty(t *testing.T) {
ctx, sqlite3 := helperSQLite3(t)
res, err := sqlite3.Submit(ctx, &Command{
Statement: "; ; ; ;",
Mode: "query",
})
assert.Error(t, err)
assert.Empty(t, res)
}
-78
View File
@@ -1,78 +0,0 @@
# docker-compose -f ./dbconnect_tests/dbconnect.yaml up --build --force-recreate --renew-anon-volumes --exit-code-from cloudflared
version: "2.3"
networks:
test-dbconnect-network:
driver: bridge
services:
cloudflared:
build:
context: ../
dockerfile: dev.Dockerfile
command: go test github.com/cloudflare/cloudflared/dbconnect_tests -v
depends_on:
postgres:
condition: service_healthy
mysql:
condition: service_healthy
mssql:
condition: service_healthy
clickhouse:
condition: service_healthy
environment:
DBCONNECT_INTEGRATION_TEST: "true"
POSTGRESQL_URL: postgres://postgres:secret@postgres/db?sslmode=disable
MYSQL_URL: mysql://root:secret@mysql/db?tls=false
MSSQL_URL: mssql://sa:secret12345!@mssql
CLICKHOUSE_URL: clickhouse://clickhouse:9000/db
networks:
- test-dbconnect-network
postgres:
image: postgres:11.4-alpine
environment:
POSTGRES_DB: db
POSTGRES_PASSWORD: secret
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
start_period: 3s
interval: 1s
timeout: 3s
retries: 10
networks:
- test-dbconnect-network
mysql:
image: mysql:8.0
environment:
MYSQL_DATABASE: db
MYSQL_ROOT_PASSWORD: secret
healthcheck:
test: ["CMD", "mysqladmin", "ping"]
start_period: 3s
interval: 1s
timeout: 3s
retries: 10
networks:
- test-dbconnect-network
mssql:
image: mcr.microsoft.com/mssql/server:2017-CU8-ubuntu
environment:
ACCEPT_EULA: "Y"
SA_PASSWORD: secret12345!
healthcheck:
test: ["CMD", "/opt/mssql-tools/bin/sqlcmd", "-S", "localhost", "-U", "sa", "-P", "secret12345!", "-Q", "SELECT 1"]
start_period: 3s
interval: 1s
timeout: 3s
retries: 10
networks:
- test-dbconnect-network
clickhouse:
image: yandex/clickhouse-server:19.11
healthcheck:
test: ["CMD", "clickhouse-client", "--query", "SELECT 1"]
start_period: 3s
interval: 1s
timeout: 3s
retries: 10
networks:
- test-dbconnect-network
-265
View File
@@ -1,265 +0,0 @@
package dbconnect_tests
import (
"context"
"log"
"net/url"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/cloudflare/cloudflared/dbconnect"
)
func TestIntegrationPostgreSQL(t *testing.T) {
ctx, pq := helperNewSQLClient(t, "POSTGRESQL_URL")
err := pq.Ping(ctx)
assert.NoError(t, err)
_, err = pq.Submit(ctx, &dbconnect.Command{
Statement: "CREATE TABLE t (a TEXT, b UUID, c JSON, d INET[], e SERIAL);",
Mode: "exec",
})
assert.NoError(t, err)
_, err = pq.Submit(ctx, &dbconnect.Command{
Statement: "INSERT INTO t VALUES ($1, $2, $3, $4);",
Mode: "exec",
Arguments: dbconnect.Arguments{
Positional: []interface{}{
"text",
"6b8d686d-bd8e-43bc-b09a-cfcbbe702c10",
"{\"bool\":true,\"array\":[\"a\", 1, 3.14],\"embed\":{\"num\":21}}",
[]string{"1.1.1.1", "1.0.0.1"},
},
},
})
assert.NoError(t, err)
_, err = pq.Submit(ctx, &dbconnect.Command{
Statement: "UPDATE t SET b = NULL;",
Mode: "exec",
})
assert.NoError(t, err)
res, err := pq.Submit(ctx, &dbconnect.Command{
Statement: "SELECT * FROM t;",
Mode: "query",
})
assert.NoError(t, err)
assert.IsType(t, make([]map[string]interface{}, 0), res)
actual := res.([]map[string]interface{})[0]
expected := map[string]interface{}{
"a": "text",
"b": nil,
"c": map[string]interface{}{
"bool": true,
"array": []interface{}{"a", float64(1), 3.14},
"embed": map[string]interface{}{"num": float64(21)},
},
"d": "{1.1.1.1,1.0.0.1}",
"e": int64(1),
}
assert.EqualValues(t, expected, actual)
_, err = pq.Submit(ctx, &dbconnect.Command{
Statement: "DROP TABLE t;",
Mode: "exec",
})
assert.NoError(t, err)
}
func TestIntegrationMySQL(t *testing.T) {
ctx, my := helperNewSQLClient(t, "MYSQL_URL")
err := my.Ping(ctx)
assert.NoError(t, err)
_, err = my.Submit(ctx, &dbconnect.Command{
Statement: "CREATE TABLE t (a CHAR, b TINYINT, c FLOAT, d JSON, e YEAR);",
Mode: "exec",
})
assert.NoError(t, err)
_, err = my.Submit(ctx, &dbconnect.Command{
Statement: "INSERT INTO t VALUES (?, ?, ?, ?, ?);",
Mode: "exec",
Arguments: dbconnect.Arguments{
Positional: []interface{}{
"a",
10,
3.14,
"{\"bool\":true}",
2000,
},
},
})
assert.NoError(t, err)
_, err = my.Submit(ctx, &dbconnect.Command{
Statement: "ALTER TABLE t ADD COLUMN f GEOMETRY;",
Mode: "exec",
})
assert.NoError(t, err)
res, err := my.Submit(ctx, &dbconnect.Command{
Statement: "SELECT * FROM t;",
Mode: "query",
})
assert.NoError(t, err)
assert.IsType(t, make([]map[string]interface{}, 0), res)
actual := res.([]map[string]interface{})[0]
expected := map[string]interface{}{
"a": "a",
"b": float64(10),
"c": 3.14,
"d": map[string]interface{}{"bool": true},
"e": float64(2000),
"f": nil,
}
assert.EqualValues(t, expected, actual)
_, err = my.Submit(ctx, &dbconnect.Command{
Statement: "DROP TABLE t;",
Mode: "exec",
})
assert.NoError(t, err)
}
func TestIntegrationMSSQL(t *testing.T) {
ctx, ms := helperNewSQLClient(t, "MSSQL_URL")
err := ms.Ping(ctx)
assert.NoError(t, err)
_, err = ms.Submit(ctx, &dbconnect.Command{
Statement: "CREATE TABLE t (a BIT, b DECIMAL, c MONEY, d TEXT);",
Mode: "exec"})
assert.NoError(t, err)
_, err = ms.Submit(ctx, &dbconnect.Command{
Statement: "INSERT INTO t VALUES (?, ?, ?, ?);",
Mode: "exec",
Arguments: dbconnect.Arguments{
Positional: []interface{}{
0,
3,
"$0.99",
"text",
},
},
})
assert.NoError(t, err)
_, err = ms.Submit(ctx, &dbconnect.Command{
Statement: "UPDATE t SET d = NULL;",
Mode: "exec",
})
assert.NoError(t, err)
res, err := ms.Submit(ctx, &dbconnect.Command{
Statement: "SELECT * FROM t;",
Mode: "query",
})
assert.NoError(t, err)
assert.IsType(t, make([]map[string]interface{}, 0), res)
actual := res.([]map[string]interface{})[0]
expected := map[string]interface{}{
"a": false,
"b": float64(3),
"c": float64(0.99),
"d": nil,
}
assert.EqualValues(t, expected, actual)
_, err = ms.Submit(ctx, &dbconnect.Command{
Statement: "DROP TABLE t;",
Mode: "exec",
})
assert.NoError(t, err)
}
func TestIntegrationClickhouse(t *testing.T) {
ctx, ch := helperNewSQLClient(t, "CLICKHOUSE_URL")
err := ch.Ping(ctx)
assert.NoError(t, err)
_, err = ch.Submit(ctx, &dbconnect.Command{
Statement: "CREATE TABLE t (a UUID, b String, c Float64, d UInt32, e Int16, f Array(Enum8('a'=1, 'b'=2, 'c'=3))) engine=Memory;",
Mode: "exec",
})
assert.NoError(t, err)
_, err = ch.Submit(ctx, &dbconnect.Command{
Statement: "INSERT INTO t VALUES (?, ?, ?, ?, ?, ?);",
Mode: "exec",
Arguments: dbconnect.Arguments{
Positional: []interface{}{
"ec65f626-6f50-4c86-9628-6314ef1edacd",
"",
3.14,
314,
-144,
[]string{"a", "b", "c"},
},
},
})
assert.NoError(t, err)
res, err := ch.Submit(ctx, &dbconnect.Command{
Statement: "SELECT * FROM t;",
Mode: "query",
})
assert.NoError(t, err)
assert.IsType(t, make([]map[string]interface{}, 0), res)
actual := res.([]map[string]interface{})[0]
expected := map[string]interface{}{
"a": "ec65f626-6f50-4c86-9628-6314ef1edacd",
"b": "",
"c": float64(3.14),
"d": uint32(314),
"e": int16(-144),
"f": []string{"a", "b", "c"},
}
assert.EqualValues(t, expected, actual)
_, err = ch.Submit(ctx, &dbconnect.Command{
Statement: "DROP TABLE t;",
Mode: "exec",
})
assert.NoError(t, err)
}
func helperNewSQLClient(t *testing.T, env string) (context.Context, dbconnect.Client) {
_, ok := os.LookupEnv("DBCONNECT_INTEGRATION_TEST")
if ok {
t.Helper()
} else {
t.SkipNow()
}
val, ok := os.LookupEnv(env)
if !ok {
log.Fatalf("must provide database url as environment variable: %s", env)
}
parsed, err := url.Parse(val)
if err != nil {
log.Fatalf("cannot provide invalid database url: %s=%s", env, val)
}
ctx := context.Background()
client, err := dbconnect.NewSQLClient(ctx, parsed)
if err != nil {
log.Fatalf("could not start test client: %s", err)
}
return ctx, client
}
+2 -1
View File
@@ -5,8 +5,9 @@ import (
"net"
"sync"
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
)
const (
Executable
+14
View File
@@ -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
+4 -15
View File
@@ -3,29 +3,21 @@ module github.com/cloudflare/cloudflared
go 1.15
require (
github.com/DATA-DOG/go-sqlmock v1.3.3
github.com/acmacalister/skittles v0.0.0-20160609003031-7423546701e1 // indirect
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect
github.com/aws/aws-sdk-go v1.34.19 // indirect
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect
github.com/cloudflare/brotli-go v0.0.0-20191101163834-d34379f7ff93
github.com/cloudflare/golibs v0.0.0-20170913112048-333127dbecfc
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/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0
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
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect
github.com/frankban/quicktest v1.10.0 // indirect
github.com/fsnotify/fsnotify v1.4.9
github.com/gdamore/tcell v1.3.0
github.com/getsentry/raven-go v0.0.0-20180517221441-ed7bcb39ff10
github.com/gliderlabs/ssh v0.0.0-20191009160644-63518b5243e0 // indirect
github.com/go-sql-driver/mysql v1.5.0
github.com/gobwas/httphead v0.0.0-20200921212729-da3d93bc3c58 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.0.4
@@ -34,28 +26,23 @@ require (
github.com/google/uuid v1.1.2
github.com/gorilla/mux v1.7.3
github.com/gorilla/websocket v1.4.2
github.com/jmoiron/sqlx v1.2.0
github.com/json-iterator/go v1.1.10
github.com/kr/text v0.2.0 // indirect
github.com/kshvakov/clickhouse v1.3.11
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lib/pq v1.2.0
github.com/mattn/go-colorable v0.1.8
github.com/mattn/go-sqlite3 v1.11.0
github.com/miekg/dns v1.1.31
github.com/mitchellh/go-homedir v1.1.0
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pierrec/lz4 v2.5.2+incompatible // indirect
github.com/pkg/errors v0.9.1
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/prometheus/client_golang v1.7.1
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
github.com/xo/dburl v0.0.0-20191005012637-293c3298d6c0
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
golang.org/x/net v0.0.0-20200904194848-62affa334b73
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 // indirect
@@ -72,3 +59,5 @@ require (
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.20210324024421-b6ea8234fe3d
+7 -45
View File
@@ -62,7 +62,6 @@ github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbt
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/DataDog/datadog-go v3.5.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/DataDog/zstd v1.3.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
@@ -75,8 +74,6 @@ github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWX
github.com/Shopify/sarama v1.21.0/go.mod h1:yuqtN/pe8cXRWG5zPaO7hCfNJp5MwmkoJEoLjkm5tCQ=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/acmacalister/skittles v0.0.0-20160609003031-7423546701e1 h1:RKnVV4C7qoN/sToLX2y1dqH7T6kKLMHcwRJlgwb9Ggk=
github.com/acmacalister/skittles v0.0.0-20160609003031-7423546701e1/go.mod h1:gI5CyA/CEnS6eqNV22rqs4dG3aGfaSbXgPORIlwr2r0=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.0/go.mod h1:zpDJeKyp9ScW4NNrbdr+Eyxvry3ilGPewKoXw3XGN1k=
github.com/alangpierce/go-forceexport v0.0.0-20160317203124-8f1d6941cd75/go.mod h1:uAXEEpARkRhCZfEvy/y0Jcc888f9tHCc1W7/UeEtreE=
@@ -84,12 +81,9 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
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 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
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=
@@ -100,7 +94,6 @@ github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQ
github.com/aws/aws-sdk-go v1.23.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.32.1/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/aws/aws-sdk-go v1.34.19/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@@ -108,8 +101,6 @@ 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/bkaradzic/go-lz4 v1.0.0 h1:RXc4wYsyz985CkXXeX04y4VnZFGG8Rd43pRaHsOXAKk=
github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4=
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=
@@ -133,8 +124,6 @@ github.com/cloudflare/brotli-go v0.0.0-20191101163834-d34379f7ff93/go.mod h1:QiT
github.com/cloudflare/cloudflare-go v0.10.2/go.mod h1:qhVI5MKwBGhdNU89ZRz2plgYutcJ5PCekLxXn56w6SY=
github.com/cloudflare/golibs v0.0.0-20170913112048-333127dbecfc h1:Dvk3ySBsOm5EviLx6VCyILnafPcQinXGP5jbTdHUJgE=
github.com/cloudflare/golibs v0.0.0-20170913112048-333127dbecfc/go.mod h1:HlgKKR8V5a1wroIDDIz3/A+T+9Janfq+7n1P5sEFdi0=
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 h1:F1EaeKL/ta07PY/k9Os/UFtwERei2/XzGemhpGnBKNg=
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
@@ -151,16 +140,15 @@ 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=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decker502/dnspod-go v0.2.0/go.mod h1:qsurYu1FgxcDwfSwXJdLt4kRsBLZeosEb9uq4Sy+08g=
github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0 h1:epsH3lb7KVbXHYk7LYGN5EiE0MxcevHU85CKITJ0wUY=
github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
github.com/dnaeon/go-vcr v0.0.0-20180814043457-aafff18a5cc2/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
@@ -199,8 +187,6 @@ github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjr
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
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/frankban/quicktest v1.10.0 h1:Gfh+GAJZOAoKZsIZeZbdn2JF10kN1XHNvjsvQK8gVkE=
github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
@@ -212,8 +198,6 @@ 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.0.0-20191009160644-63518b5243e0 h1:gF8ngtda767ddth2SH0YSAhswhz6qUkvyI9EZFYCWJA=
github.com/gliderlabs/ssh v0.0.0-20191009160644-63518b5243e0/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=
@@ -234,7 +218,6 @@ github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
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/gobwas/httphead v0.0.0-20200921212729-da3d93bc3c58 h1:YyrUZvJaU8Q0QsoVo+xLFBgWDTam29PKea6GYmwvSiQ=
@@ -253,8 +236,6 @@ github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4=
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -367,12 +348,11 @@ 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.20210324024421-b6ea8234fe3d h1:PRDnysJ9dF1vUMmEzBu6aHQeUluSQy4eWH3RsSSy/vI=
github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
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 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@@ -397,22 +377,16 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxv
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
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/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=
github.com/kshvakov/clickhouse v1.3.11 h1:dtzTJY0fCA+MWkLyuKZaNPkmSwdX4gh8+Klic9NB1Lw=
github.com/kshvakov/clickhouse v1.3.11/go.mod h1:/SVBAcqF3u7rxQ9sTWCZwf8jzzvxiZGeQvtmSF2BBEc=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/labbsr0x/bindman-dns-webhook v1.0.2/go.mod h1:p6b+VCXIR8NYKpDr8/dg1HKfQoRHCdcsROXKvmoehKA=
github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/linode/linodego v0.10.0/go.mod h1:cziNP7pbvE3mXIPneHj0oRY8L1WtGEIKlZ8LANE4eXA=
@@ -437,9 +411,6 @@ github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
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=
@@ -519,8 +490,6 @@ github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ=
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI=
github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -576,14 +545,14 @@ 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/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/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
@@ -619,15 +588,11 @@ 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/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
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=
github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xo/dburl v0.0.0-20191005012637-293c3298d6c0 h1:6DtWz8hNS4qbq0OCRPhdBMG9E2qKTSDKlwnP3dmZvuA=
github.com/xo/dburl v0.0.0-20191005012637-293c3298d6c0/go.mod h1:A47W3pdWONaZmXuLZgfKLAVgUY0qvfTRM5vVDKS40S4=
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=
@@ -657,7 +622,6 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/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-20190325154230-a5d413f7728c/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=
@@ -815,9 +779,6 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w
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-20200909081042-eff7692f9009 h1:W0lCpv29Hv0UaM1LXb9QlBHLNP8UFfcKjblhVCWftOM=
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
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=
@@ -1012,6 +973,7 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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=
+2 -1
View File
@@ -663,7 +663,8 @@ func AssertIfPipeReadable(t *testing.T, pipe io.ReadCloser) {
b := []byte{0}
n, err := pipe.Read(b)
if n > 0 {
t.Fatalf("read pipe was not empty")
t.Errorf("read pipe was not empty")
return
}
errC <- err
}()
+4 -4
View File
@@ -125,12 +125,12 @@ func IsWebsocketClientHeader(headerName string) bool {
headerName == "upgrade"
}
func H1ResponseToH2ResponseHeaders(h1 *http.Response) (h2 []Header) {
func H1ResponseToH2ResponseHeaders(status int, h1 http.Header) (h2 []Header) {
h2 = []Header{
{Name: ":status", Value: strconv.Itoa(h1.StatusCode)},
{Name: ":status", Value: strconv.Itoa(status)},
}
userHeaders := make(http.Header, len(h1.Header))
for header, values := range h1.Header {
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,
+3 -3
View File
@@ -579,7 +579,7 @@ func TestH1ResponseToH2ResponseHeaders(t *testing.T) {
Header: mockHeaders,
}
headers := H1ResponseToH2ResponseHeaders(&mockResponse)
headers := H1ResponseToH2ResponseHeaders(mockResponse.StatusCode, mockResponse.Header)
serializedHeadersIndex := -1
for i, header := range headers {
@@ -622,7 +622,7 @@ func TestHeaderSize(t *testing.T) {
Header: largeHeaders,
}
serializedHeaders := H1ResponseToH2ResponseHeaders(&mockResponse)
serializedHeaders := H1ResponseToH2ResponseHeaders(mockResponse.StatusCode, mockResponse.Header)
request, err := http.NewRequest(http.MethodGet, "https://example.com/", nil)
assert.NoError(t, err)
for _, header := range serializedHeaders {
@@ -669,6 +669,6 @@ func BenchmarkH1ResponseToH2ResponseHeaders(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = H1ResponseToH2ResponseHeaders(h1resp)
_ = H1ResponseToH2ResponseHeaders(h1resp.StatusCode, h1resp.Header)
}
}
+2 -1
View File
@@ -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"
)
+4 -2
View File
@@ -81,7 +81,8 @@ func TestSharedBufferConcurrentReadWrite(t *testing.T) {
expectedResult.Write(block[:blockSize])
n, err := b.Write(block[:blockSize])
if n != blockSize || err != nil {
t.Fatalf("write error: %d %s", n, err)
t.Errorf("write error: %d %s", n, err)
return
}
}
}
@@ -94,7 +95,8 @@ func TestSharedBufferConcurrentReadWrite(t *testing.T) {
for i := 0; i < 256; i++ {
n, err := io.ReadFull(b, block[:blockSize])
if n != blockSize || err != nil {
t.Fatalf("read error: %d %s", n, err)
t.Errorf("read error: %d %s", n, err)
return
}
actualResult.Write(block[:blockSize])
}
+61 -11
View File
@@ -9,11 +9,12 @@ import (
"strings"
"sync"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/ipaccess"
)
var (
@@ -24,6 +25,12 @@ var (
ErrURLIncompatibleWithIngress = errors.New("You can't set the --url flag (or $TUNNEL_URL) when using multiple-origin ingress rules")
)
const (
ServiceBastion = "bastion"
ServiceSocksProxy = "socks-proxy"
ServiceWarpRouting = "warp-routing"
)
// FindMatchingRule returns the index of the Ingress Rule which matches the given
// hostname and path. This function assumes the last rule matches everything,
// which is the case if the rules were instantiated via the ingress#Validate method
@@ -38,6 +45,7 @@ func (ing Ingress) FindMatchingRule(hostname, path string) (*Rule, int) {
return &rule, i
}
}
i := len(ing.Rules) - 1
return &ing.Rules[i], i
}
@@ -84,17 +92,35 @@ func NewSingleOrigin(c *cli.Context, allowURLFromArgs bool) (Ingress, error) {
return ing, err
}
// WarpRoutingService starts a tcp stream between the origin and requests from
// warp clients.
type WarpRoutingService struct {
Proxy StreamBasedOriginProxy
}
func NewWarpRoutingService() *WarpRoutingService {
return &WarpRoutingService{Proxy: &rawTCPService{name: ServiceWarpRouting}}
}
// 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
}
if c.IsSet("url") || c.IsSet(config.BastionFlag) {
if c.IsSet(config.BastionFlag) {
return newBastionService(), nil
}
if c.IsSet("url") {
originURL, err := config.ValidateUrl(c, allowURLFromArgs)
if err != nil {
return nil, errors.Wrap(err, "Error validating origin URL")
}
return &localService{URL: originURL, RootURL: originURL}, nil
if isHTTPService(originURL) {
return &httpService{
url: originURL,
}, nil
}
return newTCPOverWSService(originURL), nil
}
if c.IsSet("unix-socket") {
path, err := config.ValidateUnixSocket(c)
@@ -104,7 +130,7 @@ func parseSingleOriginService(c *cli.Context, allowURLFromArgs bool) (OriginServ
return &unixSocketPath{path: path}, nil
}
u, err := url.Parse("http://localhost:8080")
return &localService{URL: u, RootURL: u}, err
return &httpService{url: u}, err
}
// IsEmpty checks if there are any ingress rules.
@@ -136,7 +162,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
@@ -151,12 +177,29 @@ func validate(ingress []config.UnvalidatedIngressRule, defaults OriginRequestCon
service = &srv
} else if r.Service == "hello_world" || r.Service == "hello-world" || r.Service == "helloworld" {
service = new(helloWorld)
} else if r.Service == "bastion" || cfg.BastionMode {
} else if r.Service == ServiceSocksProxy {
rules := make([]ipaccess.Rule, len(r.OriginRequest.IPRules))
for i, ipRule := range r.OriginRequest.IPRules {
rule, err := ipaccess.NewRuleByCIDR(ipRule.Prefix, ipRule.Ports, ipRule.Allow)
if err != nil {
return Ingress{}, fmt.Errorf("unable to create ip rule for %s: %s", r.Service, err)
}
rules[i] = rule
}
accessPolicy, err := ipaccess.NewPolicy(false, rules)
if err != nil {
return Ingress{}, fmt.Errorf("unable to create ip access policy for %s: %s", r.Service, err)
}
service = newSocksProxyOverWSService(accessPolicy)
} else if r.Service == ServiceBastion || cfg.BastionMode {
// Bastion mode will always start a Websocket proxy server, which will
// overwrite the localService.URL field when `start` is called. So,
// leave the URL field empty for now.
cfg.BastionMode = true
service = new(localService)
service = newBastionService()
} else {
// Validate URL services
u, err := url.Parse(r.Service)
@@ -171,8 +214,11 @@ func validate(ingress []config.UnvalidatedIngressRule, defaults OriginRequestCon
if u.Path != "" {
return Ingress{}, fmt.Errorf("%s is an invalid address, ingress rules don't support proxying to a different path on the origin service. The path will be the same as the eyeball request's path", r.Service)
}
serviceURL := localService{URL: u}
service = &serviceURL
if isHTTPService(u) {
service = &httpService{url: u}
} else {
service = newTCPOverWSService(u)
}
}
if err := validateHostname(r, i, len(ingress)); err != nil {
@@ -241,3 +287,7 @@ func ParseIngress(conf *config.Configuration) (Ingress, error) {
}
return validate(conf.Ingress, originRequestFromYAML(conf.OriginRequest))
}
func isHTTPService(url *url.URL) bool {
return url.Scheme == "http" || url.Scheme == "https" || url.Scheme == "ws" || url.Scheme == "wss"
}

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