Compare commits

..

262 Commits

Author SHA1 Message Date
Dalton 12c615e2e6 Release 2020.6.4 2020-06-16 14:13:06 -05:00
Dalton 6e5ccd7c85 AUTH-2815 add the log file to support the config.yaml file
added small delay to handle the possiblity of the server not being started yet
2020-06-16 17:48:12 +00:00
Adam Chalmers 3ec500bdbb TUN-3084: Generate and store tunnel_secret value during tunnel creation 2020-06-16 11:45:27 -05:00
Igor Postelnik 8f75feac94 TUN-3085: Pass connection authentication information using TunnelAuth struct 2020-06-16 16:35:46 +00:00
Igor Postelnik 448a7798f7 TUN-3015: Add a new cap'n'proto RPC interface for connection registration as well as matching client and server implementations. The old interface extends the new one for backward compatibility. 2020-06-16 16:35:46 +00:00
cthuang dc3a228d51 Release 2020.6.3 2020-06-16 08:45:03 +08:00
Dalton 554c97a8cb AUTH-2813 adds back a single file support a cloudflared log file 2020-06-16 00:43:09 +00:00
Robert McNeil fd1941dfbe DEVTOOLS-7321: Add openssh-client pkg for missing ssh-keyscan 2020-06-15 17:08:10 -07:00
Adam Chalmers 5fd348dcfc Release 2020.6.2 2020-06-15 16:40:44 +00:00
Dalton 1a6403b2fd AUTH-2694 added destination header support to config file 2020-06-15 10:10:22 -05:00
Dalton 55acf7283c AUTH-2810 added warn for backwards compatibility sake 2020-06-12 22:15:28 +00:00
Adam Chalmers acb7d604fd TUN-3038: Add connections to tunnel list table 2020-06-12 11:43:06 -05:00
Adam Chalmers 6e761cb7ae TUN-3090: Upgrade crypto dep 2020-06-12 00:03:09 -05:00
Dalton ae8d784e36 AUTH-2763 don't redirect from curl command 2020-06-11 15:38:11 -05:00
Dalton c716dd273c AUTH-2648 updated usage text 2020-06-11 11:08:05 -05:00
Dalton eb3c4a7a9f Release 2020.6.1 2020-06-09 15:33:32 -05:00
Dalton 97a901a229 AUTH-2796 fixed windows build 2020-06-09 19:49:13 +00:00
Dalton e054c5bb7b Release 2020.6.0 2020-06-09 11:44:09 -05:00
Dalton 0d87279b2f AUTH-2785 service token flag fix and logger fix 2020-06-09 11:00:56 -05:00
Dalton f8638839c0 AUTH-2729 added log file and level to cmd flags to match config file settings 2020-06-08 19:42:34 +00:00
Dalton 2f70b05c64 AUTH-2169 make access login page more generic 2020-06-08 11:20:30 -05:00
Robert McNeil e02d09a731 DEVTOOLS-7321: Push to a test branch instead of to master 2020-06-08 15:44:28 +00:00
Robert McNeil 0ff491905f DEVTOOLS-7321: Use SSH key from env for pushing to GitHub 2020-06-08 15:44:28 +00:00
Robert McNeil 8c59254488 DEVTOOLS-7321: Add scripts for macOS builds and homebrew uploads 2020-06-08 15:44:28 +00:00
Dalton 9e76e42e3c AUTH-2687 don't copy config unnecessarily 2020-06-08 15:24:36 +00:00
Dalton e376a13025 AUTH-2645 protect against user mistaken flag input 2020-06-05 15:10:09 -05:00
cthuang 3a086e9cc2 TUN-3020: Remove declarative tunnel related RPC code 2020-05-30 06:15:25 +08:00
cthuang fb82b2ced5 TUN-3019: Remove declarative tunnel entry code 2020-05-30 05:54:17 +08:00
Michael Fornaro be0514c5c9 Adding support for multi-architecture images and binaries (#184)
* Allow Dockerfile --build-args to override GOOS and GOARCH defaults

Allow Dockerfile --build-args to override GOOS and GOARCH defaults

Support building multi architecture binaries

remove default OS and ARCH to avoid tag confusion when compiling image through Makefile

Tag image with corrosponding OS and ARCH build variables

updating Makefile

Signed-off-by: Michael Fornaro <20387402+xUnholy@users.noreply.github.com>

* remove duplicate import on windows_service.go

Signed-off-by: Michael Fornaro <20387402+xUnholy@users.noreply.github.com>
2020-05-29 02:06:27 +01:00
Dalton 046be63253 AUTH-2596 added new logger package and replaced logrus 2020-05-27 17:07:19 -05:00
Igor Postelnik a908453aa4 TUN-2928, TUN-2929, TUN-2930: Add tunnel subcommands to interact with tunnel store service 2020-05-21 15:36:49 -05:00
Michael Borkenstein 7a77ead423 AUTH-2682: Create buster build 2020-05-20 11:59:55 -05:00
Michael Borkenstein b89cc22896 AUTH-2369: RDP Bastion prototype 2020-05-19 21:10:50 -05:00
Michael Borkenstein 6a7418e1af AUTH-2686: Added error handling to tunnel subcommand 2020-05-18 15:36:25 -05:00
Dalton df3ad2b223 AUTH-2529 added deprecation text to db-connect command 2020-05-14 10:26:09 -05:00
Dalton 8c870c19a6 AUTH-2505 added aliases 2020-05-13 15:15:08 -05:00
Igor Postelnik 83a1dc93d8 Release 2020.5.1 2020-05-11 16:12:19 -05:00
Dalton 2b7fbbb7b7 AUTH-2588 add DoH to service mode 2020-05-11 17:09:16 +00:00
Michael Borkenstein 2c878c47ed AUTH-2564: error handling and minor fixes 2020-05-11 15:42:35 +00:00
Igor Postelnik 8cc69f2a95 TUN-2860: Enable quick reconnect feature by default 2020-05-07 14:41:55 -05:00
cthuang 7bb9e0af4c Release 2020.5.0 2020-05-06 11:15:29 +08:00
Igor Postelnik fbe2989f61 TUN-2955: Fix connection and goroutine leaks when tunnel conection is terminated on error. Only unregister tunnels that had connected successfully. Close edge connection used to unregister the tunnel. Use buffered channels for error channels where receiver may quit early on context cancellation. 2020-05-06 03:13:24 +00:00
cthuang c3fa4552aa TUN-2872: Exit with non-0 status code when the binary is updated so launchd will restart the service 2020-05-06 05:53:14 +08:00
Adam Chalmers 2cf327ba01 TUN-2943: Copy certutil from edge into cloudflared 2020-05-04 17:37:29 -05:00
Igor Postelnik dd0881f32b TUN-2940: Added delay parameter to stdin reconnect command. 2020-05-01 15:58:19 +00:00
Dalton 41c358147c AUTH-2587 add config watcher and reload logic for access client forwarder 2020-04-29 11:07:35 -05:00
Igor Postelnik 976eb24883 TUN-2921: Rework address selection logic to avoid corner cases 2020-04-27 14:47:47 -05:00
Dalton c782716e49 Release 2020.4.0 2020-04-14 13:00:38 -05:00
Austin Cherry f18209af7d ARES-899: Fixes DoH client as system resolver. Fixes #91 2020-04-14 12:37:59 -05:00
Elvin Tan 6d63f84a75 AUTH-2235 GetTokenIfExists now parses JWT payload for json expiry field to detect if the cached access token is expired 2020-04-14 15:29:30 +00:00
Areg Harutyunyan 1c6ea36e73 TUN-2894: ResponseMetaHeader should be public 2020-04-11 01:01:05 +01:00
Areg Harutyunyan 06f29306cd TUN-2881: Parameterize response meta information header name in the generating function 2020-04-10 20:26:09 +01:00
Areg Harutyunyan 322f909edb TUN-2880: Return metadata about source of the response from cloudflared 2020-04-10 01:01:38 +01:00
Dalton a37da2b165 AUTH-2394 added socks5 proxy 2020-04-07 13:30:28 -05:00
Areg Harutyunyan 710f66b0bb Release 2020.3.2 2020-03-31 17:56:07 +01:00
Areg Harutyunyan 0b2b6c8e12 TUN-2850: Tunnel stripping Cloudflare headers 2020-03-31 16:52:13 +00:00
Adam Chalmers acea15161c TUN-2854: Quick Reconnects should be an optional supported feature 2020-03-31 08:59:00 -05:00
Areg Harutyunyan c76283a2b4 Release 2020.3.1 2020-03-27 17:20:31 +00:00
Areg Harutyunyan ae374c0463 TUN-2846: Trigger debug reconnects from stdin commands, not SIGUSR1 2020-03-27 17:04:21 +00:00
Areg Harutyunyan 42246f986c Release 2020.3.0 2020-03-26 15:17:01 +00:00
Areg Harutyunyan 80f387214c TUN-2796: Implement HTTP2 CONTINUATION headers correctly 2020-03-24 13:46:17 +00:00
Dalton a368fbbe9b AUTH-2394 fixed header for websockets. Added TCP alias 2020-03-23 10:27:53 -05:00
Adam Chalmers 32df01a9da TUN-2823: Bugfix. cloudflared would hang forever if error occurred. 2020-03-19 17:48:42 -05:00
Adam Chalmers 6dcf3a4cbc TUN-2819: cloudflared should close its connections when a signal is sent 2020-03-19 21:02:15 +00:00
Areg Harutyunyan 96f11de7ab TUN-2820: Serialized headers for Websockets 2020-03-19 18:08:58 +00:00
Adam Chalmers d03469b6d3 TUN-2816: cloudflared metrics server should be more discoverable 2020-03-18 09:53:24 -05:00
Adam Chalmers 0cf6ce9aeb TUN-2812: Tunnel proxies and RPCs can share an edge address 2020-03-16 15:50:57 -05:00
Adam Chalmers e8f55cc911 TUN-2756: Set connection digest after reconnect.
Previously it was only being set after RegisterTunnel.
2020-03-13 17:31:05 -05:00
Adam Chalmers 35cee13175 TUN-2807: cloudflared hello-world shouldn't assume it's my first tunnel 2020-03-13 12:09:21 -05:00
Adam Chalmers 5376df5439 TUN-2788: cloudflared should store one ConnDigest per HA connection 2020-03-11 18:19:22 -05:00
Adam Chalmers db9b6541d0 TUN-2797: Fix panic in SetConnDigest by making mutexes values. 2020-03-10 12:21:02 -05:00
Areg Harutyunyan 5bd4028ea7 TUN-2761: Use the new header management functions in cloudflared 2020-03-08 03:38:54 +00:00
Adam Chalmers 1b2a96f96b TUN-2755: ReconnectTunnel RPC now transmits ConnectionDigest 2020-03-06 14:48:16 -06:00
Adam Chalmers d50fee4fa0 TUN-2754: Add ConnDigest to cloudflared and its RPCs 2020-03-05 15:25:15 -06:00
Adam Chalmers 6b3e2b020b TUN-2785: Use reconnect token by default 2020-03-05 16:12:49 +00:00
Areg Harutyunyan 6624a24040 TUN-2748: Insecure randomness vulnerability in github.com/miekg/dns 2020-03-05 13:52:45 +00:00
Nick Vollmar 7b81cf8aa6 TUN-2779: update sample HTML pages 2020-03-03 19:35:41 +00:00
Areg Harutyunyan 26f5f80811 TUN-2776: Add header serialization feature in cloudflared 2020-02-28 17:36:29 +00:00
Roman Iuvshyn 29f4650e25 do not terminate tunnel if origin is not reachable on start-up (#177) 2020-02-27 23:03:00 +00:00
Areg Harutyunyan a14aa0322c TUN-2767: Test for large headers 2020-02-27 22:15:04 +00:00
Areg Harutyunyan 8f9bbcb9a0 Release 2020.2.1 2020-02-27 16:02:52 +00:00
Areg Harutyunyan afc2cd38e1 TUN-2765: Add list of features to tunnelrpc 2020-02-25 23:45:48 +00:00
Cameron Steel a5f67091bf Tweak HTTP host header. Fixes #107 (#168) 2020-02-24 17:08:14 +00:00
Rueian 464bb53049 perf(cloudflared): reuse memory from buffer pool to get better throughput (#161)
* perf(cloudflared): reuse memory from buffer pool to get better throughput

https://github.com/cloudflare/cloudflared/issues/160
2020-02-24 17:06:19 +00:00
Areg Harutyunyan 6488843ac4 TUN-2746: Add the new header management functions 2020-02-21 18:37:38 +00:00
Areg Harutyunyan 52ab2c8227 TUN-2745: Rename existing header management functions 2020-02-20 20:31:34 +00:00
Adam Chalmers 269351bbea TUN-2703: Muxer.Serve terminates when its context is Done 2020-02-20 20:09:10 +00:00
Adam Chalmers a83b6a2155 TUN-2725: Specify in code that --edge is for internal testing only 2020-02-19 16:18:48 -06:00
Adam Chalmers a60c0273f5 TUN-2714: New edge discovery. Connections try to reconnect to the same edge IP. 2020-02-14 19:49:54 +00:00
Areg Harutyunyan d6c2c4ee4a TUN-2717: Function to serialize/deserialize HTTP headers 2020-02-10 19:18:06 +00:00
Adam Chalmers 5b1bea7892 Release 2020.2.0 2020-02-06 16:48:02 -06:00
Nick Vollmar 54b386188a TUN-2651: Fix panic in h2mux reader when a stream error is encountered 2020-01-31 09:59:10 -06:00
Adam Chalmers 386b02355a TUN-2707: Inconsistent cardinality in tunnel error metrics 2020-01-29 12:42:55 -06:00
Adam Chalmers 203b939614 TUN-2690: cloudflared reconnect uses wrong context 2020-01-28 22:26:27 +00:00
Adam Chalmers e729dfc51e TUN-2699: Metrics for Authenticate RPCs 2020-01-28 14:33:41 -06:00
Adam Chalmers d5139d3882 TUN-2696: Add unknown registerRPCName 2020-01-28 11:29:33 -06:00
Adam Chalmers e31ff3a70f TUN-2693: Metrics for ReconnectTunnel 2020-01-28 10:46:37 -06:00
Adam Chalmers dfe61fda88 TUN-2645: Revert "TUN-2645: Turn on reconnect tokens"
This reverts commit 053b2c17f1.
2020-01-27 14:59:07 -06:00
Adam Chalmers 053b2c17f1 TUN-2645: Turn on reconnect tokens 2020-01-13 15:23:42 -06:00
Nick Vollmar 7367827a11 TUN-2646: Make --edge flag work again for local development 2019-12-23 23:11:00 -06:00
Areg Harutyunyan 7d7bdffde5 Release 2019.12.0 2019-12-20 02:03:08 +04:00
Nick Vollmar 7e31b77646 TUN-2637: Manage edge IPs in a region-aware manner 2019-12-19 21:58:23 +00:00
Tyler Cook 87102a2646 Fix timer scheduling for systemd update service (#159) 2019-12-19 20:53:06 +04:00
Niels Hofmans 789ca6f6f4 refactor(docker): optimize Dockerfile (#126)
* refactor(docker): optimize Dockerfile

Remove obsolete upx binary compression
Run as unprivileged user

* Use go 1.13.3

* Use debian buster distroless
2019-12-17 05:25:17 +04:00
Rueian cc2a1d1204 bug(cloudflared): Set the MaxIdleConnsPerHost of http.Transport to proxy-keepalive-connections (#155)
Setting the MaxIdleConns is not enough, the MaxIdleConnsPerHost must be set as well.
Otherwise, http.Transport will use the DefaultMaxIdleConnsPerHost, which is 2,
and then the connection pool will have only 2 connection hold.
2019-12-17 05:02:28 +04:00
Nick Vollmar 6aa48d2eb2 TUN-2554: cloudflared calls ReconnectTunnel 2019-12-13 18:48:48 +00:00
Nick Vollmar 8b43454024 TUN-2631: only notify that activeStreamMap is closed if ignoreNewStreams=true 2019-12-11 17:05:28 -06:00
Nick Vollmar 5e7ca14412 TUN-2555: origin/supervisor.go calls Authenticate 2019-12-06 11:26:54 -06:00
Nick Vollmar b499c0fdba TUN-2608: h2mux.Muxer.Shutdown always returns a non-nil channel 2019-12-04 17:21:30 +00:00
Nick Vollmar bbf31377c2 TUN-2607: add RPC stream helpers 2019-12-03 15:38:16 -06:00
Nick Vollmar 8f4fd70783 TUN-2606: add DialEdge helpers 2019-12-03 14:52:33 -06:00
Tim Bart 92736b2677 bug(cloudflared): nil pointer deference on h2DictWriter Close() (#154)
Unlike other h2DictWriter methods, the Close() method does check whether
w.comp is nil.

This PR adds a check for non nil compressor before attempting to close

Bug: #141
2019-12-03 16:29:40 +04:00
Adam Chalmers 379cb16efe TUN-2591: ReconnectTunnel now sends EventDigest 2019-11-25 18:11:50 +00:00
Ashcon Partovi 43babbc2f9 Fix "happy eyeballs" not being disabled since Golang 1.12 upgrade
* The Dialer.DualStack setting is now ignored and deprecated; RFC 6555 Fast Fallback ("Happy Eyeballs") is now enabled by default. To disable, set Dialer.FallbackDelay to a negative value.
2019-11-25 17:54:20 +00:00
Nick Vollmar dfd1ca5fb5 Merge remote-tracking branch 'github/master' 2019-11-22 13:07:40 -06:00
Adam Chalmers f51712bef9 TUN-2582: EventDigest field in tunnelrpc 2019-11-22 12:04:14 -06:00
Chung-Ting Huang b0d31a0ef3 TUN-2573: Refactor TunnelRegistration into PermanentRegistrationError, RetryableRegistrationError and SuccessfulTunnelRegistration 2019-11-22 17:30:41 +00:00
Khaled Elkhawaga dd614881b6 Fix Docker build failure (#149)
* Enables module-mode in Docker to fix build

* Statically compile binary in Docker to fix missing dependency errors

* Ensure target OS is set to Linux for Docker builds
2019-11-22 20:27:28 +04:00
Adam Chalmers 23e12cf5a3 TUN-2575: Constructors + simpler conversions for AuthOutcome 2019-11-22 02:08:57 +00:00
Chung-Ting Huang 9605f00c77 Release 2019.11.3 2019-11-20 16:33:38 -06:00
Chung-Ting Huang 871c3a194f TUN-2562: Update Cloudflare Origin CA RSA root 2019-11-20 15:37:16 -06:00
Chung-Ting Huang 8ee1faf317 Release 2019.11.2 2019-11-19 15:16:15 -06:00
Chung-Ting Huang c5bacf4d95 TUN-2563: Exposes config_version metrics 2019-11-19 20:42:41 +00:00
Nick Vollmar 1ba5abfdb3 Release 2019.11.1 2019-11-19 19:56:25 +00:00
Adam Chalmers 7173da9359 TUN-2567: AuthOutcome can be turned back into AuthResponse 2019-11-19 04:34:00 +00:00
Nick Vollmar 0676923d24 TUN-2551: TunnelRPC definitions for ReconnectTunnel flow 2019-11-18 14:26:58 -06:00
Adam Chalmers ca7fbf43da TUN-2547: TunnelRPC definitions for Authenticate flow 2019-11-18 16:39:01 +00:00
Nick Vollmar 6ea9b5c3ff TUN-2490: respect original representation of HTTP request path 2019-11-14 22:56:07 -06:00
Nick Vollmar 1d96bccc04 TUN-2178: public API to create new h2mux.MuxedStreamRequest 2019-11-13 14:04:19 -06:00
Adam Chalmers c5af83af66 TUN-2053: Add a /healthcheck endpoint to the metrics server 2019-11-12 16:55:40 -06:00
Ashcon Partovi 759cd019be Add db-connect, a SQL over HTTPS server 2019-11-12 20:34:39 +00:00
Chung-Ting Huang 13bf65ce4e TUN-2506: Expose active streams metrics 2019-11-07 14:09:31 -06:00
Chung-Ting Huang 3a2e12818f Release 2019.11.0 2019-11-07 10:11:56 -06:00
Nick Vollmar e14ec1a1fb TUN-2505: Terminate stream on receipt of RST_STREAM; MuxedStream.CloseWrite() should terminate the MuxedStream.Write() loop 2019-11-06 21:24:18 +00:00
Chung-Ting Huang 3a9a0a0d75 TUN-2489: Delete stream from activestreammap when read and write are both closed 2019-11-05 11:06:11 -06:00
Areg Harutyunyan 068b148e05 TUN-2500: Don't send client registration errors to Sentry 2019-11-05 12:29:11 +00:00
Nick Vollmar e5335b6c1b TUN-2502: Switch to go modules 2019-11-04 15:05:02 -06:00
Chung-Ting Huang fa841fc89a TUN-2451: Log inavlid path 2019-10-30 14:07:28 -05:00
Michael Borkenstein e9a8c9850f AUTH-2177: Reads and writes error streams 2019-10-25 19:44:14 +00:00
Chung-Ting Huang a52f47c9a9 TUN-2460: Configure according to the ClientConfig recevied from a successful Connect 2019-10-24 14:53:14 +00:00
Michael Borkenstein ad9559c66a AUTH-2173: Prepends access login url with scheme if one doesnt exist 2019-10-23 20:35:12 +00:00
Chung-Ting Huang 7133eceb9b TUN-2425: Enable cloudflared to serve multiple Hello World servers by having each of them create its own ServeMux 2019-10-22 10:09:59 -05:00
Areg Harutyunyan e0c2758b64 Release 2019.10.4 2019-10-21 14:48:22 -05:00
Areg Harutyunyan 5464408ea7 TUN-2450: Remove Brew publishing formula 2019-10-21 13:44:49 -05:00
Areg Harutyunyan 802e538c42 Release 2019.10.3 2019-10-21 10:20:34 -05:00
Areg Harutyunyan 79065514ff Merge remote-tracking branch 'upstream/master' 2019-10-18 15:33:27 -05:00
Michael Borkenstein 51acf2ef08 Release 2019.10.2 2019-10-18 13:11:07 -05:00
Felix Bünemann 1f6a330098 Fix #129: Excessive memory usage streaming large files (#142)
This drops the default size auf the h2mux write buffer from 512 MB to 1 MB.
This massively reduces memory usage, since each stream has its own buffer.
2019-10-17 17:15:51 -05:00
Michael Borkenstein 28cc1c65af AUTH-2167: Adds CLI option for host key directory 2019-10-17 16:31:43 -05:00
Michael Borkenstein 6322c5029d Release 2019.10.1 2019-10-17 13:17:42 -05:00
Michael Borkenstein 4bb5b97518 Adds variable to fix windows build 2019-10-17 18:15:59 +00:00
Michael Borkenstein 99daa76715 Release 2019.10.0 2019-10-17 10:58:33 -05:00
Michael Borkenstein babe684141 AUTH-2135: Adds support for IPv6 and tests 2019-10-16 16:06:07 -05:00
Michael Borkenstein 8b6e3bc1d1 AUTH-2159: Moves shutdownC close into error handling
AUTH-2161: Lowers size of preamble length
AUTH-2160: Fixes url parsing logic
2019-10-16 11:41:51 -05:00
Michael Borkenstein 95704b11fb AUTH-2114: Uses short lived cert auth for outgoing client connection 2019-10-15 14:35:15 -05:00
Nick Vollmar 4d2583edf5 TUN-2344: log more details: http2.Framer.ErrorDetail() if available, connectionID 2019-10-15 10:59:24 -05:00
Michael Borkenstein a4b3ee5959 AUTH-2105: Dont require --destination arg 2019-10-11 12:26:23 -05:00
Michael Borkenstein 91d9dca34e AUTH-2105: Adds support for local forwarding. Refactor auditlogger creation.
AUTH-2088: Adds dynamic destination routing
2019-10-10 15:25:03 -05:00
Michael Borkenstein dbde3870da AUTH-2089: Revise ssh server to function as a proxy 2019-10-07 13:04:04 -05:00
Chung-Ting Huang b3bcce97da Release 2019.9.2 2019-09-26 14:07:22 -05:00
Areg Harutyunyan a99fac1e31 TUN-2355: Roll back TUN-2276
This reverts commit 02f0ed951f.
2019-09-26 14:02:01 -05:00
Michael Borkenstein 71d66ae7ee Release 2019.9.1 2019-09-26 10:18:28 -05:00
Michael Borkenstein 133e6fdc88 AUTH-2077: Quotes open browser command in windows 2019-09-24 18:27:37 +00:00
Michael Borkenstein 1d5cc45ac7 AUTH-2055: Verifies token at edge on access login 2019-09-24 18:22:33 +00:00
Nick Vollmar a412f629c2 TUN-2334: remove tlsConfig.ServerName special case 2019-09-24 15:00:55 +00:00
Michael Borkenstein 979e5be8ab AUTH-2067: Log commands correctly 2019-09-23 20:42:41 +00:00
Michael Borkenstein 2789d0cf36 AUTH-2052: Adds tests for SSH server 2019-09-23 09:19:43 -05:00
Chung-Ting Huang 5bcb2da0fe TUN-2309: Split ConnectResult into ConnectError and ConnectSuccess, each implementing its own capnp serialization logic 2019-09-19 13:47:41 -05:00
Adam Chalmers 4f23da2a6d TUN-2315: Replace Scope with IntentLabel 2019-09-18 15:11:46 -05:00
Chung-Ting Huang fe032843f3 TUN-2307: Capnp is the only serialization format used in tunnelpogs 2019-09-17 16:29:50 +00:00
Michael Borkenstein ff795a7beb AUTH-2056: Writes stderr to its own stream for non-pty connections 2019-09-16 14:43:05 -05:00
Michael Borkenstein 40d9370bb6 AUTH-2050: Adds time.sleep to temporarily avoid hitting tunnel muxer dealock issue 2019-09-12 14:10:34 +00:00
Areg Harutyunyan 02f0ed951f TUN-2276: Path encoding broken 2019-09-11 19:28:23 +00:00
Michael Borkenstein c2a71c5a51 AUTH-2037: Adds support for ssh port forwarding 2019-09-11 10:41:09 -05:00
Dalton 945bf76897 AUTH-1942 added event log to ssh server 2019-09-10 10:16:32 -05:00
Michael Borkenstein d3b254f9ae AUTH-2036: Refactor user retrieval, shutdown after ssh server stops, add custom version string 2019-09-09 17:31:23 +00:00
Dalton ee588eeeaa AUTH-1943 hooked up uploader to logger, added timestamp to session logs, add tests 2019-09-06 15:57:32 -05:00
Adam Chalmers dd521aba29 TUN-2280: Revert "TUN-2260: add name/group to CapnpConnectParameters, remove Scope"
This reverts commit 817c3be9da5465043c2a2fda6c48f7ada760682e.
2019-09-06 15:59:32 +00:00
Adam Chalmers a06390a078 TUN-2201: change SRV records used by cloudflared
This changes cloudflarewarp.com to argotunnel.com and _warp to
_origintunneld. We've changed which zone we host the SRV records
for Argo Tunnel on.
2019-09-06 15:01:58 +00:00
Areg Harutyunyan 2cac1f0f78 Release 2019.9.0 2019-09-05 22:15:19 -05:00
Areg Harutyunyan a10785a701 TUN-2279: Revert path encoding fix
This reverts commit f6c7f8a96c.
2019-09-05 22:03:31 -05:00
Nick Vollmar dc730615f2 TUN-2260: add name/group to CapnpConnectParameters, remove Scope 2019-09-05 15:36:16 +00:00
Nick Vollmar b836cb350a TUN-2162: Decomplect OpenStream to allow finer-grained timeouts 2019-09-05 15:23:41 +00:00
Austin Cherry 5e85a8bd16 AUTH-1943: Adds session logging 2019-09-03 13:54:29 -05:00
Dalton f41121f4e5 AUTH-2021 - check error for failing tests 2019-08-30 15:20:49 -05:00
Michael Borkenstein 93a9504824 AUTH-2026: Adds support for non-pty sessions and inline command exec 2019-08-30 14:26:53 -05:00
Michael Borkenstein 7abbe91d41 AUTH-2030: Support both authorized_key and short lived cert authentication simultaniously without specifiying at start time 2019-08-30 19:23:10 +00:00
Areg Harutyunyan cf314ddb58 Release 2019.8.4 2019-08-30 13:50:41 -05:00
Areg Harutyunyan ff97fb6dc8 Merge branch 'master' of github.com:cloudflare/cloudflared 2019-08-30 13:45:40 -05:00
David Barr dc48cdce1a Fix #111: Add support for specifying a specific HTTP Host: header on the origin. (#114) 2019-08-29 22:55:54 -05:00
Dalton f130e6d4d7 AUTH-2021 - s3 bucket uploading for SSH logs 2019-08-29 16:54:54 -05:00
Adam Chalmers ef5b44b2d0 TUN-1968: Gracefully diff StreamHandler.UpdateConfig 2019-08-29 16:51:23 +00:00
Michael Borkenstein 858ef29868 AUTH-2022: Adds ssh timeout configuration 2019-08-28 15:22:35 -05:00
Michael Borkenstein baec3e289e AUTH-2018: Adds support for authorized keys and short lived certs 2019-08-28 09:58:42 -05:00
Adam Chalmers df25ed9bde TUN-2244: Add NO_AUTOUPDATE env var 2019-08-27 15:53:28 -05:00
Adam Chalmers 4e1df1a211 TUN-2243: Revert "STOR-519: Add db-connect, a SQL over HTTPS server"
This reverts commit 5da2109811.
2019-08-26 16:50:12 -05:00
Michael Borkenstein c3c88cc31e AUTH-2014: Checks users login shell 2019-08-26 18:14:39 +00:00
Adam Chalmers fb8ff33203 TUN-2165: Add ClientConfig to tunnelrpc.ConnectResult 2019-08-26 16:27:28 +00:00
Michael Borkenstein 188f4667cb AUTH-2004: Adds static host key support 2019-08-26 10:29:13 -05:00
Austin Cherry 30c9e2af9b AUTH-1941: Adds initial SSH server implementation 2019-08-21 15:49:03 -05:00
Chung-Ting Huang 66e087a825 Release 2019.8.3 2019-08-21 11:15:36 -05:00
Michael Borkenstein 47254113ee Revert "AUTH-1941: Adds initial SSH server implementation"
This reverts commit e9c9bf3cbd.
2019-08-20 17:20:48 -05:00
Chung-Ting Huang 026f2d1cc9 Release 2019.8.2 2019-08-20 16:47:23 -05:00
Austin Cherry e9c9bf3cbd AUTH-1941: Adds initial SSH server implementation 2019-08-20 16:18:37 -05:00
Ashcon Partovi 5da2109811 STOR-519: Add db-connect, a SQL over HTTPS server 2019-08-20 13:13:29 -05:00
Chung-Ting Huang 28f6c2ed7c TUN-2163: Add GrapQLType method to Scope interface 2019-08-11 12:18:54 -05:00
Areg Harutyunyan f6c7f8a96c TUN-2152: Requests with a query in the URL are erroneously escaped 2019-08-06 17:47:00 -05:00
Areg Harutyunyan 1d73c2752e Release 2019.8.1 2019-08-06 16:40:02 -05:00
Areg Harutyunyan 5afbb3fd6d Revert "TUN-1736: Missing headers when passing an invalid path"
This reverts commit 3c93d9b300.
2019-08-06 16:35:41 -05:00
Chung-Ting Huang fd4ab314dc TUN-2111: Implement custom serialization logic for FallibleConfig and OriginConfig 2019-08-06 10:56:16 -05:00
Michael Borkenstein 993a9bc4b9 Release 2019.8.0 2019-08-06 09:49:49 -05:00
Michael Borkenstein 8d1ea7202a Merge branch 'mike/AUTH-1972-delete-token-lockfile' of ssh://bitbucket.cfdata.org:7999/tun/cloudflared 2019-08-06 08:12:02 -05:00
Michael Borkenstein 9adbab96af AUTH-1972: Deletes token lock file if backoff retry attempts exceeded and intercepts signals until lock is released 2019-08-06 08:07:48 -05:00
Chung-Ting Huang bdd70e798a TUN-2110: Implement custom deserialization logic for OriginConfig 2019-08-05 19:28:51 -05:00
Adam Chalmers 5feba7e3a9 TUN-2147: Implemented ScopeUnmarshaler 2019-08-05 18:14:32 -05:00
Adam Chalmers d3d9460f64 TUN-2125: Add PostgresType() to Scope 2019-08-02 10:21:09 -05:00
Nick Vollmar 74f3a55c57 TUN-2117: read group/system-name from CLI, send it to edge 2019-08-01 22:04:05 +00:00
Areg Harutyunyan 3c93d9b300 TUN-1736: Missing headers when passing an invalid path 2019-07-31 16:56:29 +00:00
Nick Vollmar 3c0ea0db65 TUN-1956: Go 1.12 update 2019-07-12 22:09:16 +00:00
Austin Cherry 7c03e2cbd2 Release 2019.7.0 2019-07-11 10:22:37 -05:00
Austin Cherry 8f25704a90 AUTH-1736: Better handling of token revocation
We removed all token validation from cloudflared and now rely on
the edge to do the validation. This is better because the edge is
the only thing that fully knows about token revocation. So if a user
logs out or the application revokes all it's tokens cloudflared will
now handle that process instead of barfing on it.

When we go to fetch a token we will check for the existence of a
lock file. If the lock file exists, we stop and poll every half
second to see if the lock is still there. Once the lock file is
removed, it will restart the function to (hopefully) go pick up
the valid token that was just created.
2019-07-10 21:35:46 +00:00
Areg Harutyunyan 583bad4972 Merge branch 'master' of ssh://stash.cfops.it:7999/tun/cloudflared 2019-07-10 11:46:13 -05:00
Areg Harutyunyan b5f340d64c Merge branch 'master' of github.com:cloudflare/cloudflared 2019-07-10 11:45:57 -05:00
Frank Denis 9cd0b7048a DoH: change the media type to application/dns-message
The media type for DoH was changed from `application/dns-udpwireformat` to `application/dns-message` in May 2018.

Ref: RFC8484 section 6.
2019-07-03 14:13:28 -05:00
Chung-Ting Huang 4858ce79d0 TUN-1977: Validate OriginConfig has valid URL, and use scheme to determine if a HTTPOriginService is expecting HTTP or Unix 2019-07-01 15:31:58 -05:00
Chung-Ting Huang 4090049fff TUN-1981: Write response header & body on proxy error to notify eyeballs of failure category 2019-07-01 20:20:59 +00:00
Adam Chalmers e255a7da26 TUN-2005: Upgrade logrus 2019-06-26 12:25:58 -05:00
Chung-Ting Huang c2a3ac3991 TUN-1982: Load custom origin CA when OriginCAPool is specified 2019-06-21 11:44:26 -05:00
Chung-Ting Huang 2fa09e1cc6 TUN-1976: Pass tunnel hostname through header 2019-06-21 10:43:06 -05:00
Chung-Ting Huang 0a742feb98 TUN-1885: Reconfigure cloudflared on receiving new ClientConfig 2019-06-20 19:07:59 -05:00
Chung-Ting Huang 80a15547e3 TUN-1961: Create EdgeConnectionManager to maintain outbound connections to the edge 2019-06-18 16:37:38 -05:00
Chung-Ting Huang d26a8c5d44 TUN-1893: Proxy requests to the origin based on tunnel hostname 2019-06-13 14:46:22 -05:00
Chung-Ting Huang ca619a97bc TUN-1952: Group ClientConfig fields by the component that uses the config, and return the part of the config that failed to be applied 2019-06-13 14:39:51 -05:00
Areg Harutyunyan 25a04e0c69 Merge branch 'master' of ssh://stash.cfops.it:7999/tun/cloudflared 2019-06-06 13:53:03 +04:00
Johan Bergström d3f9aa2ae7 Build a docker container
Use debian/distroless (glibc) as a base which in general gives
slightly higher performance than alpine (musl) while trading
off container size (about 10mb).

The build phase makes the assumption that any capnproto-files
are pre-processed before building in-docker.
2019-06-06 13:50:13 +04:00
Chung-Ting Huang d32fb8e82c TUN-1913: Define OriginService for each type of origin 2019-06-04 17:02:34 -05:00
Austin Cherry acd17f6ab6 Release 2019.6.0 2019-06-04 11:29:24 -05:00
Austin Cherry 1ca841d220 AUTH-1811: ssh-gen config fixes 2019-06-04 16:25:34 +00:00
Chung-Ting Huang 39d60d1239 TUN-1914: Conflate HTTP and Unix OriginConfig, and add TLS config to WebSocketOriginConfig 2019-06-03 12:09:24 -05:00
Austin Cherry 713a2d689e AUTH-1802: Fixed ssh-config templating 2019-05-30 15:25:08 +00:00
Areg Harutyunyan babcd9fe2b Merge branch 'master' of github.com:cloudflare/cloudflared 2019-05-29 15:40:52 -05:00
Christoph Blecker a1403fe968 Handle exit code on err
fixes #96.

This change checks the err returned from the StartServer function, and
if it exists, passes a non-zero error code through to the urfave/cli
framework. This should allow processes like launchd to detect if
cloudflared exited gracefully or with an error.
2019-05-29 12:59:19 -05:00
Nick Vollmar 1485ca0fc7 TUN-1828: Update declarative tunnel config struct 2019-05-28 14:02:47 -05:00
Chung-Ting Huang 4bff1ef9df Release 2019.5.0 2019-05-23 10:20:48 -05:00
Austin Cherry 25cfffd0d1 AUTH-1781: fixed race condition for short lived certs, doc required config 2019-05-23 10:17:43 -05:00
Chung-Ting Huang 4662e40068 TUN-1880: Save debug and warn level log to logfile 2019-05-22 11:05:24 -05:00
Austin Cherry 38d78f9844 AUTH-1706: fixes and testing 2019-05-21 13:14:15 -05:00
Nick Vollmar 6f8708d33c TUN-1847: Log a distinct message when OpenStream fails while waiting for response headers 2019-05-13 16:46:25 -05:00
Austin Cherry fa17b0200f AUTH-1557: Short Lived Certs 2019-05-07 11:21:11 -05:00
Chung-Ting Huang 14f50d0922 TUN-1807: Send cloudflared version in Connect RPC 2019-05-06 16:13:12 -05:00
Nick Vollmar 945320880a TUN-1786: Remove low-level Windows service logging 2019-04-30 11:00:35 -05:00
Chung-Ting Huang 809d2f3f28 TUN-1781: ServeStream should return early on error 2019-04-29 09:36:41 -05:00
Areg Harutyunyan 137928ecaf Release 2019.4.1 2019-04-19 16:42:51 -05:00
Nick Vollmar 8183af31e2 Merge pull request #142 in TUN/cloudflared from nvollmar/TUN-1669 to master
* commit '28f890a70157ac234368c4a6235cc99e9662ce9b':
  TUN-1669: Update license message in help text. Also fix test
2019-04-18 20:15:00 +00:00
Nick Vollmar 28f890a701 TUN-1669: Update license message in help text. Also fix test 2019-04-18 10:42:48 -05:00
Areg Harutyunyan bab7583a97 TUN-1734: Pin packages at exact versions 2019-04-17 18:16:56 -05:00
Adam Chalmers 2e2fa29637 TUN-1732: cloudflared metrics should track userHostnames 2019-04-16 16:02:45 -05:00
Chung-Ting Huang 2bef5dbe72 TUN-1682: Add context to OpenStream to prevent it from blocking indefinitely. 2019-04-11 13:44:42 -05:00
Nick Vollmar 13d25a52a9 TUN-1606: Define CloudflaredConfig RPC structure, interface for cloudflared's RPC server 2019-04-09 15:58:40 -05:00
Nick Vollmar 9a43a92b1c TUN-1577: decompose carrier.StartServer to make TestStartServer less flappy 2019-04-09 15:09:58 -05:00
Chung-Ting Huang 102b364cc9 TUN-1619: Add flag to test declarative tunnels. 2019-04-05 10:35:23 -05:00
Chung-Ting Huang c18702f297 TUN-1626: Create new supervisor to establish connection with origintunneld 2019-04-04 15:39:22 -05:00
1963 changed files with 609650 additions and 153916 deletions
+5
View File
@@ -9,3 +9,8 @@ guide/public
\#*\#
cscope.*
cloudflared
cloudflared.exe
!cmd/cloudflared/
.DS_Store
*-session.log
ssh_server_tests/.env
Vendored Executable
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
set -euo pipefail
if ! git describe --tags --exact-match 2>/dev/null ; then
echo "Skipping public release for an untagged commit."
echo "##teamcity[buildStatus status='SUCCESS' text='Skipped due to lack of tag']"
exit 0
fi
if [[ "$(uname)" != "Darwin" ]] ; then
echo "This should be run on macOS"
exit 1
fi
go version
export GO111MODULE=on
# build 'cloudflared-darwin-amd64.tgz'
mkdir -p artifacts
FILENAME="$(pwd)/artifacts/cloudflared-darwin-amd64.tgz"
export PATH="$PATH:/usr/local/bin"
mkdir -p ../src/github.com/cloudflare/
cp -r . ../src/github.com/cloudflare/cloudflared
cd ../src/github.com/cloudflare/cloudflared
GOCACHE="$PWD/../../../../" GOPATH="$PWD/../../../../" CGO_ENABLED=1 make cloudflared
tar czf "$FILENAME" cloudflared
Vendored Executable
+70
View File
@@ -0,0 +1,70 @@
#!/bin/bash
set -euo pipefail
FILENAME="${PWD}/artifacts/cloudflared-darwin-amd64.tgz"
if ! VERSION="$(git describe --tags --exact-match 2>/dev/null)" ; then
echo "Skipping public release for an untagged commit."
echo "##teamcity[buildStatus status='SUCCESS' text='Skipped due to lack of tag']"
exit 0
fi
if [[ ! -f "$FILENAME" ]] ; then
echo "Missing $FILENAME"
exit 1
fi
if [[ "${GITHUB_PRIVATE_KEY:-}" == "" ]] ; then
echo "Missing GITHUB_PRIVATE_KEY"
exit 1
fi
# upload to s3 bucket for use by Homebrew formula
s3cmd \
--acl-public --signature-v2 --access_key="$AWS_ACCESS_KEY_ID" --secret_key="$AWS_SECRET_ACCESS_KEY" --host-bucket="%(bucket)s.s3.cfdata.org" \
put "$FILENAME" "s3://cftunnel-docs/dl/cloudflared-$VERSION-darwin-amd64.tgz"
s3cmd \
--acl-public --signature-v2 --access_key="$AWS_ACCESS_KEY_ID" --secret_key="$AWS_SECRET_ACCESS_KEY" --host-bucket="%(bucket)s.s3.cfdata.org" \
cp "s3://cftunnel-docs/dl/cloudflared-$VERSION-darwin-amd64.tgz" "s3://cftunnel-docs/dl/cloudflared-stable-darwin-amd64.tgz"
SHA256=$(sha256sum "$FILENAME" | cut -b1-64)
# set up git (note that UserKnownHostsFile is an absolute path so we can cd wherever)
mkdir -p tmp
ssh-keyscan -t rsa github.com > tmp/github.txt
echo "$GITHUB_PRIVATE_KEY" > tmp/private.key
chmod 0400 tmp/private.key
export GIT_SSH_COMMAND="ssh -o UserKnownHostsFile=$PWD/tmp/github.txt -i $PWD/tmp/private.key -o IdentitiesOnly=yes"
# clone Homebrew repo into tmp/homebrew-cloudflare
git clone git@github.com:cloudflare/homebrew-cloudflare.git tmp/homebrew-cloudflare
cd tmp/homebrew-cloudflare
git checkout -f master
git reset --hard origin/master
# modify cloudflared.rb
URL="https://developers.cloudflare.com/argo-tunnel/dl/cloudflared-$VERSION-darwin-amd64.tgz"
tee cloudflared.rb <<EOF
class Cloudflared < Formula
desc 'Argo Tunnel'
homepage 'https://developers.cloudflare.com/argo-tunnel/'
url '$URL'
sha256 '$SHA256'
version '$VERSION'
def install
bin.install 'cloudflared'
end
end
EOF
# push cloudflared.rb
git add cloudflared.rb
git diff
git config user.name "cloudflare-warp-bot"
git config user.email "warp-bot@cloudflare.com"
git commit -m "Release Argo Tunnel $VERSION"
#git push -v origin master
# don't push to master, push to some testing branch
git checkout -b deploy-test
git push -v origin deploy-test
+29
View File
@@ -0,0 +1,29 @@
# use a builder image for building cloudflare
ARG TARGET_GOOS
ARG TARGET_GOARCH
FROM golang:1.13.3 as builder
ENV GO111MODULE=on \
CGO_ENABLED=0 \
TARGET_GOOS=${TARGET_GOOS} \
TARGET_GOARCH=${TARGET_GOARCH}
WORKDIR /go/src/github.com/cloudflare/cloudflared/
# copy our sources into the builder image
COPY . .
# compile cloudflared
RUN make cloudflared
# use a distroless base image with glibc
FROM gcr.io/distroless/base-debian10:nonroot
# copy our compiled binary
COPY --from=builder --chown=nonroot /go/src/github.com/cloudflare/cloudflared/cloudflared /usr/local/bin/
# run as non-privileged user
USER nonroot
# command / entrypoint of container
ENTRYPOINT ["cloudflared", "--no-autoupdate"]
CMD ["version"]
Generated
-622
View File
@@ -1,622 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:b16fbfbcc20645cb419f78325bb2e85ec729b338e996a228124d68931a6f2a37"
name = "github.com/BurntSushi/toml"
packages = ["."]
pruneopts = "UT"
revision = "b26d9c308763d68093482582cea63d69be07a0f0"
version = "v0.3.0"
[[projects]]
branch = "master"
digest = "1:d6afaeed1502aa28e80a4ed0981d570ad91b2579193404256ce672ed0a609e0d"
name = "github.com/beorn7/perks"
packages = ["quantile"]
pruneopts = "UT"
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
[[projects]]
digest = "1:fed1f537c2f1269fe475a8556c393fe466641682d73ef8fd0491cd3aa1e47bad"
name = "github.com/certifi/gocertifi"
packages = ["."]
pruneopts = "UT"
revision = "deb3ae2ef2610fde3330947281941c562861188b"
version = "2018.01.18"
[[projects]]
branch = "master"
digest = "1:e5003c19d396d8b3cf1324ea0bf49b00f13e9466d0297d1268b641f1c617c3a2"
name = "github.com/cloudflare/brotli-go"
packages = ["."]
pruneopts = "T"
revision = "18c9f6c67e3dfc12e0ddaca748d2887f97a7ac28"
[[projects]]
branch = "master"
digest = "1:6dbb2bbc7e6333e691c4d82fd86485f0695a35902fbb9b2df5f72e22ab0040f3"
name = "github.com/cloudflare/golibs"
packages = ["lrucache"]
pruneopts = "UT"
revision = "333127dbecfcc23a8db7d9a4f52785d23aff44a1"
[[projects]]
branch = "master"
digest = "1:af68c8bdac3ceaf46c67f0c43cc1ee5f10e705d8e04f8a63fa41231d0be6b0fb"
name = "github.com/coredns/coredns"
packages = [
"core/dnsserver",
"coremain",
"pb",
"plugin",
"plugin/cache",
"plugin/cache/freq",
"plugin/etcd/msg",
"plugin/metrics",
"plugin/metrics/vars",
"plugin/pkg/cache",
"plugin/pkg/dnstest",
"plugin/pkg/dnsutil",
"plugin/pkg/doh",
"plugin/pkg/edns",
"plugin/pkg/fuzz",
"plugin/pkg/log",
"plugin/pkg/nonwriter",
"plugin/pkg/rcode",
"plugin/pkg/response",
"plugin/pkg/trace",
"plugin/pkg/uniq",
"plugin/pkg/watch",
"plugin/test",
"request",
]
pruneopts = "UT"
revision = "992e7928c7c258628d2b13b769acc86781b9faea"
[[projects]]
branch = "master"
digest = "1:6a503e232df389d94ebb97dfb22d4ae463b6e2f351660613e11d9e42f57ab6df"
name = "github.com/coreos/go-oidc"
packages = [
"http",
"jose",
"key",
"oauth2",
"oidc",
]
pruneopts = "UT"
revision = "a93f71fdfe73d2c0f5413c0565eea0af6523a6df"
[[projects]]
digest = "1:1da3a221f0bc090792d3a2a080ff09008427c0e0f0533a4ed6abd8994421da73"
name = "github.com/coreos/go-systemd"
packages = ["daemon"]
pruneopts = "UT"
revision = "39ca1b05acc7ad1220e09f133283b8859a8b71ab"
version = "v17"
[[projects]]
digest = "1:6fda0d7f5e52b081e075775b1ecebf1ea0c923e7be33604ed0225ae078e701b5"
name = "github.com/coreos/pkg"
packages = [
"health",
"httputil",
"timeutil",
]
pruneopts = "UT"
revision = "97fdf19511ea361ae1c100dd393cc47f8dcfa1e1"
version = "v4"
[[projects]]
digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39"
name = "github.com/davecgh/go-spew"
packages = ["spew"]
pruneopts = "UT"
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
branch = "master"
digest = "1:c013ffc6e15f9f898078f9d38441c68b228aa7b899659452170250ccb27f5f1e"
name = "github.com/elgs/gosqljson"
packages = ["."]
pruneopts = "UT"
revision = "027aa4915315a0b2825c0f025cea347829b974fa"
[[projects]]
branch = "master"
digest = "1:15eadee5930d325154e5e1f3c4f4b28ab62f9c0aa949580e8d9edabaf0b64e85"
name = "github.com/equinox-io/equinox"
packages = [
".",
"internal/go-update",
"internal/go-update/internal/binarydist",
"internal/go-update/internal/osext",
"internal/osext",
"proto",
]
pruneopts = "UT"
revision = "f24972fa72facf59d05c91c848b65eac38815915"
[[projects]]
branch = "master"
digest = "1:433763f10d88dba9b533a7ea2fe9f5ee11e57e00306eb97a1f6090fd978e8fa1"
name = "github.com/facebookgo/grace"
packages = ["gracenet"]
pruneopts = "UT"
revision = "75cf19382434e82df4dd84953f566b8ad23d6e9e"
[[projects]]
branch = "master"
digest = "1:50a46ab1d5edbbdd55125b4d37f1bf503d0807c26461f9ad7b358d6006641d09"
name = "github.com/flynn/go-shlex"
packages = ["."]
pruneopts = "UT"
revision = "3f9db97f856818214da2e1057f8ad84803971cff"
[[projects]]
branch = "master"
digest = "1:d4623fc7bf7e281d9107367cc4a9e76ed3e86b1eec1a4e30630c870bef1fedd0"
name = "github.com/getsentry/raven-go"
packages = ["."]
pruneopts = "UT"
revision = "ed7bcb39ff10f39ab08e317ce16df282845852fa"
[[projects]]
branch = "master"
digest = "1:3e6afc3ed8a72949aa735c00fddc23427dc9384ccfd51cf0d91a412e668da632"
name = "github.com/golang-collections/collections"
packages = ["queue"]
pruneopts = "UT"
revision = "604e922904d35e97f98a774db7881f049cd8d970"
[[projects]]
digest = "1:17fe264ee908afc795734e8c4e63db2accabaf57326dbf21763a7d6b86096260"
name = "github.com/golang/protobuf"
packages = [
"proto",
"ptypes",
"ptypes/any",
"ptypes/duration",
"ptypes/timestamp",
]
pruneopts = "UT"
revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265"
version = "v1.1.0"
[[projects]]
digest = "1:582b704bebaa06b48c29b0cec224a6058a09c86883aaddabde889cd1a5f73e1b"
name = "github.com/google/uuid"
packages = ["."]
pruneopts = "UT"
revision = "0cd6bf5da1e1c83f8b45653022c74f71af0538a4"
version = "v1.1.1"
[[projects]]
digest = "1:c79fb010be38a59d657c48c6ba1d003a8aa651fa56b579d959d74573b7dff8e1"
name = "github.com/gorilla/context"
packages = ["."]
pruneopts = "UT"
revision = "08b5f424b9271eedf6f9f0ce86cb9396ed337a42"
version = "v1.1.1"
[[projects]]
digest = "1:e73f5b0152105f18bc131fba127d9949305c8693f8a762588a82a48f61756f5f"
name = "github.com/gorilla/mux"
packages = ["."]
pruneopts = "UT"
revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf"
version = "v1.6.2"
[[projects]]
digest = "1:43dd08a10854b2056e615d1b1d22ac94559d822e1f8b6fcc92c1a1057e85188e"
name = "github.com/gorilla/websocket"
packages = ["."]
pruneopts = "UT"
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
version = "v1.2.0"
[[projects]]
branch = "master"
digest = "1:1a1206efd03a54d336dce7bb8719e74f2f8932f661cb9f57d5813a1d99c083d8"
name = "github.com/grpc-ecosystem/grpc-opentracing"
packages = ["go/otgrpc"]
pruneopts = "UT"
revision = "8e809c8a86450a29b90dcc9efbf062d0fe6d9746"
[[projects]]
digest = "1:75ab90ae3f5d876167e60f493beadfe66f0ed861a710f283fb06c86437a09538"
name = "github.com/jonboulle/clockwork"
packages = ["."]
pruneopts = "UT"
revision = "2eee05ed794112d45db504eb05aa693efd2b8b09"
version = "v0.1.0"
[[projects]]
branch = "master"
digest = "1:37ce7d7d80531b227023331002c0d42b4b4b291a96798c82a049d03a54ba79e4"
name = "github.com/lib/pq"
packages = [
".",
"oid",
]
pruneopts = "UT"
revision = "90697d60dd844d5ef6ff15135d0203f65d2f53b8"
[[projects]]
digest = "1:c658e84ad3916da105a761660dcaeb01e63416c8ec7bc62256a9b411a05fcd67"
name = "github.com/mattn/go-colorable"
packages = ["."]
pruneopts = "UT"
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
version = "v0.0.9"
[[projects]]
digest = "1:d4d17353dbd05cb52a2a52b7fe1771883b682806f68db442b436294926bbfafb"
name = "github.com/mattn/go-isatty"
packages = ["."]
pruneopts = "UT"
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
version = "v0.0.3"
[[projects]]
digest = "1:ff5ebae34cfbf047d505ee150de27e60570e8c394b3b8fdbb720ff6ac71985fc"
name = "github.com/matttproud/golang_protobuf_extensions"
packages = ["pbutil"]
pruneopts = "UT"
revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c"
version = "v1.0.1"
[[projects]]
branch = "master"
digest = "1:75fa16a231ef40da3e462d651c20b9df20bde0777bdc1ac0982242c79057ee71"
name = "github.com/mholt/caddy"
packages = [
".",
"caddyfile",
"telemetry",
]
pruneopts = "UT"
revision = "d3b731e9255b72d4571a5aac125634cf1b6031dc"
[[projects]]
digest = "1:73f788f2df39614dd7d9e75c8ca5fb8b4a68937b5e6d4a15e4c3a596f6f8c07b"
name = "github.com/miekg/dns"
packages = [
".",
"internal/socket",
]
pruneopts = "UT"
revision = "6da3249dfb57fbaa16efafcd8744cee8809d80cd"
[[projects]]
branch = "master"
digest = "1:8eb17c2ec4df79193ae65b621cd1c0c4697db3bc317fe6afdc76d7f2746abd05"
name = "github.com/mitchellh/go-homedir"
packages = ["."]
pruneopts = "UT"
revision = "3864e76763d94a6df2f9960b16a20a33da9f9a66"
[[projects]]
digest = "1:450b7623b185031f3a456801155c8320209f75d0d4c4e633c6b1e59d44d6e392"
name = "github.com/opentracing/opentracing-go"
packages = [
".",
"ext",
"log",
]
pruneopts = "UT"
revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38"
version = "v1.0.2"
[[projects]]
digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747"
name = "github.com/pkg/errors"
packages = ["."]
pruneopts = "UT"
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
pruneopts = "UT"
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
digest = "1:c968b29db5d68ec97de404b6d058d5937fa015a141b3b4f7a0d87d5f8226f04c"
name = "github.com/prometheus/client_golang"
packages = [
"prometheus",
"prometheus/promhttp",
]
pruneopts = "UT"
revision = "967789050ba94deca04a5e84cce8ad472ce313c1"
version = "v0.9.0-pre1"
[[projects]]
branch = "master"
digest = "1:32d10bdfa8f09ecf13598324dba86ab891f11db3c538b6a34d1c3b5b99d7c36b"
name = "github.com/prometheus/client_model"
packages = ["go"]
pruneopts = "UT"
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
[[projects]]
branch = "master"
digest = "1:e469cd65badf7694aeb44874518606d93c1d59e7735d3754ad442782437d3cc3"
name = "github.com/prometheus/common"
packages = [
"expfmt",
"internal/bitbucket.org/ww/goautoneg",
"model",
]
pruneopts = "UT"
revision = "7600349dcfe1abd18d72d3a1770870d9800a7801"
[[projects]]
branch = "master"
digest = "1:20d9bb50dbee172242f9bcd6ec24a917dd7a5bb17421bf16a79c33111dea7db1"
name = "github.com/prometheus/procfs"
packages = [
".",
"internal/util",
"nfs",
"xfs",
]
pruneopts = "UT"
revision = "ae68e2d4c00fed4943b5f6698d504a5fe083da8a"
[[projects]]
digest = "1:9a6f766efd8d5752adb7052aebb6e3d85255b31a8dff5e58ab4efa740ba9efa0"
name = "github.com/rifflock/lfshook"
packages = ["."]
pruneopts = "UT"
revision = "bf539943797a1f34c1f502d07de419b5238ae6c6"
version = "v2.3"
[[projects]]
digest = "1:9e9193aa51197513b3abcb108970d831fbcf40ef96aa845c4f03276e1fa316d2"
name = "github.com/sirupsen/logrus"
packages = ["."]
pruneopts = "UT"
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
version = "v1.0.5"
[[projects]]
digest = "1:18752d0b95816a1b777505a97f71c7467a8445b8ffb55631a7bf779f6ba4fa83"
name = "github.com/stretchr/testify"
packages = ["assert"]
pruneopts = "UT"
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
version = "v1.2.2"
[[projects]]
branch = "master"
digest = "1:1182d12da264c8fe593504f150c3a479297767991fc1c06796cb67e4f61c4366"
name = "golang.org/x/crypto"
packages = [
"curve25519",
"ed25519",
"ed25519/internal/edwards25519",
"internal/subtle",
"nacl/box",
"nacl/secretbox",
"poly1305",
"salsa20/salsa",
"ssh/terminal",
]
pruneopts = "UT"
revision = "a49355c7e3f8fe157a85be2f77e6e269a0f89602"
[[projects]]
branch = "master"
digest = "1:9513f8a0f1f6918181b7d744225b1931ffacafeb57067a1c26893a51058afb80"
name = "golang.org/x/net"
packages = [
"context",
"http/httpguts",
"http2",
"http2/hpack",
"idna",
"internal/timeseries",
"trace",
"websocket",
]
pruneopts = "UT"
revision = "32a936f46389aa10549d60bd7833e54b01685d09"
[[projects]]
branch = "master"
digest = "1:39ebcc2b11457b703ae9ee2e8cca0f68df21969c6102cb3b705f76cca0ea0239"
name = "golang.org/x/sync"
packages = ["errgroup"]
pruneopts = "UT"
revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
[[projects]]
branch = "master"
digest = "1:2e8e74aa73847379a370c2704ff08baf4a7303ae682766e70c23ded3fd9b3f37"
name = "golang.org/x/sys"
packages = [
"unix",
"windows",
"windows/registry",
"windows/svc",
"windows/svc/eventlog",
"windows/svc/mgr",
]
pruneopts = "UT"
revision = "ce36f3865eeb42541ce3f87f32f8462c5687befa"
[[projects]]
digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18"
name = "golang.org/x/text"
packages = [
"collate",
"collate/build",
"internal/colltab",
"internal/gen",
"internal/tag",
"internal/triegen",
"internal/ucd",
"language",
"secure/bidirule",
"transform",
"unicode/bidi",
"unicode/cldr",
"unicode/norm",
"unicode/rangetable",
]
pruneopts = "UT"
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0"
[[projects]]
branch = "master"
digest = "1:601e63e7d4577f907118bec825902505291918859d223bce015539e79f1160e3"
name = "google.golang.org/genproto"
packages = ["googleapis/rpc/status"]
pruneopts = "UT"
revision = "ff3583edef7de132f219f0efc00e097cabcc0ec0"
[[projects]]
digest = "1:2dab32a43451e320e49608ff4542fdfc653c95dcc35d0065ec9c6c3dd540ed74"
name = "google.golang.org/grpc"
packages = [
".",
"balancer",
"balancer/base",
"balancer/roundrobin",
"codes",
"connectivity",
"credentials",
"encoding",
"encoding/proto",
"grpclog",
"internal",
"internal/backoff",
"internal/channelz",
"internal/grpcrand",
"keepalive",
"metadata",
"naming",
"peer",
"resolver",
"resolver/dns",
"resolver/passthrough",
"stats",
"status",
"tap",
"transport",
]
pruneopts = "UT"
revision = "168a6198bcb0ef175f7dacec0b8691fc141dc9b8"
version = "v1.13.0"
[[projects]]
branch = "altsrc-parse-durations"
digest = "1:0370b1bceda03dbfade3abbde639a43f1113bab711ec760452e5c0dcc0c14787"
name = "gopkg.in/urfave/cli.v2"
packages = [
".",
"altsrc",
]
pruneopts = "UT"
revision = "d604b6ffeee878fbf084fd2761466b6649989cee"
source = "https://github.com/cbranch/cli"
[[projects]]
digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202"
name = "gopkg.in/yaml.v2"
packages = ["."]
pruneopts = "UT"
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
version = "v2.2.1"
[[projects]]
digest = "1:8ffc3ddc31414c0a71220957bb723b16510d7fcb5b3880dc0da4cf6d39c31642"
name = "zombiezen.com/go/capnproto2"
packages = [
".",
"encoding/text",
"internal/fulfiller",
"internal/nodemap",
"internal/packed",
"internal/queue",
"internal/schema",
"internal/strquote",
"pogs",
"rpc",
"rpc/internal/refcount",
"schemas",
"server",
"std/capnp/rpc",
]
pruneopts = "UT"
revision = "7cfd211c19c7f5783c695f3654efa46f0df259c3"
source = "https://github.com/zombiezen/go-capnproto2"
version = "v2.17.1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/cloudflare/brotli-go",
"github.com/cloudflare/golibs/lrucache",
"github.com/coredns/coredns/core/dnsserver",
"github.com/coredns/coredns/plugin",
"github.com/coredns/coredns/plugin/cache",
"github.com/coredns/coredns/plugin/metrics/vars",
"github.com/coredns/coredns/plugin/pkg/dnstest",
"github.com/coredns/coredns/plugin/pkg/rcode",
"github.com/coredns/coredns/request",
"github.com/coreos/go-oidc/jose",
"github.com/coreos/go-oidc/oidc",
"github.com/coreos/go-systemd/daemon",
"github.com/elgs/gosqljson",
"github.com/equinox-io/equinox",
"github.com/facebookgo/grace/gracenet",
"github.com/getsentry/raven-go",
"github.com/golang-collections/collections/queue",
"github.com/google/uuid",
"github.com/gorilla/mux",
"github.com/gorilla/websocket",
"github.com/lib/pq",
"github.com/mattn/go-colorable",
"github.com/miekg/dns",
"github.com/mitchellh/go-homedir",
"github.com/pkg/errors",
"github.com/prometheus/client_golang/prometheus",
"github.com/prometheus/client_golang/prometheus/promhttp",
"github.com/rifflock/lfshook",
"github.com/sirupsen/logrus",
"github.com/stretchr/testify/assert",
"golang.org/x/crypto/nacl/box",
"golang.org/x/crypto/ssh/terminal",
"golang.org/x/net/context",
"golang.org/x/net/http2",
"golang.org/x/net/http2/hpack",
"golang.org/x/net/idna",
"golang.org/x/net/trace",
"golang.org/x/net/websocket",
"golang.org/x/sync/errgroup",
"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",
"gopkg.in/urfave/cli.v2",
"gopkg.in/urfave/cli.v2/altsrc",
"zombiezen.com/go/capnproto2",
"zombiezen.com/go/capnproto2/encoding/text",
"zombiezen.com/go/capnproto2/pogs",
"zombiezen.com/go/capnproto2/rpc",
"zombiezen.com/go/capnproto2/schemas",
"zombiezen.com/go/capnproto2/server",
"zombiezen.com/go/capnproto2/std/capnp/rpc",
]
solver-name = "gps-cdcl"
solver-version = 1
-83
View File
@@ -1,83 +0,0 @@
[prune]
go-tests = true
unused-packages = true
[[prune.project]]
name = "github.com/cloudflare/brotli-go"
unused-packages = false
[[constraint]]
name = "github.com/facebookgo/grace"
branch = "master"
[[constraint]]
name = "github.com/getsentry/raven-go"
branch = "master"
[[constraint]]
name = "github.com/pkg/errors"
version = "0.8.0"
[[constraint]]
name = "github.com/prometheus/client_golang"
version = "0.9.0-pre1"
[[constraint]]
name = "github.com/sirupsen/logrus"
version = "1.0.3"
[[constraint]]
name = "github.com/stretchr/testify"
version = "1.2.1"
[[constraint]]
name = "golang.org/x/net"
branch = "master"
[[constraint]]
name = "golang.org/x/sync"
branch = "master"
[[constraint]]
name = "gopkg.in/urfave/cli.v2"
source = "https://github.com/cbranch/cli"
branch = "altsrc-parse-durations"
[[constraint]]
name = "zombiezen.com/go/capnproto2"
source = "https://github.com/zombiezen/go-capnproto2"
version = "2.17.1"
[[constraint]]
name = "github.com/gorilla/websocket"
version = "1.2.0"
[[constraint]]
name = "github.com/coredns/coredns"
branch = "master"
[[constraint]]
name = "github.com/miekg/dns"
revision = "6da3249dfb57fbaa16efafcd8744cee8809d80cd"
[[constraint]]
name = "github.com/cloudflare/brotli-go"
branch = "master"
[[override]]
name = "github.com/mholt/caddy"
branch = "master"
[[constraint]]
branch = "master"
name = "github.com/coreos/go-oidc"
[[constraint]]
branch = "master"
name = "golang.org/x/crypto"
[[constraint]]
branch = "master"
name = "github.com/cloudflare/golibs"
+53 -15
View File
@@ -7,17 +7,37 @@ PACKAGE_DIR := $(CURDIR)/packaging
INSTALL_BINDIR := usr/local/bin
EQUINOX_FLAGS = --version="$(VERSION)" \
--platforms="$(EQUINOX_BUILD_PLATFORMS)" \
--app="$(EQUINOX_APP_ID)" \
--token="$(EQUINOX_TOKEN)" \
--channel="$(EQUINOX_CHANNEL)"
--platforms="$(EQUINOX_BUILD_PLATFORMS)" \
--app="$(EQUINOX_APP_ID)" \
--token="$(EQUINOX_TOKEN)" \
--channel="$(EQUINOX_CHANNEL)"
ifeq ($(EQUINOX_IS_DRAFT), true)
EQUINOX_FLAGS := --draft $(EQUINOX_FLAGS)
endif
ifeq ($(GOARCH),)
GOARCH := amd64
LOCAL_ARCH ?= $(shell uname -m)
ifeq ($(LOCAL_ARCH),x86_64)
TARGET_ARCH ?= amd64
else ifeq ($(shell echo $(LOCAL_ARCH) | head -c 5),armv8)
TARGET_ARCH ?= arm64
else ifeq ($(LOCAL_ARCH),aarch64)
TARGET_ARCH ?= arm64
else ifeq ($(shell echo $(LOCAL_ARCH) | head -c 4),armv)
TARGET_ARCH ?= arm
else
$(error This system's architecture $(LOCAL_ARCH) isn't supported)
endif
LOCAL_OS ?= $(shell go env GOOS)
ifeq ($(LOCAL_OS),linux)
TARGET_OS ?= linux
else ifeq ($(LOCAL_OS),darwin)
TARGET_OS ?= darwin
else ifeq ($(LOCAL_OS),windows)
TARGET_OS ?= windows
else
$(error This system's OS $(LOCAL_OS) isn't supported)
endif
.PHONY: all
@@ -28,19 +48,27 @@ clean:
go clean
.PHONY: cloudflared
cloudflared:
go build -v $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/cloudflared
cloudflared: tunnel-deps
GOOS=$(TARGET_OS) GOARCH=$(TARGET_ARCH) go build -v -mod=vendor $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/cloudflared
.PHONY: container
container:
docker build --build-arg=TARGET_ARCH=$(TARGET_ARCH) --build-arg=TARGET_OS=$(TARGET_OS) -t cloudflare/cloudflared-$(TARGET_OS)-$(TARGET_ARCH):"$(VERSION)" .
.PHONY: test
test:
go test -v -race $(VERSION_FLAGS) ./...
test: vet
go test -v -mod=vendor -race $(VERSION_FLAGS) ./...
.PHONY: test-ssh-server
test-ssh-server:
docker-compose -f ssh_server_tests/docker-compose.yml up
.PHONY: cloudflared-deb
cloudflared-deb: cloudflared
mkdir -p $(PACKAGE_DIR)
cp cloudflared $(PACKAGE_DIR)/cloudflared
fakeroot fpm -C $(PACKAGE_DIR) -s dir -t deb --deb-compression bzip2 \
-a $(GOARCH) -v $(VERSION) -n cloudflared cloudflared=/usr/local/bin/
-a $(TARGET_ARCH) -v $(VERSION) -n cloudflared cloudflared=/usr/local/bin/
.PHONY: cloudflared-darwin-amd64.tgz
cloudflared-darwin-amd64.tgz: cloudflared
@@ -51,8 +79,8 @@ cloudflared-darwin-amd64.tgz: cloudflared
homebrew-upload: cloudflared-darwin-amd64.tgz
aws s3 --endpoint-url $(S3_ENDPOINT) cp --acl public-read $$^ $(S3_URI)/cloudflared-$$(VERSION)-$1.tgz
aws s3 --endpoint-url $(S3_ENDPOINT) cp --acl public-read $(S3_URI)/cloudflared-$$(VERSION)-$1.tgz $(S3_URI)/cloudflared-stable-$1.tgz
.PHONY: homebrew-release
.PHONY: homebrew-release
homebrew-release: homebrew-upload
./publish-homebrew-formula.sh cloudflared-darwin-amd64.tgz $(VERSION) homebrew-cloudflare
@@ -65,5 +93,15 @@ bin/equinox:
curl -s https://bin.equinox.io/c/75JtLRTsJ3n/release-tool-beta-$(EQUINOX_PLATFORM).tgz | tar xz -C bin/
.PHONY: tunnel-deps
tunnel-deps:
capnp compile -ogo -I ./tunnelrpc tunnelrpc/tunnelrpc.capnp
tunnel-deps: tunnelrpc/tunnelrpc.capnp.go
tunnelrpc/tunnelrpc.capnp.go: tunnelrpc/tunnelrpc.capnp
which capnp # https://capnproto.org/install.html
which capnpc-go # go get zombiezen.com/go/capnproto2/capnpc-go
capnp compile -ogo tunnelrpc/tunnelrpc.capnp
.PHONY: vet
vet:
go vet -mod=vendor ./...
which go-sumtype # go get github.com/BurntSushi/go-sumtype
go-sumtype $$(go list -mod=vendor ./...)
+286
View File
@@ -1,3 +1,289 @@
2020.6.4
- 2020-06-11 TUN-3085: Pass connection authentication information using TunnelAuth struct
- 2020-06-15 TUN-3084: Generate and store tunnel_secret value during tunnel creation
- 2020-06-16 AUTH-2815 add the log file to support the config.yaml file
- 2020-06-02 TUN-3015: Add a new cap'n'proto RPC interface for connection registration as well as matching client and server implementations. The old interface extends the new one for backward compatibility.
2020.6.3
- 2020-06-15 DEVTOOLS-7321: Add openssh-client pkg for missing ssh-keyscan
- 2020-06-15 AUTH-2813 adds back a single file support a cloudflared log file
2020.6.2
- 2020-06-11 AUTH-2648 updated usage text
- 2020-06-11 AUTH-2763 don't redirect from curl command
- 2020-06-12 TUN-3090: Upgrade crypto dep
- 2020-06-11 TUN-3038: Add connections to tunnel list table
- 2020-06-12 AUTH-2810 added warn for backwards compatibility sake
2020.6.1
- 2020-06-09 AUTH-2796 fixed windows build
2020.6.0
- 2020-06-05 AUTH-2645 protect against user mistaken flag input
- 2020-06-05 AUTH-2687 don't copy config unnecessarily
- 2020-06-05 AUTH-2169 make access login page more generic
- 2020-06-05 AUTH-2729 added log file and level to cmd flags to match config file settings
- 2020-06-08 AUTH-2785 service token flag fix and logger fix
- 2020-05-20 AUTH-2682: Create buster build
- 2020-05-21 TUN-2928, TUN-2929, TUN-2930: Add tunnel subcommands to interact with tunnel store service
- 2020-05-29 Adding support for multi-architecture images and binaries (#184)
- 2020-05-29 TUN-3019: Remove declarative tunnel entry code
- 2020-05-29 TUN-3020: Remove declarative tunnel related RPC code
- 2020-05-13 AUTH-2505 added aliases
- 2020-05-14 AUTH-2529 added deprecation text to db-connect command
- 2020-05-18 AUTH-2686: Added error handling to tunnel subcommand
- 2020-05-04 AUTH-2369: RDP Bastion prototype
- 2020-04-29 AUTH-2596 added new logger package and replaced logrus
- 2020-04-25 DEVTOOLS-7321: Use SSH key from env for pushing to GitHub
- 2020-04-25 DEVTOOLS-7321: Push to a test branch instead of to master
- 2020-03-30 DEVTOOLS-7321: Add scripts for macOS builds and homebrew uploads
2020.5.1
- 2020-05-07 TUN-2860: Enable quick reconnect feature by default
- 2020-05-07 AUTH-2564: error handling and minor fixes
- 2020-05-01 AUTH-2588 add DoH to service mode
2020.5.0
- 2020-05-01 TUN-2943: Copy certutil from edge into cloudflared
- 2020-05-05 TUN-2955: Fix connection and goroutine leaks when tunnel conection is terminated on error. Only unregister tunnels that had connected successfully. Close edge connection used to unregister the tunnel. Use buffered channels for error channels where receiver may quit early on context cancellation.
- 2020-04-30 TUN-2940: Added delay parameter to stdin reconnect command.
- 2020-04-27 TUN-2921: Rework address selection logic to avoid corner cases
- 2020-04-28 TUN-2872: Exit with non-0 status code when the binary is updated so launchd will restart the service
- 2020-04-13 AUTH-2587 add config watcher and reload logic for access client forwarder
2020.4.0
- 2020-04-10 TUN-2881: Parameterize response meta information header name in the generating function
- 2020-04-11 TUN-2894: ResponseMetaHeader should be public
- 2020-04-09 TUN-2880: Return metadata about source of the response from cloudflared
- 2020-04-04 ARES-899: Fixes DoH client as system resolver. Fixes #91
- 2020-03-31 AUTH-2394 added socks5 proxy
- 2020-02-24 AUTH-2235 GetTokenIfExists now parses JWT payload for json expiry field to detect if the cached access token is expired
2020.3.2
- 2020-03-31 TUN-2854: Quick Reconnects should be an optional supported feature
- 2020-03-30 TUN-2850: Tunnel stripping Cloudflare headers
2020.3.1
- 2020-03-27 TUN-2846: Trigger debug reconnects from stdin commands, not SIGUSR1
2020.3.0
- 2020-03-23 AUTH-2394 fixed header for websockets. Added TCP alias
- 2020-03-10 TUN-2797: Fix panic in SetConnDigest by making mutexes values.
- 2020-03-13 TUN-2807: cloudflared hello-world shouldn't assume it's my first tunnel
- 2020-03-13 TUN-2756: Set connection digest after reconnect.
- 2020-03-16 TUN-2812: Tunnel proxies and RPCs can share an edge address
- 2020-03-18 TUN-2816: cloudflared metrics server should be more discoverable
- 2020-03-19 TUN-2820: Serialized headers for Websockets
- 2020-03-19 TUN-2819: cloudflared should close its connections when a signal is sent
- 2020-03-19 TUN-2823: Bugfix. cloudflared would hang forever if error occurred.
- 2020-03-10 TUN-2796: Implement HTTP2 CONTINUATION headers correctly
- 2020-03-02 TUN-2779: update sample HTML pages
- 2020-03-04 TUN-2785: Use reconnect token by default
- 2020-03-05 TUN-2754: Add ConnDigest to cloudflared and its RPCs
- 2020-03-06 TUN-2755: ReconnectTunnel RPC now transmits ConnectionDigest
- 2020-03-06 TUN-2761: Use the new header management functions in cloudflared
- 2020-03-06 TUN-2788: cloudflared should store one ConnDigest per HA connection
- 2020-02-26 TUN-2767: Test for large headers
- 2020-02-28 do not terminate tunnel if origin is not reachable on start-up (#177)
- 2020-02-28 TUN-2776: Add header serialization feature in cloudflared
- 2020-02-21 TUN-2748: Insecure randomness vulnerability in github.com/miekg/dns
2020.2.1
- 2020-02-20 TUN-2745: Rename existing header management functions
- 2020-02-21 TUN-2746: Add the new header management functions
- 2020-02-25 perf(cloudflared): reuse memory from buffer pool to get better throughput (#161)
- 2020-02-25 Tweak HTTP host header. Fixes #107 (#168)
- 2020-02-25 TUN-2765: Add list of features to tunnelrpc
- 2020-02-19 TUN-2725: Specify in code that --edge is for internal testing only
- 2020-02-19 TUN-2703: Muxer.Serve terminates when its context is Done
- 2020-02-09 TUN-2717: Function to serialize/deserialize HTTP headers
- 2020-02-05 TUN-2714: New edge discovery. Connections try to reconnect to the same edge IP.
2020.2.0
- 2020-01-30 TUN-2651: Fix panic in h2mux reader when a stream error is encountered
- 2020-01-27 TUN-2645: Revert "TUN-2645: Turn on reconnect tokens"
- 2020-01-28 TUN-2693: Metrics for ReconnectTunnel
- 2020-01-28 TUN-2696: Add unknown registerRPCName
- 2020-01-28 TUN-2699: Metrics for Authenticate RPCs
- 2020-01-28 TUN-2690: cloudflared reconnect uses wrong context
- 2020-01-29 TUN-2707: Inconsistent cardinality in tunnel error metrics
- 2020-01-13 TUN-2645: Turn on reconnect tokens
- 2019-12-23 TUN-2646: Make --edge flag work again for local development
2019.12.0
- 2019-12-11 TUN-2631: only notify that activeStreamMap is closed if ignoreNewStreams=true
- 2019-12-17 bug(cloudflared): Set the MaxIdleConnsPerHost of http.Transport to proxy-keepalive-connections (#155)
- 2019-12-17 refactor(docker): optimize Dockerfile (#126)
- 2019-12-19 Fix timer scheduling for systemd update service (#159)
- 2019-12-13 TUN-2637: Manage edge IPs in a region-aware manner
- 2019-12-03 bug(cloudflared): nil pointer deference on h2DictWriter Close() (#154)
- 2019-12-03 TUN-2608: h2mux.Muxer.Shutdown always returns a non-nil channel
- 2019-12-04 TUN-2555: origin/supervisor.go calls Authenticate
- 2019-12-06 TUN-2554: cloudflared calls ReconnectTunnel
- 2019-11-20 TUN-2575: Constructors + simpler conversions for AuthOutcome
- 2019-11-22 Fix Docker build failure (#149)
- 2019-11-21 TUN-2573: Refactor TunnelRegistration into PermanentRegistrationError, RetryableRegistrationError and SuccessfulTunnelRegistration
- 2019-11-22 TUN-2582: EventDigest field in tunnelrpc
- 2019-11-22 Fix "happy eyeballs" not being disabled since Golang 1.12 upgrade * The Dialer.DualStack setting is now ignored and deprecated; RFC 6555 Fast Fallback ("Happy Eyeballs") is now enabled by default. To disable, set Dialer.FallbackDelay to a negative value.
- 2019-11-25 TUN-2591: ReconnectTunnel now sends EventDigest
- 2019-11-21 TUN-2606: add DialEdge helpers
- 2019-11-21 TUN-2607: add RPC stream helpers
2019.11.3
- 2019-11-20 TUN-2562: Update Cloudflare Origin CA RSA root
2019.11.2
- 2019-11-18 TUN-2567: AuthOutcome can be turned back into AuthResponse
- 2019-11-18 TUN-2563: Exposes config_version metrics
2019.11.1
- 2019-11-12 Add db-connect, a SQL over HTTPS server
- 2019-11-12 TUN-2053: Add a /healthcheck endpoint to the metrics server
- 2019-11-13 TUN-2178: public API to create new h2mux.MuxedStreamRequest
- 2019-11-13 TUN-2490: respect original representation of HTTP request path
- 2019-11-18 TUN-2547: TunnelRPC definitions for Authenticate flow
- 2019-11-18 TUN-2551: TunnelRPC definitions for ReconnectTunnel flow
- 2019-11-05 TUN-2506: Expose active streams metrics
2019.11.0
- 2019-11-04 TUN-2502: Switch to go modules
- 2019-11-04 TUN-2500: Don't send client registration errors to Sentry
- 2019-11-04 TUN-2489: Delete stream from activestreammap when read and write are both closed
- 2019-11-05 TUN-2505: Terminate stream on receipt of RST_STREAM; MuxedStream.CloseWrite() should terminate the MuxedStream.Write() loop
- 2019-10-30 TUN-2451: Log inavlid path
- 2019-10-22 TUN-2425: Enable cloudflared to serve multiple Hello World servers by having each of them create its own ServeMux
- 2019-10-22 AUTH-2173: Prepends access login url with scheme if one doesnt exist
- 2019-10-23 TUN-2460: Configure according to the ClientConfig recevied from a successful Connect
- 2019-10-23 AUTH-2177: Reads and writes error streams
2019.10.4
- 2019-10-21 TUN-2450: Remove Brew publishing formula
2019.10.3
- 2019-10-18 Fix #129: Excessive memory usage streaming large files (#142)
2019.10.2
- 2019-10-17 AUTH-2167: Adds CLI option for host key directory
2019.10.1
- 2019-10-17 Adds variable to fix windows build
2019.10.0
- 2019-10-11 AUTH-2105: Dont require --destination arg
- 2019-10-14 TUN-2344: log more details: http2.Framer.ErrorDetail() if available, connectionID
- 2019-10-16 AUTH-2159: Moves shutdownC close into error handling AUTH-2161: Lowers size of preamble length AUTH-2160: Fixes url parsing logic
- 2019-10-16 AUTH-2135: Adds support for IPv6 and tests
- 2019-10-02 AUTH-2105: Adds support for local forwarding. Refactor auditlogger creation. AUTH-2088: Adds dynamic destination routing
- 2019-10-09 AUTH-2114: Uses short lived cert auth for outgoing client connection
- 2019-09-30 AUTH-2089: Revise ssh server to function as a proxy
2019.9.2
- 2019-09-26 TUN-2355: Roll back TUN-2276
2019.9.1
- 2019-09-23 TUN-2334: remove tlsConfig.ServerName special case
- 2019-09-23 AUTH-2077: Quotes open browser command in windows
- 2019-09-11 AUTH-2050: Adds time.sleep to temporarily avoid hitting tunnel muxer dealock issue
- 2019-09-10 AUTH-2056: Writes stderr to its own stream for non-pty connections
- 2019-09-16 TUN-2307: Capnp is the only serialization format used in tunnelpogs
- 2019-09-18 TUN-2315: Replace Scope with IntentLabel
- 2019-09-17 TUN-2309: Split ConnectResult into ConnectError and ConnectSuccess, each implementing its own capnp serialization logic
- 2019-09-18 AUTH-2052: Adds tests for SSH server
- 2019-09-18 AUTH-2067: Log commands correctly
- 2019-09-19 AUTH-2055: Verifies token at edge on access login
- 2019-09-04 TUN-2201: change SRV records used by cloudflared
- 2019-09-06 TUN-2280: Revert "TUN-2260: add name/group to CapnpConnectParameters, remove Scope"
- 2019-09-03 AUTH-1943 hooked up uploader to logger, added timestamp to session logs, add tests
- 2019-09-04 AUTH-2036: Refactor user retrieval, shutdown after ssh server stops, add custom version string
- 2019-09-06 AUTH-1942 added event log to ssh server
- 2019-09-04 AUTH-2037: Adds support for ssh port forwarding
- 2019-09-05 TUN-2276: Path encoding broken
2019.9.0
- 2019-09-05 TUN-2279: Revert path encoding fix
- 2019-08-30 AUTH-2021 - check error for failing tests
- 2019-08-29 AUTH-2030: Support both authorized_key and short lived cert authentication simultaniously without specifiying at start time
- 2019-08-29 AUTH-2026: Adds support for non-pty sessions and inline command exec
- 2019-08-26 AUTH-1943: Adds session logging
- 2019-08-26 TUN-2162: Decomplect OpenStream to allow finer-grained timeouts
- 2019-08-29 TUN-2260: add name/group to CapnpConnectParameters, remove Scope
2019.8.4
- 2019-08-30 Fix #111: Add support for specifying a specific HTTP Host: header on the origin. (#114)
- 2019-08-22 TUN-2165: Add ClientConfig to tunnelrpc.ConnectResult
- 2019-08-20 AUTH-2014: Checks users login shell
- 2019-08-26 TUN-2243: Revert "STOR-519: Add db-connect, a SQL over HTTPS server"
- 2019-08-27 TUN-2244: Add NO_AUTOUPDATE env var
- 2019-08-22 AUTH-2018: Adds support for authorized keys and short lived certs
- 2019-08-28 AUTH-2022: Adds ssh timeout configuration
- 2019-08-28 TUN-1968: Gracefully diff StreamHandler.UpdateConfig
- 2019-08-26 AUTH-2021 - s3 bucket uploading for SSH logs
- 2019-08-19 AUTH-2004: Adds static host key support
- 2019-07-18 AUTH-1941: Adds initial SSH server implementation
2019.8.3
- 2019-08-20 STOR-519: Add db-connect, a SQL over HTTPS server
- 2019-08-20 Release 2019.8.2
- 2019-08-20 Revert "AUTH-1941: Adds initial SSH server implementation"
- 2019-08-11 TUN-2163: Add GrapQLType method to Scope interface
- 2019-08-06 TUN-2152: Requests with a query in the URL are erroneously escaped
- 2019-07-18 AUTH-1941: Adds initial SSH server implementation
2019.8.1
- 2019-08-05 TUN-2111: Implement custom serialization logic for FallibleConfig and OriginConfig
- 2019-08-06 Revert "TUN-1736: Missing headers when passing an invalid path"
2019.8.0
- 2019-07-11 TUN-1956: Go 1.12 update
- 2019-07-24 TUN-1736: Missing headers when passing an invalid path
- 2019-07-30 TUN-2117: read group/system-name from CLI, send it to edge
- 2019-08-02 TUN-2125: Add PostgresType() to Scope
- 2019-08-05 TUN-2147: Implemented ScopeUnmarshaler
- 2019-07-31 TUN-2110: Implement custom deserialization logic for OriginConfig
- 2019-07-31 AUTH-1972: Deletes token lock file if backoff retry attempts exceeded and intercepts signals until lock is released
2019.7.0
- 2019-05-28 TUN-1913: Define OriginService for each type of origin
- 2019-04-29 Build a docker container
- 2019-06-12 TUN-1952: Group ClientConfig fields by the component that uses the config, and return the part of the config that failed to be applied
- 2019-06-05 TUN-1893: Proxy requests to the origin based on tunnel hostname
- 2019-06-17 TUN-1961: Create EdgeConnectionManager to maintain outbound connections to the edge
- 2019-06-18 TUN-1885: Reconfigure cloudflared on receiving new ClientConfig
- 2019-06-19 TUN-1976: Pass tunnel hostname through header
- 2019-06-20 TUN-1982: Load custom origin CA when OriginCAPool is specified
- 2019-06-26 TUN-2005: Upgrade logrus
- 2019-06-20 TUN-1981: Write response header & body on proxy error to notify eyeballs of failure category
- 2019-06-20 TUN-1977: Validate OriginConfig has valid URL, and use scheme to determine if a HTTPOriginService is expecting HTTP or Unix
- 2019-06-13 DoH: change the media type to application/dns-message
- 2019-06-26 AUTH-1736: Better handling of token revocation
2019.6.0
- 2019-05-17 TUN-1828: Update declarative tunnel config struct
- 2019-05-29 Handle exit code on err
- 2019-05-29 AUTH-1802: Fixed ssh-config templating
- 2019-05-30 TUN-1914: Conflate HTTP and Unix OriginConfig, and add TLS config to WebSocketOriginConfig
- 2019-06-03 AUTH-1811: ssh-gen config fixes
2019.5.0
- 2019-04-25 TUN-1781: ServeStream should return early on error
- 2019-04-30 TUN-1786: Remove low-level Windows service logging
- 2019-05-03 TUN-1807: Send cloudflared version in Connect RPC
- 2019-01-23 AUTH-1557: Short Lived Certs
- 2019-05-13 TUN-1847: Log a distinct message when OpenStream fails while waiting for response headers
- 2019-05-13 AUTH-1706: fixes and testing
- 2019-05-22 TUN-1880: Save debug and warn level log to logfile
- 2019-05-22 AUTH-1781: fixed race condition for short lived certs, doc required config
2019.4.1
- 2019-03-18 TUN-1626: Create new supervisor to establish connection with origintunneld
- 2019-04-04 TUN-1619: Add flag to test declarative tunnels.
- 2019-04-05 TUN-1577: decompose carrier.StartServer to make TestStartServer less flappy
- 2019-03-29 TUN-1606: Define CloudflaredConfig RPC structure, interface for cloudflared's RPC server
- 2019-04-02 TUN-1682: Add context to OpenStream to prevent it from blocking indefinitely.
- 2019-04-16 TUN-1732: cloudflared metrics should track userHostnames
- 2019-04-17 TUN-1734: Pin packages at exact versions
- 2019-04-18 TUN-1669: Update license message in help text. Also fix test
2019.4.0
- 2019-03-28 TUN-1648: ConnectionID is now a UUID
- 2019-04-01 TUN-1673: Conflate Hello and Connect RPCs
+106
View File
@@ -0,0 +1,106 @@
package awsuploader
import (
"os"
"path/filepath"
"time"
"github.com/cloudflare/cloudflared/logger"
)
// DirectoryUploadManager is used to manage file uploads on an interval from a directory
type DirectoryUploadManager struct {
logger logger.Service
uploader Uploader
rootDirectory string
sweepInterval time.Duration
ticker *time.Ticker
shutdownC chan struct{}
workQueue chan string
}
// NewDirectoryUploadManager create a new DirectoryUploadManager
// uploader is an Uploader to use as an actual uploading engine
// directory is the directory to sweep for files to upload
// sweepInterval is how often to iterate the directory and upload the files within
func NewDirectoryUploadManager(logger logger.Service, uploader Uploader, directory string, sweepInterval time.Duration, shutdownC chan struct{}) *DirectoryUploadManager {
workerCount := 10
manager := &DirectoryUploadManager{
logger: logger,
uploader: uploader,
rootDirectory: directory,
sweepInterval: sweepInterval,
shutdownC: shutdownC,
workQueue: make(chan string, workerCount),
}
//start workers
for i := 0; i < workerCount; i++ {
go manager.worker()
}
return manager
}
// Upload a file using the uploader
// This is useful for "out of band" uploads that need to be triggered immediately instead of waiting for the sweep
func (m *DirectoryUploadManager) Upload(filepath string) error {
return m.uploader.Upload(filepath)
}
// Start the upload ticker to walk the directories
func (m *DirectoryUploadManager) Start() {
m.ticker = time.NewTicker(m.sweepInterval)
go m.run()
}
func (m *DirectoryUploadManager) run() {
for {
select {
case <-m.shutdownC:
m.ticker.Stop()
return
case <-m.ticker.C:
m.sweep()
}
}
}
// sweep the directory and kick off uploads
func (m *DirectoryUploadManager) sweep() {
filepath.Walk(m.rootDirectory, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return nil
}
//30 days ago
retentionTime := 30 * (time.Hour * 24)
checkTime := time.Now().Add(-time.Duration(retentionTime))
//delete the file it is stale
if info.ModTime().Before(checkTime) {
os.Remove(path)
return nil
}
//add the upload to the work queue
go func() {
m.workQueue <- path
}()
return nil
})
}
// worker handles upload requests
func (m *DirectoryUploadManager) worker() {
for {
select {
case <-m.shutdownC:
return
case filepath := <-m.workQueue:
if err := m.Upload(filepath); err != nil {
m.logger.Errorf("Cannot upload file to s3 bucket: %s", err)
} else {
os.Remove(filepath)
}
}
}
}
@@ -0,0 +1,137 @@
package awsuploader
import (
"errors"
"io/ioutil"
"math/rand"
"os"
"path/filepath"
"testing"
"time"
"github.com/cloudflare/cloudflared/logger"
)
type MockUploader struct {
shouldFail bool
}
func (m *MockUploader) Upload(filepath string) error {
if m.shouldFail {
return errors.New("upload set to fail")
}
return nil
}
func NewMockUploader(shouldFail bool) Uploader {
return &MockUploader{shouldFail: shouldFail}
}
func getDirectoryPath(t *testing.T) string {
dir, err := os.Getwd()
if err != nil {
t.Fatal("couldn't create the test directory!", err)
}
return filepath.Join(dir, "uploads")
}
func setupTestDirectory(t *testing.T) string {
path := getDirectoryPath(t)
os.RemoveAll(path)
time.Sleep(100 * time.Millisecond) //short way to wait for the OS to delete the folder
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
t.Fatal("couldn't create the test directory!", err)
}
return path
}
func createUploadManager(t *testing.T, shouldFailUpload bool) *DirectoryUploadManager {
rootDirectory := setupTestDirectory(t)
uploader := NewMockUploader(shouldFailUpload)
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
shutdownC := make(chan struct{})
return NewDirectoryUploadManager(logger, uploader, rootDirectory, 1*time.Second, shutdownC)
}
func createFile(t *testing.T, fileName string) (*os.File, string) {
path := filepath.Join(getDirectoryPath(t), fileName)
f, err := os.Create(path)
if err != nil {
t.Fatal("upload to create file for sweep test", err)
}
return f, path
}
func TestUploadSuccess(t *testing.T) {
manager := createUploadManager(t, false)
path := filepath.Join(getDirectoryPath(t), "test_file")
if err := manager.Upload(path); err != nil {
t.Fatal("the upload request method failed", err)
}
}
func TestUploadFailure(t *testing.T) {
manager := createUploadManager(t, true)
path := filepath.Join(getDirectoryPath(t), "test_file")
if err := manager.Upload(path); err == nil {
t.Fatal("the upload request method should have failed and didn't", err)
}
}
func TestSweepSuccess(t *testing.T) {
manager := createUploadManager(t, false)
f, path := createFile(t, "test_file")
defer f.Close()
manager.Start()
time.Sleep(2 * time.Second)
if _, err := os.Stat(path); os.IsExist(err) {
//the file should have been deleted
t.Fatal("the manager failed to delete the file", err)
}
}
func TestSweepFailure(t *testing.T) {
manager := createUploadManager(t, true)
f, path := createFile(t, "test_file")
defer f.Close()
manager.Start()
time.Sleep(2 * time.Second)
_, serr := f.Stat()
if serr != nil {
//the file should still exist
os.Remove(path)
t.Fatal("the manager failed to delete the file", serr)
}
}
func TestHighLoad(t *testing.T) {
manager := createUploadManager(t, false)
for i := 0; i < 30; i++ {
f, _ := createFile(t, randomString(6))
defer f.Close()
}
manager.Start()
time.Sleep(4 * time.Second)
directory := getDirectoryPath(t)
files, err := ioutil.ReadDir(directory)
if err != nil || len(files) > 0 {
t.Fatalf("the manager failed to upload all the files: %s files left: %d", err, len(files))
}
}
// LowerCase [a-z]
const randSet = "abcdefghijklmnopqrstuvwxyz"
// String returns a string of length 'n' from a set of letters 'lset'
func randomString(n int) string {
b := make([]byte, n)
lsetLen := len(randSet)
for i := range b {
b[i] = randSet[rand.Intn(lsetLen)]
}
return string(b)
}
+62
View File
@@ -0,0 +1,62 @@
package awsuploader
import (
"context"
"os"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
)
//FileUploader aws compliant bucket upload
type FileUploader struct {
storage *s3.S3
bucketName string
clientID string
secretID string
}
// NewFileUploader creates a new S3 compliant bucket uploader
func NewFileUploader(bucketName, region, accessKeyID, secretID, token, s3Host string) (*FileUploader, error) {
sess, err := session.NewSession(&aws.Config{
Region: aws.String(region),
Credentials: credentials.NewStaticCredentials(accessKeyID, secretID, token),
})
if err != nil {
return nil, err
}
var storage *s3.S3
if s3Host != "" {
storage = s3.New(sess, &aws.Config{Endpoint: aws.String(s3Host)})
} else {
storage = s3.New(sess)
}
return &FileUploader{
storage: storage,
bucketName: bucketName,
}, nil
}
// Upload a file to the bucket
func (u *FileUploader) Upload(filepath string) error {
info, err := os.Stat(filepath)
if err != nil {
return err
}
file, err := os.Open(filepath)
if err != nil {
return err
}
defer file.Close()
_, serr := u.storage.PutObjectWithContext(context.Background(), &s3.PutObjectInput{
Bucket: aws.String(u.bucketName),
Key: aws.String(info.Name()),
Body: file,
})
return serr
}
+7
View File
@@ -0,0 +1,7 @@
package awsuploader
// UploadManager is used to manage file uploads on an interval
type UploadManager interface {
Upload(string) error
Start()
}
+7
View File
@@ -0,0 +1,7 @@
package awsuploader
// Uploader the functions required to upload to a bucket
type Uploader interface {
//Upload a file to the bucket
Upload(string) error
}
+29
View File
@@ -0,0 +1,29 @@
package buffer
import (
"sync"
)
type Pool struct {
// A Pool must not be copied after first use.
// https://golang.org/pkg/sync/#Pool
buffers sync.Pool
}
func NewPool(bufferSize int) *Pool {
return &Pool{
buffers: sync.Pool{
New: func() interface{} {
return make([]byte, bufferSize)
},
},
}
}
func (p *Pool) Get() []byte {
return p.buffers.Get().([]byte)
}
func (p *Pool) Put(buf []byte) {
p.buffers.Put(buf)
}
+90 -77
View File
@@ -4,7 +4,6 @@
package carrier
import (
"errors"
"io"
"net"
"net/http"
@@ -12,10 +11,25 @@ import (
"strings"
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
"github.com/cloudflare/cloudflared/websocket"
"github.com/sirupsen/logrus"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
)
type StartOptions struct {
OriginURL string
Headers http.Header
}
// Connection wraps up all the needed functions to forward over the tunnel
type Connection interface {
// ServeStream is used to forward data from the client to the edge
ServeStream(*StartOptions, io.ReadWriter) error
// StartServer is used to listen for incoming connections from the edge to the origin
StartServer(net.Listener, string, <-chan struct{}) error
}
// StdinoutStream is empty struct for wrapping stdin/stdout
// into a single ReadWriter
type StdinoutStream struct {
@@ -32,110 +46,109 @@ func (c *StdinoutStream) Write(p []byte) (int, error) {
return os.Stdout.Write(p)
}
// StartClient will copy the data from stdin/stdout over a WebSocket connection
// to the edge (originURL)
func StartClient(logger *logrus.Logger, originURL string, stream io.ReadWriter, headers http.Header) error {
return serveStream(logger, originURL, stream, headers)
// Helper to allow defering the response close with a check that the resp is not nil
func closeRespBody(resp *http.Response) {
if resp != nil {
resp.Body.Close()
}
}
// StartServer will setup a server on a specified port and copy data over a WebSocket connection
// to the edge (originURL)
func StartServer(logger *logrus.Logger, address, originURL string, shutdownC <-chan struct{}, headers http.Header) error {
// StartForwarder will setup a listener on a specified address/port and then
// forward connections to the origin by calling `Serve()`.
func StartForwarder(conn Connection, address string, shutdownC <-chan struct{}, options *StartOptions) error {
listener, err := net.Listen("tcp", address)
if err != nil {
logger.WithError(err).Error("failed to start forwarding server")
return err
return errors.Wrap(err, "failed to start forwarding server")
}
logger.Info("Started listening on ", address)
return Serve(conn, listener, shutdownC, options)
}
// StartClient will copy the data from stdin/stdout over a WebSocket connection
// to the edge (originURL)
func StartClient(conn Connection, stream io.ReadWriter, options *StartOptions) error {
return conn.ServeStream(options, stream)
}
// Serve accepts incoming connections on the specified net.Listener.
// Each connection is handled in a new goroutine: its data is copied over a
// WebSocket connection to the edge (originURL).
// `Serve` always closes `listener`.
func Serve(remoteConn Connection, listener net.Listener, shutdownC <-chan struct{}, options *StartOptions) error {
defer listener.Close()
for {
select {
case <-shutdownC:
return nil
default:
errChan := make(chan error)
go func() {
for {
conn, err := listener.Accept()
if err != nil {
return err
// don't block if parent goroutine quit early
select {
case errChan <- err:
default:
}
return
}
go serveConnection(logger, conn, originURL, headers)
go serveConnection(remoteConn, conn, options)
}
}
}
}()
// serveConnection handles connections for the StartServer call
func serveConnection(logger *logrus.Logger, c net.Conn, originURL string, headers http.Header) {
defer c.Close()
serveStream(logger, originURL, c, headers)
}
// serveStream will serve the data over the WebSocket stream
func serveStream(logger *logrus.Logger, originURL string, conn io.ReadWriter, headers http.Header) error {
wsConn, err := createWebsocketStream(originURL, headers)
if err != nil {
logger.WithError(err).Errorf("failed to connect to %s\n", originURL)
select {
case <-shutdownC:
return nil
case err := <-errChan:
return err
}
defer wsConn.Close()
websocket.Stream(wsConn, conn)
return nil
}
// 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(originURL string, headers http.Header) (*websocket.Conn, error) {
req, err := http.NewRequest(http.MethodGet, originURL, nil)
if err != nil {
return nil, err
}
req.Header = headers
wsConn, resp, err := websocket.ClientConnect(req, nil)
if err != nil && resp != nil && resp.StatusCode > 300 {
location, err := resp.Location()
if err != nil {
return nil, err
}
if !strings.Contains(location.String(), "cdn-cgi/access/login") {
return nil, errors.New("not an Access redirect")
}
req, err := buildAccessRequest(originURL)
if err != nil {
return nil, err
}
wsConn, _, err = websocket.ClientConnect(req, nil)
if err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
return &websocket.Conn{Conn: wsConn}, nil
// serveConnection handles connections for the Serve() call
func serveConnection(remoteConn Connection, c net.Conn, options *StartOptions) {
defer c.Close()
remoteConn.ServeStream(options, c)
}
// buildAccessRequest builds an HTTP request with the Access token set
func buildAccessRequest(originURL string) (*http.Request, error) {
req, err := http.NewRequest(http.MethodGet, originURL, nil)
// IsAccessResponse checks the http Response to see if the url location
// contains the Access structure.
func IsAccessResponse(resp *http.Response) bool {
if resp == nil || resp.StatusCode != http.StatusFound {
return false
}
location, err := resp.Location()
if err != nil || location == nil {
return false
}
if strings.HasPrefix(location.Path, "/cdn-cgi/access/login") {
return true
}
return false
}
// BuildAccessRequest builds an HTTP request with the Access token set
func BuildAccessRequest(options *StartOptions, logger logger.Service) (*http.Request, error) {
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
if err != nil {
return nil, err
}
token, err := token.FetchToken(req.URL)
token, err := token.FetchTokenWithRedirect(req.URL, logger)
if err != nil {
return nil, err
}
// We need to create a new request as FetchToken will modify req (boo mutable)
// as it has to follow redirect on the API and such, so here we init a new one
originRequest, err := http.NewRequest(http.MethodGet, originURL, nil)
originRequest, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
if err != nil {
return nil, err
}
originRequest.Header.Set("cf-access-token", token)
originRequest.Header.Set(h2mux.CFAccessTokenHeader, token)
for k, v := range options.Headers {
if len(v) >= 1 {
originRequest.Header.Set(k, v[0])
}
}
return originRequest, nil
}
+46 -11
View File
@@ -9,8 +9,8 @@ import (
"sync"
"testing"
"github.com/cloudflare/cloudflared/logger"
ws "github.com/gorilla/websocket"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
@@ -43,12 +43,17 @@ func (s *testStreamer) Write(p []byte) (int, error) {
func TestStartClient(t *testing.T) {
message := "Good morning Austin! Time for another sunny day in the great state of Texas."
logger := logrus.New()
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
wsConn := NewWSConnection(logger, false)
ts := newTestWebSocketServer()
defer ts.Close()
buf := newTestStream()
err := StartClient(logger, "http://"+ts.Listener.Addr().String(), buf, nil)
options := &StartOptions{
OriginURL: "http://" + ts.Listener.Addr().String(),
Headers: nil,
}
err := StartClient(wsConn, buf, options)
assert.NoError(t, err)
buf.Write([]byte(message))
@@ -58,24 +63,29 @@ func TestStartClient(t *testing.T) {
}
func TestStartServer(t *testing.T) {
listenerAddress := "localhost:1117"
listener, err := net.Listen("tcp", "localhost:")
if err != nil {
t.Fatalf("Error starting listener: %v", err)
}
message := "Good morning Austin! Time for another sunny day in the great state of Texas."
logger := logrus.New()
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
shutdownC := make(chan struct{})
wsConn := NewWSConnection(logger, false)
ts := newTestWebSocketServer()
defer ts.Close()
options := &StartOptions{
OriginURL: "http://" + ts.Listener.Addr().String(),
Headers: nil,
}
go func() {
err := StartServer(logger, listenerAddress, "http://"+ts.Listener.Addr().String(), shutdownC, nil)
err := Serve(wsConn, listener, shutdownC, options)
if err != nil {
t.Fatalf("Error starting server: %v", err)
t.Fatalf("Error running server: %v", err)
}
}()
conn, err := net.Dial("tcp", listenerAddress)
if err != nil {
t.Fatalf("Error connecting to server: %v", err)
}
conn, err := net.Dial("tcp", listener.Addr().String())
conn.Write([]byte(message))
readBuffer := make([]byte, len(message))
@@ -83,6 +93,31 @@ func TestStartServer(t *testing.T) {
assert.Equal(t, string(readBuffer), message)
}
func TestIsAccessResponse(t *testing.T) {
validLocationHeader := http.Header{}
validLocationHeader.Add("location", "https://test.cloudflareaccess.com/cdn-cgi/access/login/blahblah")
invalidLocationHeader := http.Header{}
invalidLocationHeader.Add("location", "https://google.com")
testCases := []struct {
Description string
In *http.Response
ExpectedOut bool
}{
{"nil response", nil, false},
{"redirect with no location", &http.Response{StatusCode: http.StatusFound}, false},
{"200 ok", &http.Response{StatusCode: http.StatusOK}, false},
{"redirect with location", &http.Response{StatusCode: http.StatusFound, Header: validLocationHeader}, true},
{"redirect with invalid location", &http.Response{StatusCode: http.StatusFound, Header: invalidLocationHeader}, false},
}
for i, tc := range testCases {
if IsAccessResponse(tc.In) != tc.ExpectedOut {
t.Fatalf("Failed case %d -- %s", i, tc.Description)
}
}
}
func newTestWebSocketServer() *httptest.Server {
upgrader := ws.Upgrader{
ReadBufferSize: 1024,
+145
View File
@@ -0,0 +1,145 @@
package carrier
import (
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/socks"
cfwebsocket "github.com/cloudflare/cloudflared/websocket"
"github.com/gorilla/websocket"
)
// Websocket is used to carry data via WS binary frames over the tunnel from client to the origin
// This implements the functions for glider proxy (sock5) and the carrier interface
type Websocket struct {
logger logger.Service
isSocks bool
}
type wsdialer struct {
conn *cfwebsocket.Conn
}
func (d *wsdialer) Dial(address string) (io.ReadWriteCloser, *socks.AddrSpec, error) {
local, ok := d.conn.LocalAddr().(*net.TCPAddr)
if !ok {
return nil, nil, fmt.Errorf("not a tcp connection")
}
addr := socks.AddrSpec{IP: local.IP, Port: local.Port}
return d.conn, &addr, nil
}
// NewWSConnection returns a new connection object
func NewWSConnection(logger logger.Service, isSocks bool) Connection {
return &Websocket{
logger: logger,
isSocks: isSocks,
}
}
// ServeStream will create a Websocket client stream connection to the edge
// it blocks and writes the raw data from conn over the tunnel
func (ws *Websocket) ServeStream(options *StartOptions, conn io.ReadWriter) error {
wsConn, err := createWebsocketStream(options, ws.logger)
if err != nil {
ws.logger.Errorf("failed to connect to %s with error: %s", options.OriginURL, err)
return err
}
defer wsConn.Close()
if ws.isSocks {
dialer := &wsdialer{conn: wsConn}
requestHandler := socks.NewRequestHandler(dialer)
socksServer := socks.NewConnectionHandler(requestHandler)
socksServer.Serve(conn)
} else {
cfwebsocket.Stream(wsConn, conn)
}
return nil
}
// StartServer creates a Websocket server to listen for connections.
// This is used on the origin (tunnel) side to take data from the muxer and send it to the origin
func (ws *Websocket) StartServer(listener net.Listener, remote string, shutdownC <-chan struct{}) error {
return cfwebsocket.StartProxyServer(ws.logger, listener, remote, shutdownC, cfwebsocket.DefaultStreamHandler)
}
// createWebsocketStream will create a WebSocket connection to stream data over
// It also handles redirects from Access and will present that flow if
// the token is not present on the request
func createWebsocketStream(options *StartOptions, logger logger.Service) (*cfwebsocket.Conn, error) {
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
if err != nil {
return nil, err
}
req.Header = options.Headers
dump, err := httputil.DumpRequest(req, false)
logger.Debugf("Websocket request: %s", string(dump))
wsConn, resp, err := cfwebsocket.ClientConnect(req, nil)
defer closeRespBody(resp)
if err != nil && IsAccessResponse(resp) {
wsConn, err = createAccessAuthenticatedStream(options, logger)
if err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
return &cfwebsocket.Conn{Conn: wsConn}, nil
}
// createAccessAuthenticatedStream will try load a token from storage and make
// a connection with the token set on the request. If it still get redirect,
// this probably means the token in storage is invalid (expired/revoked). If that
// happens it deletes the token and runs the connection again, so the user can
// login again and generate a new one.
func createAccessAuthenticatedStream(options *StartOptions, logger logger.Service) (*websocket.Conn, error) {
wsConn, resp, err := createAccessWebSocketStream(options, logger)
defer closeRespBody(resp)
if err == nil {
return wsConn, nil
}
if !IsAccessResponse(resp) {
return nil, err
}
// Access Token is invalid for some reason. Go through regen flow
originReq, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
if err != nil {
return nil, err
}
if err := token.RemoveTokenIfExists(originReq.URL); err != nil {
return nil, err
}
wsConn, resp, err = createAccessWebSocketStream(options, logger)
defer closeRespBody(resp)
if err != nil {
return nil, err
}
return wsConn, nil
}
// createAccessWebSocketStream builds an Access request and makes a connection
func createAccessWebSocketStream(options *StartOptions, logger logger.Service) (*websocket.Conn, *http.Response, error) {
req, err := BuildAccessRequest(options, logger)
if err != nil {
return nil, nil, err
}
dump, err := httputil.DumpRequest(req, false)
logger.Debugf("Access Websocket request: %s", string(dump))
return cfwebsocket.ClientConnect(req, nil)
}
+94
View File
@@ -0,0 +1,94 @@
package certutil
import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"strings"
)
type namedTunnelToken struct {
ZoneID string `json:"zoneID"`
AccountID string `json:"accountID"`
ServiceKey string `json:"serviceKey"`
}
type OriginCert struct {
PrivateKey interface{}
Cert *x509.Certificate
ZoneID string
ServiceKey string
AccountID string
}
func DecodeOriginCert(blocks []byte) (*OriginCert, error) {
if len(blocks) == 0 {
return nil, fmt.Errorf("Cannot decode empty certificate")
}
originCert := OriginCert{}
block, rest := pem.Decode(blocks)
for {
if block == nil {
break
}
switch block.Type {
case "PRIVATE KEY":
if originCert.PrivateKey != nil {
return nil, fmt.Errorf("Found multiple private key in the certificate")
}
// RSA private key
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("Cannot parse private key")
}
originCert.PrivateKey = privateKey
case "CERTIFICATE":
if originCert.Cert != nil {
return nil, fmt.Errorf("Found multiple certificates in the certificate")
}
cert, err := x509.ParseCertificates(block.Bytes)
if err != nil {
return nil, fmt.Errorf("Cannot parse certificate")
} else if len(cert) > 1 {
return nil, fmt.Errorf("Found multiple certificates in the certificate")
}
originCert.Cert = cert[0]
case "WARP TOKEN", "ARGO TUNNEL TOKEN":
if originCert.ZoneID != "" || originCert.ServiceKey != "" {
return nil, fmt.Errorf("Found multiple tokens in the certificate")
}
// The token is a string,
// Try the newer JSON format
ntt := namedTunnelToken{}
if err := json.Unmarshal(block.Bytes, &ntt); err == nil {
originCert.ZoneID = ntt.ZoneID
originCert.ServiceKey = ntt.ServiceKey
originCert.AccountID = ntt.AccountID
} else {
// Try the older format, where the zoneID and service key are seperated by
// a new line character
token := string(block.Bytes)
s := strings.Split(token, "\n")
if len(s) != 2 {
return nil, fmt.Errorf("Cannot parse token")
}
originCert.ZoneID = s[0]
originCert.ServiceKey = s[1]
}
default:
return nil, fmt.Errorf("Unknown block %s in the certificate", block.Type)
}
block, rest = pem.Decode(rest)
}
if originCert.PrivateKey == nil {
return nil, fmt.Errorf("Missing private key in the certificate")
} else if originCert.Cert == nil {
return nil, fmt.Errorf("Missing certificate in the certificate")
} else if originCert.ZoneID == "" || originCert.ServiceKey == "" {
return nil, fmt.Errorf("Missing token in the certificate")
}
return &originCert, nil
}
+67
View File
@@ -0,0 +1,67 @@
package certutil
import (
"fmt"
"io/ioutil"
"testing"
"github.com/stretchr/testify/assert"
)
func TestLoadOriginCert(t *testing.T) {
cert, err := DecodeOriginCert([]byte{})
assert.Equal(t, fmt.Errorf("Cannot decode empty certificate"), err)
assert.Nil(t, cert)
blocks, err := ioutil.ReadFile("test-cert-no-key.pem")
assert.Nil(t, err)
cert, err = DecodeOriginCert(blocks)
assert.Equal(t, fmt.Errorf("Missing private key in the certificate"), err)
assert.Nil(t, cert)
blocks, err = ioutil.ReadFile("test-cert-two-certificates.pem")
assert.Nil(t, err)
cert, err = DecodeOriginCert(blocks)
assert.Equal(t, fmt.Errorf("Found multiple certificates in the certificate"), err)
assert.Nil(t, cert)
blocks, err = ioutil.ReadFile("test-cert-unknown-block.pem")
assert.Nil(t, err)
cert, err = DecodeOriginCert(blocks)
assert.Equal(t, fmt.Errorf("Unknown block RSA PRIVATE KEY in the certificate"), err)
assert.Nil(t, cert)
blocks, err = ioutil.ReadFile("test-cert.pem")
assert.Nil(t, err)
cert, err = DecodeOriginCert(blocks)
assert.Nil(t, err)
assert.NotNil(t, cert)
assert.Equal(t, "7b0a4d77dfb881c1a3b7d61ea9443e19", cert.ZoneID)
key := "v1.0-58bd4f9e28f7b3c28e05a35ff3e80ab4fd9644ef3fece537eb0d12e2e9258217-183442fbb0bbdb3e571558fec9b5589ebd77aafc87498ee3f09f64a4ad79ffe8791edbae08b36c1d8f1d70a8670de56922dff92b15d214a524f4ebfa1958859e-7ce80f79921312a6022c5d25e2d380f82ceaefe3fbdc43dd13b080e3ef1e26f7"
assert.Equal(t, key, cert.ServiceKey)
}
func TestNewlineArgoTunnelToken(t *testing.T) {
ArgoTunnelTokenTest(t, "test-argo-tunnel-cert.pem")
}
func TestJSONArgoTunnelToken(t *testing.T) {
// The given cert's Argo Tunnel Token was generated by base64 encoding this JSON:
// {
// "zoneID": "7b0a4d77dfb881c1a3b7d61ea9443e19",
// "serviceKey": "test-service-key",
// "accountID": "abcdabcdabcdabcd1234567890abcdef"
// }
ArgoTunnelTokenTest(t, "test-argo-tunnel-cert-json.pem")
}
func ArgoTunnelTokenTest(t *testing.T, path string) {
blocks, err := ioutil.ReadFile(path)
assert.Nil(t, err)
cert, err := DecodeOriginCert(blocks)
assert.Nil(t, err)
assert.NotNil(t, cert)
assert.Equal(t, "7b0a4d77dfb881c1a3b7d61ea9443e19", cert.ZoneID)
key := "test-service-key"
assert.Equal(t, key, cert.ServiceKey)
}
+57
View File
@@ -0,0 +1,57 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
uQicoQq3yzeQh20wtrtaXzTNmA==
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
x+Yo/cL8fGfVpPt4UM8=
-----END CERTIFICATE-----
-----BEGIN ARGO TUNNEL TOKEN-----
eyJ6b25lSUQiOiAiN2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkiLCAi
c2VydmljZUtleSI6ICJ0ZXN0LXNlcnZpY2Uta2V5IiwgImFjY291bnRJRCI6ICJh
YmNkYWJjZGFiY2RhYmNkMTIzNDU2Nzg5MGFiY2RlZiJ9
-----END ARGO TUNNEL TOKEN-----
+56
View File
@@ -0,0 +1,56 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
uQicoQq3yzeQh20wtrtaXzTNmA==
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
x+Yo/cL8fGfVpPt4UM8=
-----END CERTIFICATE-----
-----BEGIN ARGO TUNNEL TOKEN-----
N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdGVzdC1zZXJ2aWNlLWtl
eQ==
-----END ARGO TUNNEL TOKEN-----
+33
View File
@@ -0,0 +1,33 @@
-----BEGIN CERTIFICATE-----
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
x+Yo/cL8fGfVpPt4UM8=
-----END CERTIFICATE-----
-----BEGIN WARP TOKEN-----
N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdjEuMC01OGJkNGY5ZTI4
ZjdiM2MyOGUwNWEzNWZmM2U4MGFiNGZkOTY0NGVmM2ZlY2U1MzdlYjBkMTJlMmU5
MjU4MjE3LTE4MzQ0MmZiYjBiYmRiM2U1NzE1NThmZWM5YjU1ODllYmQ3N2FhZmM4
NzQ5OGVlM2YwOWY2NGE0YWQ3OWZmZTg3OTFlZGJhZTA4YjM2YzFkOGYxZDcwYTg2
NzBkZTU2OTIyZGZmOTJiMTVkMjE0YTUyNGY0ZWJmYTE5NTg4NTllLTdjZTgwZjc5
OTIxMzEyYTYwMjJjNWQyNWUyZDM4MGY4MmNlYWVmZTNmYmRjNDNkZDEzYjA4MGUz
ZWYxZTI2Zjc=
-----END WARP TOKEN-----
+85
View File
@@ -0,0 +1,85 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
uQicoQq3yzeQh20wtrtaXzTNmA==
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
x+Yo/cL8fGfVpPt4UM8=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
x+Yo/cL8fGfVpPt4UM8=
-----END CERTIFICATE-----
-----BEGIN WARP TOKEN-----
N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdjEuMC01OGJkNGY5ZTI4
ZjdiM2MyOGUwNWEzNWZmM2U4MGFiNGZkOTY0NGVmM2ZlY2U1MzdlYjBkMTJlMmU5
MjU4MjE3LTE4MzQ0MmZiYjBiYmRiM2U1NzE1NThmZWM5YjU1ODllYmQ3N2FhZmM4
NzQ5OGVlM2YwOWY2NGE0YWQ3OWZmZTg3OTFlZGJhZTA4YjM2YzFkOGYxZDcwYTg2
NzBkZTU2OTIyZGZmOTJiMTVkMjE0YTUyNGY0ZWJmYTE5NTg4NTllLTdjZTgwZjc5
OTIxMzEyYTYwMjJjNWQyNWUyZDM4MGY4MmNlYWVmZTNmYmRjNDNkZDEzYjA4MGUz
ZWYxZTI2Zjc=
-----END WARP TOKEN-----
+89
View File
@@ -0,0 +1,89 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
uQicoQq3yzeQh20wtrtaXzTNmA==
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
x+Yo/cL8fGfVpPt4UM8=
-----END CERTIFICATE-----
-----BEGIN WARP TOKEN-----
N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdjEuMC01OGJkNGY5ZTI4
ZjdiM2MyOGUwNWEzNWZmM2U4MGFiNGZkOTY0NGVmM2ZlY2U1MzdlYjBkMTJlMmU5
MjU4MjE3LTE4MzQ0MmZiYjBiYmRiM2U1NzE1NThmZWM5YjU1ODllYmQ3N2FhZmM4
NzQ5OGVlM2YwOWY2NGE0YWQ3OWZmZTg3OTFlZGJhZTA4YjM2YzFkOGYxZDcwYTg2
NzBkZTU2OTIyZGZmOTJiMTVkMjE0YTUyNGY0ZWJmYTE5NTg4NTllLTdjZTgwZjc5
OTIxMzEyYTYwMjJjNWQyNWUyZDM4MGY4MmNlYWVmZTNmYmRjNDNkZDEzYjA4MGUz
ZWYxZTI2Zjc=
-----END WARP TOKEN-----
-----BEGIN RSA PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
uQicoQq3yzeQh20wtrtaXzTNmA==
-----END RSA PRIVATE KEY-----
+61
View File
@@ -0,0 +1,61 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
uQicoQq3yzeQh20wtrtaXzTNmA==
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
x+Yo/cL8fGfVpPt4UM8=
-----END CERTIFICATE-----
-----BEGIN WARP TOKEN-----
N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdjEuMC01OGJkNGY5ZTI4
ZjdiM2MyOGUwNWEzNWZmM2U4MGFiNGZkOTY0NGVmM2ZlY2U1MzdlYjBkMTJlMmU5
MjU4MjE3LTE4MzQ0MmZiYjBiYmRiM2U1NzE1NThmZWM5YjU1ODllYmQ3N2FhZmM4
NzQ5OGVlM2YwOWY2NGE0YWQ3OWZmZTg3OTFlZGJhZTA4YjM2YzFkOGYxZDcwYTg2
NzBkZTU2OTIyZGZmOTJiMTVkMjE0YTUyNGY0ZWJmYTE5NTg4NTllLTdjZTgwZjc5
OTIxMzEyYTYwMjJjNWQyNWUyZDM4MGY4MmNlYWVmZTNmYmRjNDNkZDEzYjA4MGUz
ZWYxZTI2Zjc=
-----END WARP TOKEN-----
+33 -18
View File
@@ -1,5 +1,6 @@
pinned_go: &pinned_go go=1.11.5-1
build_dir: &build_dir /cfsetup_build/src/github.com/cloudflare/cloudflared/
pinned_go: &pinned_go go=1.12.9-1
build_dir: &build_dir /cfsetup_build
default-flavor: stretch
stretch: &stretch
build:
build_dir: *build_dir
@@ -7,7 +8,6 @@ stretch: &stretch
- *pinned_go
- build-essential
post-cache:
- export GOPATH=/cfsetup_build/
- export GOOS=linux
- export GOARCH=amd64
- make cloudflared
@@ -19,7 +19,6 @@ stretch: &stretch
- fakeroot
- rubygem-fpm
post-cache:
- export GOPATH=/cfsetup_build/
- export GOOS=linux
- export GOARCH=amd64
- make cloudflared-deb
@@ -29,7 +28,6 @@ stretch: &stretch
- *pinned_go
- build-essential
post-cache:
- export GOPATH=/cfsetup_build/
- export GOOS=linux
- export GOARCH=amd64
- make release
@@ -40,28 +38,25 @@ stretch: &stretch
- crossbuild-essential-armhf
- gcc-arm-linux-gnueabihf
post-cache:
- export GOPATH=/cfsetup_build/
- export GOOS=linux
- export GOARCH=arm
- export CC=arm-linux-gnueabihf-gcc
- make release
release-linux-386:
build_dir: *build_dir
builddeps:
- *pinned_go
- gcc-multilib
post-cache:
- export GOPATH=/cfsetup_build/
- export GOOS=linux
- export GOARCH=386
- make release
build_dir: *build_dir
builddeps:
- *pinned_go
- gcc-multilib
post-cache:
- export GOOS=linux
- export GOARCH=386
- make release
release-windows-amd64:
build_dir: *build_dir
builddeps:
- *pinned_go
- gcc-mingw-w64
post-cache:
- export GOPATH=/cfsetup_build/
- export GOOS=windows
- export GOARCH=amd64
- export CC=x86_64-w64-mingw32-gcc
@@ -72,7 +67,6 @@ stretch: &stretch
- *pinned_go
- gcc-mingw-w64
post-cache:
- export GOPATH=/cfsetup_build/
- export GOOS=windows
- export GOARCH=386
- export CC=i686-w64-mingw32-gcc-win32
@@ -83,9 +77,30 @@ stretch: &stretch
- *pinned_go
- build-essential
post-cache:
- export GOPATH=/cfsetup_build/
- export GOOS=linux
- export GOARCH=amd64
# cd to a non-module directory: https://github.com/golang/go/issues/24250
- (cd / && go get github.com/BurntSushi/go-sumtype)
- export PATH="$HOME/go/bin:$PATH"
- make test
update-homebrew:
builddeps:
- openssh-client
- s3cmd
post-cache:
- .teamcity/update-homebrew.sh
jessie: *stretch
buster: *stretch
# 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
+97 -15
View File
@@ -6,44 +6,126 @@ import (
"strings"
"github.com/cloudflare/cloudflared/carrier"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/validation"
"github.com/pkg/errors"
cli "gopkg.in/urfave/cli.v2"
)
// StartForwarder starts a client side websocket forward
func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, logger logger.Service) error {
validURLString, err := validation.ValidateUrl(forwarder.Listener)
if err != nil {
return errors.Wrap(err, "error validating origin URL")
}
validURL, err := url.Parse(validURLString)
if err != nil {
return errors.Wrap(err, "error parsing origin URL")
}
// get the headers from the config file and add to the request
headers := make(http.Header)
if forwarder.TokenClientID != "" {
headers.Set(h2mux.CFAccessClientIDHeader, forwarder.TokenClientID)
}
if forwarder.TokenSecret != "" {
headers.Set(h2mux.CFAccessClientSecretHeader, forwarder.TokenSecret)
}
if forwarder.Destination != "" {
headers.Add(h2mux.CFJumpDestinationHeader, forwarder.Destination)
}
options := &carrier.StartOptions{
OriginURL: forwarder.URL,
Headers: headers, //TODO: TUN-2688 support custom headers from config file
}
// we could add a cmd line variable for this bool if we want the SOCK5 server to be on the client side
wsConn := carrier.NewWSConnection(logger, false)
logger.Infof("Start Websocket listener on: %s", validURL.Host)
return carrier.StartForwarder(wsConn, validURL.Host, shutdown, options)
}
// ssh will start a WS proxy server for server mode
// or copy from stdin/stdout for client mode
// useful for proxying other protocols (like ssh) over websockets
// (which you can put Access in front of)
func ssh(c *cli.Context) error {
hostname, err := validation.ValidateHostname(c.String("hostname"))
if err != nil || c.String("hostname") == "" {
return cli.ShowCommandHelp(c, "ssh")
}
headers := buildRequestHeaders(c.StringSlice("header"))
if c.IsSet("service-token-id") {
headers.Add("CF-Access-Client-Id", c.String("service-token-id"))
}
if c.IsSet("service-token-secret") {
headers.Add("CF-Access-Client-Secret", c.String("service-token-secret"))
logDirectory, logLevel := config.FindLogSettings()
flagLogDirectory := c.String(sshLogDirectoryFlag)
if flagLogDirectory != "" {
logDirectory = flagLogDirectory
}
if c.NArg() > 0 || c.IsSet("url") {
flagLogLevel := c.String(sshLogLevelFlag)
if flagLogLevel != "" {
logLevel = flagLogLevel
}
logger, err := logger.New(logger.DefaultFile(logDirectory), logger.LogLevelString(logLevel))
if err != nil {
return cliutil.PrintLoggerSetupError("error setting up logger", err)
}
// get the hostname from the cmdline and error out if its not provided
rawHostName := c.String(sshHostnameFlag)
hostname, err := validation.ValidateHostname(rawHostName)
if err != nil || rawHostName == "" {
return cli.ShowCommandHelp(c, "ssh")
}
originURL := ensureURLScheme(hostname)
// get the headers from the cmdline and add them
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
if c.IsSet(sshTokenIDFlag) {
headers.Set(h2mux.CFAccessClientIDHeader, c.String(sshTokenIDFlag))
}
if c.IsSet(sshTokenSecretFlag) {
headers.Set(h2mux.CFAccessClientSecretHeader, c.String(sshTokenSecretFlag))
}
destination := c.String(sshDestinationFlag)
if destination != "" {
headers.Add(h2mux.CFJumpDestinationHeader, destination)
}
options := &carrier.StartOptions{
OriginURL: originURL,
Headers: headers,
}
// we could add a cmd line variable for this bool if we want the SOCK5 server to be on the client side
wsConn := carrier.NewWSConnection(logger, false)
if c.NArg() > 0 || c.IsSet(sshURLFlag) {
localForwarder, err := config.ValidateUrl(c)
if err != nil {
logger.WithError(err).Error("Error validating origin URL")
logger.Errorf("Error validating origin URL: %s", err)
return errors.Wrap(err, "error validating origin URL")
}
forwarder, err := url.Parse(localForwarder)
if err != nil {
logger.WithError(err).Error("Error validating origin URL")
logger.Errorf("Error validating origin URL: %s", err)
return errors.Wrap(err, "error validating origin URL")
}
return carrier.StartServer(logger, forwarder.Host, "https://"+hostname, shutdownC, headers)
logger.Infof("Start Websocket listener on: %s", forwarder.Host)
err = carrier.StartForwarder(wsConn, forwarder.Host, shutdownC, options)
if err != nil {
logger.Errorf("Error on Websocket listener: %s", err)
}
return err
}
return carrier.StartClient(logger, "https://"+hostname, &carrier.StdinoutStream{}, headers)
return carrier.StartClient(wsConn, &carrier.StdinoutStream{}, options)
}
func buildRequestHeaders(values []string) http.Header {
+257 -48
View File
@@ -1,25 +1,60 @@
package access
import (
"errors"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"text/template"
"time"
"github.com/cloudflare/cloudflared/carrier"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/sshgen"
"github.com/cloudflare/cloudflared/validation"
"github.com/pkg/errors"
"golang.org/x/net/idna"
"github.com/cloudflare/cloudflared/log"
raven "github.com/getsentry/raven-go"
cli "gopkg.in/urfave/cli.v2"
"github.com/getsentry/raven-go"
"gopkg.in/urfave/cli.v2"
)
const (
sshHostnameFlag = "hostname"
sshDestinationFlag = "destination"
sshURLFlag = "url"
sshHeaderFlag = "header"
sshTokenIDFlag = "service-token-id"
sshTokenSecretFlag = "service-token-secret"
sshGenCertFlag = "short-lived-cert"
sshLogDirectoryFlag = "log-directory"
sshLogLevelFlag = "log-level"
sshConfigTemplate = `
Add to your {{.Home}}/.ssh/config:
Host {{.Hostname}}
{{- if .ShortLivedCerts}}
ProxyCommand bash -c '{{.Cloudflared}} access ssh-gen --hostname %h; ssh -tt %r@cfpipe-{{.Hostname}} >&2 <&1'
Host cfpipe-{{.Hostname}}
HostName {{.Hostname}}
ProxyCommand {{.Cloudflared}} access ssh --hostname %h
IdentityFile ~/.cloudflared/{{.Hostname}}-cf_key
CertificateFile ~/.cloudflared/{{.Hostname}}-cf_key-cert.pub
{{- else}}
ProxyCommand {{.Cloudflared}} access ssh --hostname %h
{{end}}
`
)
const sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b@sentry.io/189878"
var (
logger = log.CreateLogger()
shutdownC chan struct{}
graceShutdownC chan struct{}
)
@@ -39,17 +74,17 @@ func Commands() []*cli.Command {
return []*cli.Command{
{
Name: "access",
Category: "Access (BETA)",
Aliases: []string{"forward"},
Category: "Access",
Usage: "access <subcommand>",
Description: `(BETA) Cloudflare Access protects internal resources by securing, authenticating and monitoring access
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. This feature is considered beta. Your feedback is greatly appreciated!
https://cfl.re/CLIAuthBeta`,
applications from the command line.`,
Subcommands: []*cli.Command{
{
Name: "login",
Action: login,
Action: cliutil.ErrorHandler(login),
Usage: "login <url of access application>",
Description: `The login subcommand initiates an authentication flow with your identity provider.
The subcommand will launch a browser. For headless systems, a url is provided.
@@ -65,7 +100,7 @@ func Commands() []*cli.Command {
},
{
Name: "curl",
Action: curl,
Action: cliutil.ErrorHandler(curl),
Usage: "curl [--allow-request, -ar] <url> [<curl args>...]",
Description: `The curl subcommand wraps curl and automatically injects the JWT into a cf-access-token
header when using curl to reach an application behind Access.`,
@@ -74,7 +109,7 @@ func Commands() []*cli.Command {
},
{
Name: "token",
Action: generateToken,
Action: cliutil.ErrorHandler(generateToken),
Usage: "token -app=<url of access application>",
ArgsUsage: "url of Access application",
Description: `The token subcommand produces a JWT which can be used to authenticate requests.`,
@@ -85,43 +120,81 @@ func Commands() []*cli.Command {
},
},
{
Name: "ssh",
Action: ssh,
Aliases: []string{"rdp"},
Name: "tcp",
Action: cliutil.ErrorHandler(ssh),
Aliases: []string{"rdp", "ssh", "smb"},
Usage: "",
ArgsUsage: "",
Description: `The ssh subcommand sends data over a proxy to the Cloudflare edge.`,
Description: `The tcp subcommand sends data over a proxy to the Cloudflare edge.`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "hostname",
Usage: "specifics the hostname of your application.",
Name: sshHostnameFlag,
Aliases: []string{"tunnel-host", "T"},
Usage: "specify the hostname of your application.",
},
&cli.StringFlag{
Name: "url",
Usage: "specifics the host:port to forward data to Cloudflare edge.",
Name: sshDestinationFlag,
Usage: "specify the destination address of your SSH server.",
},
&cli.StringFlag{
Name: sshURLFlag,
Aliases: []string{"listener", "L"},
Usage: "specify the host:port to forward data to Cloudflare edge.",
},
&cli.StringSliceFlag{
Name: "header",
Name: sshHeaderFlag,
Aliases: []string{"H"},
Usage: "specific additional headers you wish to send.",
Usage: "specify additional headers you wish to send.",
},
&cli.StringSliceFlag{
Name: "service-token-id",
&cli.StringFlag{
Name: sshTokenIDFlag,
Aliases: []string{"id"},
Usage: "specific an Access service token ID you wish to use.",
Usage: "specify an Access service token ID you wish to use.",
},
&cli.StringSliceFlag{
Name: "service-token-secret",
&cli.StringFlag{
Name: sshTokenSecretFlag,
Aliases: []string{"secret"},
Usage: "specific an Access service token secret you wish to use.",
Usage: "specify an Access service token secret you wish to use.",
},
&cli.StringFlag{
Name: sshLogDirectoryFlag,
Aliases: []string{"logfile"}, //added to match the tunnel side
Usage: "Save application log to this directory for reporting issues.",
},
&cli.StringFlag{
Name: sshLogLevelFlag,
Aliases: []string{"loglevel"}, //added to match the tunnel side
Usage: "Application logging level {fatal, error, info, debug}. ",
},
},
},
{
Name: "ssh-config",
Action: sshConfig,
Usage: "ssh-config",
Action: cliutil.ErrorHandler(sshConfig),
Usage: "",
Description: `Prints an example configuration ~/.ssh/config`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: sshHostnameFlag,
Usage: "specify the hostname of your application.",
},
&cli.BoolFlag{
Name: sshGenCertFlag,
Usage: "specify if you wish to generate short lived certs.",
},
},
},
{
Name: "ssh-gen",
Action: cliutil.ErrorHandler(sshGen),
Usage: "",
Description: `Generates a short lived certificate for given hostname`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: sshHostnameFlag,
Usage: "specify the hostname of your application.",
},
},
},
},
},
@@ -130,28 +203,60 @@ func Commands() []*cli.Command {
// login pops up the browser window to do the actual login and JWT generation
func login(c *cli.Context) error {
raven.SetDSN(sentryDSN)
logger := log.CreateLogger()
if err := raven.SetDSN(sentryDSN); err != nil {
return err
}
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
args := c.Args()
appURL, err := url.Parse(args.First())
rawURL := ensureURLScheme(args.First())
appURL, err := url.Parse(rawURL)
if args.Len() < 1 || err != nil {
logger.Errorf("Please provide the url of the Access application\n")
return err
}
token, err := token.FetchToken(appURL)
if err != nil {
logger.Errorf("Failed to fetch token: %s\n", err)
if err := verifyTokenAtEdge(appURL, c, logger); err != nil {
logger.Errorf("Could not verify token: %s", err)
return err
}
fmt.Fprintf(os.Stdout, "Successfully fetched your token:\n\n%s\n\n", string(token))
cfdToken, err := token.GetTokenIfExists(appURL)
if err != nil {
fmt.Fprintln(os.Stderr, "Unable to find token for provided application.")
return err
} else if cfdToken == "" {
fmt.Fprintln(os.Stderr, "token for provided application was empty.")
return errors.New("empty application token")
}
fmt.Fprintf(os.Stdout, "Successfully fetched your token:\n\n%s\n\n", cfdToken)
return nil
}
// ensureURLScheme prepends a URL with https:// if it doesnt have a scheme. http:// URLs will not be converted.
func ensureURLScheme(url string) string {
url = strings.Replace(strings.ToLower(url), "http://", "https://", 1)
if !strings.HasPrefix(url, "https://") {
url = fmt.Sprintf("https://%s", url)
}
return url
}
// curl provides a wrapper around curl, passing Access JWT along in request
func curl(c *cli.Context) error {
raven.SetDSN(sentryDSN)
logger := log.CreateLogger()
if err := raven.SetDSN(sentryDSN); err != nil {
return err
}
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
args := c.Args()
if args.Len() < 1 {
logger.Error("Please provide the access app and command you wish to run.")
@@ -159,7 +264,7 @@ func curl(c *cli.Context) error {
}
cmdArgs, allowRequest := parseAllowRequest(args.Slice())
appURL, err := getAppURL(cmdArgs)
appURL, err := getAppURL(cmdArgs, logger)
if err != nil {
return err
}
@@ -167,24 +272,26 @@ func curl(c *cli.Context) error {
tok, err := token.GetTokenIfExists(appURL)
if err != nil || tok == "" {
if allowRequest {
logger.Warn("You don't have an Access token set. Please run access token <access application> to fetch one.")
logger.Info("You don't have an Access token set. Please run access token <access application> to fetch one.")
return shell.Run("curl", cmdArgs...)
}
tok, err = token.FetchToken(appURL)
tok, err = token.FetchToken(appURL, logger)
if err != nil {
logger.Error("Failed to refresh token: ", err)
logger.Errorf("Failed to refresh token: %s", err)
return err
}
}
cmdArgs = append(cmdArgs, "-H")
cmdArgs = append(cmdArgs, fmt.Sprintf("cf-access-token: %s", tok))
cmdArgs = append(cmdArgs, fmt.Sprintf("%s: %s", h2mux.CFAccessTokenHeader, tok))
return shell.Run("curl", cmdArgs...)
}
// token dumps provided token to stdout
func generateToken(c *cli.Context) error {
raven.SetDSN(sentryDSN)
if err := raven.SetDSN(sentryDSN); err != nil {
return err
}
appURL, err := url.Parse(c.String("app"))
if err != nil || c.NumFlags() < 1 {
fmt.Fprintln(os.Stderr, "Please provide a url.")
@@ -205,13 +312,59 @@ func generateToken(c *cli.Context) error {
// sshConfig prints an example SSH config to stdout
func sshConfig(c *cli.Context) error {
outputMessage := "Add this configuration block to your %s/.ssh/config:\n\nHost [your hostname]\n\tProxyCommand %s access ssh --hostname %%h\n"
logger.Printf(outputMessage, os.Getenv("HOME"), cloudflaredPath())
genCertBool := c.Bool(sshGenCertFlag)
hostname := c.String(sshHostnameFlag)
if hostname == "" {
hostname = "[your hostname]"
}
type config struct {
Home string
ShortLivedCerts bool
Hostname string
Cloudflared string
}
t := template.Must(template.New("sshConfig").Parse(sshConfigTemplate))
return t.Execute(os.Stdout, config{Home: os.Getenv("HOME"), ShortLivedCerts: genCertBool, Hostname: hostname, Cloudflared: cloudflaredPath()})
}
// sshGen generates a short lived certificate for provided hostname
func sshGen(c *cli.Context) error {
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
// get the hostname from the cmdline and error out if its not provided
rawHostName := c.String(sshHostnameFlag)
hostname, err := validation.ValidateHostname(rawHostName)
if err != nil || rawHostName == "" {
return cli.ShowCommandHelp(c, "ssh-gen")
}
originURL, err := url.Parse(ensureURLScheme(hostname))
if err != nil {
return err
}
// this fetchToken function mutates the appURL param. We should refactor that
fetchTokenURL := &url.URL{}
*fetchTokenURL = *originURL
cfdToken, err := token.FetchTokenWithRedirect(fetchTokenURL, logger)
if err != nil {
return err
}
if err := sshgen.GenerateShortLivedCertificate(originURL, cfdToken); err != nil {
return err
}
return nil
}
// getAppURL will pull the appURL needed for fetching a user's Access token
func getAppURL(cmdArgs []string) (*url.URL, error) {
func getAppURL(cmdArgs []string, logger logger.Service) (*url.URL, error) {
if len(cmdArgs) < 1 {
logger.Error("Please provide a valid URL as the first argument to curl.")
return nil, errors.New("not a valid url")
@@ -281,3 +434,59 @@ func isFileThere(candidate string) bool {
}
return true
}
// verifyTokenAtEdge checks for a token on disk, or generates a new one.
// Then makes a request to to the origin with the token to ensure it is valid.
// Returns nil if token is valid.
func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context, logger logger.Service) error {
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
if c.IsSet(sshTokenIDFlag) {
headers.Add(h2mux.CFAccessClientIDHeader, c.String(sshTokenIDFlag))
}
if c.IsSet(sshTokenSecretFlag) {
headers.Add(h2mux.CFAccessClientSecretHeader, c.String(sshTokenSecretFlag))
}
options := &carrier.StartOptions{OriginURL: appUrl.String(), Headers: headers}
if valid, err := isTokenValid(options, logger); err != nil {
return err
} else if valid {
return nil
}
if err := token.RemoveTokenIfExists(appUrl); err != nil {
return err
}
if valid, err := isTokenValid(options, logger); err != nil {
return err
} else if !valid {
return errors.New("failed to verify token")
}
return nil
}
// isTokenValid makes a request to the origin and returns true if the response was not a 302.
func isTokenValid(options *carrier.StartOptions, logger logger.Service) (bool, error) {
req, err := carrier.BuildAccessRequest(options, logger)
if err != nil {
return false, errors.Wrap(err, "Could not create access request")
}
// Do not follow redirects
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
Timeout: time.Second * 5,
}
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
// A redirect to login means the token was invalid.
return !carrier.IsAccessResponse(resp), nil
}
+25
View File
@@ -0,0 +1,25 @@
package access
import "testing"
func Test_ensureURLScheme(t *testing.T) {
type args struct {
url string
}
tests := []struct {
name string
args args
want string
}{
{"no scheme", args{"localhost:123"}, "https://localhost:123"},
{"http scheme", args{"http://test"}, "https://test"},
{"https scheme", args{"https://test"}, "https://test"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ensureURLScheme(tt.args.url); got != tt.want {
t.Errorf("ensureURLScheme() = %v, want %v", got, tt.want)
}
})
}
}
+50
View File
@@ -0,0 +1,50 @@
package main
import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
)
// ForwardServiceType is used to identify what kind of overwatch service this is
const ForwardServiceType = "forward"
// ForwarderService is used to wrap the access package websocket forwarders
// into a service model for the overwatch package.
// it also holds a reference to the config object that represents its state
type ForwarderService struct {
forwarder config.Forwarder
shutdown chan struct{}
logger logger.Service
}
// NewForwardService creates a new forwarder service
func NewForwardService(f config.Forwarder, logger logger.Service) *ForwarderService {
return &ForwarderService{forwarder: f, shutdown: make(chan struct{}, 1), logger: logger}
}
// Name is used to figure out this service is related to the others (normally the addr it binds to)
// e.g. localhost:78641 or 127.0.0.1:2222 since this is a websocket forwarder
func (s *ForwarderService) Name() string {
return s.forwarder.Listener
}
// Type is used to identify what kind of overwatch service this is
func (s *ForwarderService) Type() string {
return ForwardServiceType
}
// Hash is used to figure out if this forwarder is the unchanged or not from the config file updates
func (s *ForwarderService) Hash() string {
return s.forwarder.Hash()
}
// Shutdown stops the websocket listener
func (s *ForwarderService) Shutdown() {
s.shutdown <- struct{}{}
}
// Run is the run loop that is started by the overwatch service
func (s *ForwarderService) Run() error {
return access.StartForwarder(s.forwarder, s.shutdown, s.logger)
}
+73
View File
@@ -0,0 +1,73 @@
package main
import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tunneldns"
)
// ResolverServiceType is used to identify what kind of overwatch service this is
const ResolverServiceType = "resolver"
// ResolverService is used to wrap the tunneldns package's DNS over HTTP
// into a service model for the overwatch package.
// it also holds a reference to the config object that represents its state
type ResolverService struct {
resolver config.DNSResolver
shutdown chan struct{}
logger logger.Service
}
// NewResolverService creates a new resolver service
func NewResolverService(r config.DNSResolver, logger logger.Service) *ResolverService {
return &ResolverService{resolver: r,
shutdown: make(chan struct{}),
logger: logger,
}
}
// Name is used to figure out this service is related to the others (normally the addr it binds to)
// this is just "resolver" since there can only be one DNS resolver running
func (s *ResolverService) Name() string {
return ResolverServiceType
}
// Type is used to identify what kind of overwatch service this is
func (s *ResolverService) Type() string {
return ResolverServiceType
}
// Hash is used to figure out if this forwarder is the unchanged or not from the config file updates
func (s *ResolverService) Hash() string {
return s.resolver.Hash()
}
// Shutdown stops the tunneldns listener
func (s *ResolverService) Shutdown() {
s.shutdown <- struct{}{}
}
// Run is the run loop that is started by the overwatch service
func (s *ResolverService) Run() error {
// create a listener
l, err := tunneldns.CreateListener(s.resolver.AddressOrDefault(), s.resolver.PortOrDefault(),
s.resolver.UpstreamsOrDefault(), s.resolver.BootstrapsOrDefault(), s.logger)
if err != nil {
return err
}
// start the listener.
readySignal := make(chan struct{})
err = l.Start(readySignal)
if err != nil {
l.Stop()
return err
}
<-readySignal
s.logger.Infof("start resolver on: %s:%d", s.resolver.AddressOrDefault(), s.resolver.PortOrDefault())
// wait for shutdown signal
<-s.shutdown
s.logger.Infof("shutdown on: %s:%d", s.resolver.AddressOrDefault(), s.resolver.PortOrDefault())
return l.Stop()
}
+90
View File
@@ -0,0 +1,90 @@
package main
import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/overwatch"
)
// AppService is the main service that runs when no command lines flags are passed to cloudflared
// it manages all the running services such as tunnels, forwarders, DNS resolver, etc
type AppService struct {
configManager config.Manager
serviceManager overwatch.Manager
shutdownC chan struct{}
configUpdateChan chan config.Root
logger logger.Service
}
// NewAppService creates a new AppService with needed supporting services
func NewAppService(configManager config.Manager, serviceManager overwatch.Manager, shutdownC chan struct{}, logger logger.Service) *AppService {
return &AppService{
configManager: configManager,
serviceManager: serviceManager,
shutdownC: shutdownC,
configUpdateChan: make(chan config.Root),
logger: logger,
}
}
// Run starts the run loop to handle config updates and run forwarders, tunnels, etc
func (s *AppService) Run() error {
go s.actionLoop()
return s.configManager.Start(s)
}
// Shutdown kills all the running services
func (s *AppService) Shutdown() error {
s.configManager.Shutdown()
s.shutdownC <- struct{}{}
return nil
}
// ConfigDidUpdate is a delegate notification from the config manager
// it is trigger when the config file has been updated and now the service needs
// to update its services accordingly
func (s *AppService) ConfigDidUpdate(c config.Root) {
s.configUpdateChan <- c
}
// actionLoop handles the actions from running processes
func (s *AppService) actionLoop() {
for {
select {
case c := <-s.configUpdateChan:
s.handleConfigUpdate(c)
case <-s.shutdownC:
for _, service := range s.serviceManager.Services() {
service.Shutdown()
}
return
}
}
}
func (s *AppService) handleConfigUpdate(c config.Root) {
// handle the client forward listeners
activeServices := map[string]struct{}{}
for _, f := range c.Forwarders {
service := NewForwardService(f, s.logger)
s.serviceManager.Add(service)
activeServices[service.Name()] = struct{}{}
}
// handle resolver changes
if c.Resolver.Enabled {
service := NewResolverService(c.Resolver, s.logger)
s.serviceManager.Add(service)
activeServices[service.Name()] = struct{}{}
}
// TODO: TUN-1451 - tunnels
// remove any services that are no longer active
for _, service := range s.serviceManager.Services() {
if _, ok := activeServices[service.Name()]; !ok {
s.serviceManager.Remove(service.Name())
}
}
}
+28
View File
@@ -0,0 +1,28 @@
package buildinfo
import (
"runtime"
"github.com/cloudflare/cloudflared/logger"
)
type BuildInfo struct {
GoOS string `json:"go_os"`
GoVersion string `json:"go_version"`
GoArch string `json:"go_arch"`
CloudflaredVersion string `json:"cloudflared_version"`
}
func GetBuildInfo(cloudflaredVersion string) *BuildInfo {
return &BuildInfo{
GoOS: runtime.GOOS,
GoVersion: runtime.Version(),
GoArch: runtime.GOARCH,
CloudflaredVersion: cloudflaredVersion,
}
}
func (bi *BuildInfo) Log(logger logger.Service) {
logger.Infof("Version %s", bi.CloudflaredVersion)
logger.Infof("GOOS: %s, GOVersion: %s, GoArch: %s", bi.GoOS, bi.GoVersion, bi.GoArch)
}
+55
View File
@@ -0,0 +1,55 @@
package cliutil
import (
"fmt"
"log"
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
"gopkg.in/urfave/cli.v2"
)
type usageError string
func (ue usageError) Error() string {
return string(ue)
}
func UsageError(format string, args ...interface{}) error {
if len(args) == 0 {
return usageError(format)
} else {
msg := fmt.Sprintf(format, args...)
return usageError(msg)
}
}
// Ensures exit with error code if actionFunc returns an error
func ErrorHandler(actionFunc cli.ActionFunc) cli.ActionFunc {
return func(ctx *cli.Context) error {
err := actionFunc(ctx)
if err != nil {
if _, ok := err.(usageError); ok {
msg := fmt.Sprintf("%s\nSee 'cloudflared %s --help'.", err.Error(), ctx.Command.FullName())
return cli.Exit(msg, -1)
}
// os.Exits with error code if err is cli.ExitCoder or cli.MultiError
cli.HandleExitCoder(err)
err = cli.Exit(err.Error(), 1)
}
logger.SharedWriteManager.Shutdown()
return err
}
}
// PrintLoggerSetupError returns an error to stdout to notify when a logger can't start
func PrintLoggerSetupError(msg string, err error) error {
l, le := logger.New()
if le != nil {
log.Printf("%s: %s", msg, err)
} else {
l.Errorf("%s: %s", msg, err)
}
return errors.Wrap(err, msg)
}
+30
View File
@@ -9,6 +9,7 @@ import (
homedir "github.com/mitchellh/go-homedir"
"gopkg.in/urfave/cli.v2"
"gopkg.in/urfave/cli.v2/altsrc"
"gopkg.in/yaml.v2"
)
var (
@@ -63,6 +64,35 @@ func FindDefaultConfigPath() string {
return ""
}
// FindLogSettings gets the log directory and level from the config file
func FindLogSettings() (string, string) {
configPath := FindDefaultConfigPath()
defaultDirectory := filepath.Dir(configPath)
defaultLevel := "info"
file, err := os.Open(configPath)
if err != nil {
return defaultDirectory, defaultLevel
}
defer file.Close()
var config Root
if err := yaml.NewDecoder(file).Decode(&config); err != nil {
return defaultDirectory, defaultLevel
}
directory := defaultDirectory
if config.LogDirectory != "" {
directory = config.LogDirectory
}
level := defaultLevel
if config.LogLevel != "" {
level = config.LogLevel
}
return directory, level
}
// ValidateUnixSocket ensures --unix-socket param is used exclusively
// i.e. it fails if a user specifies both --url and --unix-socket
func ValidateUnixSocket(c *cli.Context) (string, error) {
+106
View File
@@ -0,0 +1,106 @@
package config
import (
"errors"
"os"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/watcher"
"gopkg.in/yaml.v2"
)
// Notifier sends out config updates
type Notifier interface {
ConfigDidUpdate(Root)
}
// Manager is the base functions of the config manager
type Manager interface {
Start(Notifier) error
Shutdown()
}
// FileManager watches the yaml config for changes
// sends updates to the service to reconfigure to match the updated config
type FileManager struct {
watcher watcher.Notifier
notifier Notifier
configPath string
logger logger.Service
ReadConfig func(string) (Root, error)
}
// NewFileManager creates a config manager
func NewFileManager(watcher watcher.Notifier, configPath string, logger logger.Service) (*FileManager, error) {
m := &FileManager{
watcher: watcher,
configPath: configPath,
logger: logger,
ReadConfig: readConfigFromPath,
}
err := watcher.Add(configPath)
return m, err
}
// Start starts the runloop to watch for config changes
func (m *FileManager) Start(notifier Notifier) error {
m.notifier = notifier
// update the notifier with a fresh config on start
config, err := m.GetConfig()
if err != nil {
return err
}
notifier.ConfigDidUpdate(config)
m.watcher.Start(m)
return nil
}
// GetConfig reads the yaml file from the disk
func (m *FileManager) GetConfig() (Root, error) {
return m.ReadConfig(m.configPath)
}
// Shutdown stops the watcher
func (m *FileManager) Shutdown() {
m.watcher.Shutdown()
}
func readConfigFromPath(configPath string) (Root, error) {
if configPath == "" {
return Root{}, errors.New("unable to find config file")
}
file, err := os.Open(configPath)
if err != nil {
return Root{}, err
}
defer file.Close()
var config Root
if err := yaml.NewDecoder(file).Decode(&config); err != nil {
return Root{}, err
}
return config, nil
}
// File change notifications from the watcher
// WatcherItemDidChange triggers when the yaml config is updated
// sends the updated config to the service to reload its state
func (m *FileManager) WatcherItemDidChange(filepath string) {
config, err := m.GetConfig()
if err != nil {
m.logger.Errorf("Failed to read new config: %s", err)
return
}
m.logger.Info("Config file has been updated")
m.notifier.ConfigDidUpdate(config)
}
// WatcherDidError notifies of errors with the file watcher
func (m *FileManager) WatcherDidError(err error) {
m.logger.Errorf("Config watcher encountered an error: %s", err)
}
+90
View File
@@ -0,0 +1,90 @@
package config
import (
"os"
"testing"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/watcher"
"github.com/stretchr/testify/assert"
)
type mockNotifier struct {
configs []Root
}
func (n *mockNotifier) ConfigDidUpdate(c Root) {
n.configs = append(n.configs, c)
}
type mockFileWatcher struct {
path string
notifier watcher.Notification
ready chan struct{}
}
func (w *mockFileWatcher) Start(n watcher.Notification) {
w.notifier = n
w.ready <- struct{}{}
}
func (w *mockFileWatcher) Add(string) error {
return nil
}
func (w *mockFileWatcher) Shutdown() {
}
func (w *mockFileWatcher) TriggerChange() {
w.notifier.WatcherItemDidChange(w.path)
}
func TestConfigChanged(t *testing.T) {
filePath := "config.yaml"
f, err := os.Create(filePath)
assert.NoError(t, err)
defer func() {
f.Close()
os.Remove(filePath)
}()
c := &Root{
OrgKey: "abcd",
ConfigType: "mytype",
CheckinInterval: 1,
Forwarders: []Forwarder{
{
URL: "test.daltoniam.com",
Listener: "127.0.0.1:8080",
},
},
}
configRead := func(configPath string) (Root, error) {
return *c, nil
}
wait := make(chan struct{})
w := &mockFileWatcher{path: filePath, ready: wait}
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
service, err := NewFileManager(w, filePath, logger)
service.ReadConfig = configRead
assert.NoError(t, err)
n := &mockNotifier{}
go service.Start(n)
<-wait
c.Forwarders = append(c.Forwarders, Forwarder{URL: "add.daltoniam.com", Listener: "127.0.0.1:8081"})
w.TriggerChange()
service.Shutdown()
assert.Len(t, n.configs, 2, "did not get 2 config updates as expected")
assert.Len(t, n.configs[0].Forwarders, 1, "not the amount of forwarders expected")
assert.Len(t, n.configs[1].Forwarders, 2, "not the amount of forwarders expected")
assert.Equal(t, n.configs[0].Forwarders[0].Hash(), c.Forwarders[0].Hash(), "forwarder hashes don't match")
assert.Equal(t, n.configs[1].Forwarders[0].Hash(), c.Forwarders[0].Hash(), "forwarder hashes don't match")
assert.Equal(t, n.configs[1].Forwarders[1].Hash(), c.Forwarders[1].Hash(), "forwarder hashes don't match")
}
+104
View File
@@ -0,0 +1,104 @@
package config
import (
"crypto/md5"
"fmt"
"io"
"strings"
)
// Forwarder represents a client side listener to forward traffic to the edge
type Forwarder struct {
URL string `json:"url"`
Listener string `json:"listener"`
TokenClientID string `json:"service_token_id" yaml:"serviceTokenID"`
TokenSecret string `json:"secret_token_id" yaml:"serviceTokenSecret"`
Destination string `json:"destination"`
}
// Tunnel represents a tunnel that should be started
type Tunnel struct {
URL string `json:"url"`
Origin string `json:"origin"`
ProtocolType string `json:"type"`
}
// DNSResolver represents a client side DNS resolver
type DNSResolver struct {
Enabled bool `json:"enabled"`
Address string `json:"address,omitempty"`
Port uint16 `json:"port,omitempty"`
Upstreams []string `json:"upstreams,omitempty"`
Bootstraps []string `json:"bootstraps,omitempty"`
}
// Root is the base options to configure the service
type Root struct {
OrgKey string `json:"org_key" yaml:"orgKey"`
ConfigType string `json:"type"`
LogDirectory string `json:"log_directory" yaml:"logDirectory,omitempty"`
LogLevel string `json:"log_level" yaml:"logLevel"`
CheckinInterval int `json:"checkin_interval" yaml:"checkinInterval"`
Forwarders []Forwarder `json:"forwarders,omitempty"`
Tunnels []Tunnel `json:"tunnels,omitempty"`
Resolver DNSResolver `json:"resolver,omitempty"`
}
// Hash returns the computed values to see if the forwarder values change
func (f *Forwarder) Hash() string {
h := md5.New()
io.WriteString(h, f.URL)
io.WriteString(h, f.Listener)
io.WriteString(h, f.TokenClientID)
io.WriteString(h, f.TokenSecret)
io.WriteString(h, f.Destination)
return fmt.Sprintf("%x", h.Sum(nil))
}
// Hash returns the computed values to see if the forwarder values change
func (r *DNSResolver) Hash() string {
h := md5.New()
io.WriteString(h, r.Address)
io.WriteString(h, strings.Join(r.Bootstraps, ","))
io.WriteString(h, strings.Join(r.Upstreams, ","))
io.WriteString(h, fmt.Sprintf("%d", r.Port))
io.WriteString(h, fmt.Sprintf("%v", r.Enabled))
return fmt.Sprintf("%x", h.Sum(nil))
}
// EnabledOrDefault returns the enabled property
func (r *DNSResolver) EnabledOrDefault() bool {
return r.Enabled
}
// AddressOrDefault returns the address or returns the default if empty
func (r *DNSResolver) AddressOrDefault() string {
if r.Address != "" {
return r.Address
}
return "localhost"
}
// PortOrDefault return the port or returns the default if 0
func (r *DNSResolver) PortOrDefault() uint16 {
if r.Port > 0 {
return r.Port
}
return 53
}
// UpstreamsOrDefault returns the upstreams or returns the default if empty
func (r *DNSResolver) UpstreamsOrDefault() []string {
if len(r.Upstreams) > 0 {
return r.Upstreams
}
return []string{"https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"}
}
// BootstrapsOrDefault returns the bootstraps or returns the default if empty
func (r *DNSResolver) BootstrapsOrDefault() []string {
if len(r.Bootstraps) > 0 {
return r.Bootstraps
}
return []string{"https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"}
}
+45 -29
View File
@@ -8,6 +8,8 @@ import (
"path/filepath"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
cli "gopkg.in/urfave/cli.v2"
)
@@ -73,7 +75,7 @@ ExecStart=/bin/bash -c '{{ .Path }} update; code=$?; if [ $code -eq 64 ]; then s
Description=Update Argo Tunnel
[Timer]
OnUnitActiveSec=1d
OnCalendar=daily
[Install]
WantedBy=timers.target
@@ -118,10 +120,6 @@ case "$1" in
echo "Starting $name"
$cmd >> "$stdout_log" 2>> "$stderr_log" &
echo $! > "$pid_file"
if ! is_running; then
echo "Unable to start, see $stdout_log and $stderr_log"
exit 1
fi
fi
;;
stop)
@@ -182,24 +180,37 @@ func isSystemd() bool {
return false
}
func copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile string) error {
func copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile string, logger logger.Service) error {
if err := ensureConfigDirExists(serviceConfigDir); err != nil {
return err
}
srcCredentialPath := filepath.Join(userConfigDir, userCredentialFile)
destCredentialPath := filepath.Join(serviceConfigDir, serviceCredentialFile)
if err := copyCredential(srcCredentialPath, destCredentialPath); err != nil {
return err
if srcCredentialPath != destCredentialPath {
if err := copyCredential(srcCredentialPath, destCredentialPath); err != nil {
return err
}
}
srcConfigPath := filepath.Join(userConfigDir, userConfigFile)
destConfigPath := filepath.Join(serviceConfigDir, serviceConfigFile)
if err := copyConfig(srcConfigPath, destConfigPath); err != nil {
return err
if srcConfigPath != destConfigPath {
if err := copyConfig(srcConfigPath, destConfigPath); err != nil {
return err
}
logger.Infof("Copied %s to %s", srcConfigPath, destConfigPath)
}
return nil
}
func installLinuxService(c *cli.Context) error {
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
etPath, err := os.Executable()
if err != nil {
return fmt.Errorf("error determining executable path: %v", err)
@@ -209,8 +220,8 @@ func installLinuxService(c *cli.Context) error {
userConfigDir := filepath.Dir(c.String("config"))
userConfigFile := filepath.Base(c.String("config"))
userCredentialFile := config.DefaultCredentialFile
if err = copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile); err != nil {
logger.WithError(err).Infof("Failed to copy user configuration. Before running the service, ensure that %s contains two files, %s and %s",
if err = copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile, logger); err != nil {
logger.Errorf("Failed to copy user configuration: %s. Before running the service, ensure that %s contains two files, %s and %s", err,
serviceConfigDir, serviceCredentialFile, serviceConfigFile)
return err
}
@@ -218,41 +229,41 @@ func installLinuxService(c *cli.Context) error {
switch {
case isSystemd():
logger.Infof("Using Systemd")
return installSystemd(&templateArgs)
return installSystemd(&templateArgs, logger)
default:
logger.Infof("Using Sysv")
return installSysv(&templateArgs)
return installSysv(&templateArgs, logger)
}
}
func installSystemd(templateArgs *ServiceTemplateArgs) error {
func installSystemd(templateArgs *ServiceTemplateArgs, logger logger.Service) error {
for _, serviceTemplate := range systemdTemplates {
err := serviceTemplate.Generate(templateArgs)
if err != nil {
logger.WithError(err).Infof("error generating service template")
logger.Errorf("error generating service template: %s", err)
return err
}
}
if err := runCommand("systemctl", "enable", "cloudflared.service"); err != nil {
logger.WithError(err).Infof("systemctl enable cloudflared.service error")
logger.Errorf("systemctl enable cloudflared.service error: %s", err)
return err
}
if err := runCommand("systemctl", "start", "cloudflared-update.timer"); err != nil {
logger.WithError(err).Infof("systemctl start cloudflared-update.timer error")
logger.Errorf("systemctl start cloudflared-update.timer error: %s", err)
return err
}
logger.Infof("systemctl daemon-reload")
return runCommand("systemctl", "daemon-reload")
}
func installSysv(templateArgs *ServiceTemplateArgs) error {
func installSysv(templateArgs *ServiceTemplateArgs, logger logger.Service) error {
confPath, err := sysvTemplate.ResolvePath()
if err != nil {
logger.WithError(err).Infof("error resolving system path")
logger.Errorf("error resolving system path: %s", err)
return err
}
if err := sysvTemplate.Generate(templateArgs); err != nil {
logger.WithError(err).Infof("error generating system template")
logger.Errorf("error generating system template: %s", err)
return err
}
for _, i := range [...]string{"2", "3", "4", "5"} {
@@ -269,28 +280,33 @@ func installSysv(templateArgs *ServiceTemplateArgs) error {
}
func uninstallLinuxService(c *cli.Context) error {
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
switch {
case isSystemd():
logger.Infof("Using Systemd")
return uninstallSystemd()
return uninstallSystemd(logger)
default:
logger.Infof("Using Sysv")
return uninstallSysv()
return uninstallSysv(logger)
}
}
func uninstallSystemd() error {
func uninstallSystemd(logger logger.Service) error {
if err := runCommand("systemctl", "disable", "cloudflared.service"); err != nil {
logger.WithError(err).Infof("systemctl disable cloudflared.service error")
logger.Errorf("systemctl disable cloudflared.service error: %s", err)
return err
}
if err := runCommand("systemctl", "stop", "cloudflared-update.timer"); err != nil {
logger.WithError(err).Infof("systemctl stop cloudflared-update.timer error")
logger.Errorf("systemctl stop cloudflared-update.timer error: %s", err)
return err
}
for _, serviceTemplate := range systemdTemplates {
if err := serviceTemplate.Remove(); err != nil {
logger.WithError(err).Infof("error removing service template")
logger.Errorf("error removing service template: %s", err)
return err
}
}
@@ -298,9 +314,9 @@ func uninstallSystemd() error {
return nil
}
func uninstallSysv() error {
func uninstallSysv(logger logger.Service) error {
if err := sysvTemplate.Remove(); err != nil {
logger.WithError(err).Infof("error removing service template")
logger.Errorf("error removing service template: %s", err)
return err
}
for _, i := range [...]string{"2", "3", "4", "5"} {
+21 -7
View File
@@ -8,6 +8,7 @@ import (
"gopkg.in/urfave/cli.v2"
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
)
@@ -60,7 +61,7 @@ func newLaunchdTemplate(installPath, stdoutPath, stderrPath string) *ServiceTemp
<false/>
</dict>
<key>ThrottleInterval</key>
<integer>20</integer>
<integer>5</integer>
</dict>
</plist>`, launchdIdentifier, stdoutPath, stderrPath),
}
@@ -105,6 +106,11 @@ func stderrPath() (string, error) {
}
func installLaunchd(c *cli.Context) error {
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
if isRootUser() {
logger.Infof("Installing Argo Tunnel client as a system launch daemon. " +
"Argo Tunnel client will run at boot")
@@ -116,35 +122,38 @@ func installLaunchd(c *cli.Context) error {
}
etPath, err := os.Executable()
if err != nil {
logger.WithError(err).Errorf("Error determining executable path")
logger.Errorf("Error determining executable path: %s", err)
return fmt.Errorf("Error determining executable path: %v", err)
}
installPath, err := installPath()
if err != nil {
logger.Errorf("Error determining install path: %s", err)
return errors.Wrap(err, "Error determining install path")
}
stdoutPath, err := stdoutPath()
if err != nil {
logger.Errorf("error determining stdout path: %s", err)
return errors.Wrap(err, "error determining stdout path")
}
stderrPath, err := stderrPath()
if err != nil {
logger.Errorf("error determining stderr path: %s", err)
return errors.Wrap(err, "error determining stderr path")
}
launchdTemplate := newLaunchdTemplate(installPath, stdoutPath, stderrPath)
if err != nil {
logger.WithError(err).Errorf("error creating launchd template")
logger.Errorf("error creating launchd template: %s", err)
return errors.Wrap(err, "error creating launchd template")
}
templateArgs := ServiceTemplateArgs{Path: etPath}
err = launchdTemplate.Generate(&templateArgs)
if err != nil {
logger.WithError(err).Errorf("error generating launchd template")
logger.Errorf("error generating launchd template: %s", err)
return err
}
plistPath, err := launchdTemplate.ResolvePath()
if err != nil {
logger.WithError(err).Infof("error resolving launchd template path")
logger.Errorf("error resolving launchd template path: %s", err)
return err
}
@@ -153,6 +162,11 @@ func installLaunchd(c *cli.Context) error {
}
func uninstallLaunchd(c *cli.Context) error {
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
if isRootUser() {
logger.Infof("Uninstalling Argo Tunnel as a system launch daemon")
} else {
@@ -176,12 +190,12 @@ func uninstallLaunchd(c *cli.Context) error {
}
plistPath, err := launchdTemplate.ResolvePath()
if err != nil {
logger.WithError(err).Infof("error resolving launchd template path")
logger.Errorf("error resolving launchd template path: %s", err)
return err
}
err = runCommand("launchctl", "unload", plistPath)
if err != nil {
logger.WithError(err).Infof("error unloading")
logger.Errorf("error unloading: %s", err)
return err
}
+64 -15
View File
@@ -6,28 +6,29 @@ import (
"time"
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
"github.com/cloudflare/cloudflared/log"
log "github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/metrics"
"github.com/cloudflare/cloudflared/overwatch"
"github.com/cloudflare/cloudflared/watcher"
"github.com/getsentry/raven-go"
"github.com/mitchellh/go-homedir"
"gopkg.in/urfave/cli.v2"
raven "github.com/getsentry/raven-go"
homedir "github.com/mitchellh/go-homedir"
cli "gopkg.in/urfave/cli.v2"
"github.com/pkg/errors"
)
const (
developerPortal = "https://developers.cloudflare.com/argo-tunnel"
licenseUrl = developerPortal + "/license/"
versionText = "Print the version"
versionText = "Print the version"
)
var (
Version = "DEV"
BuildTime = "unknown"
logger = log.CreateLogger()
// Mostly network errors that we don't want reported back to Sentry, this is done by substring match.
ignoredErrors = []string{
"connection reset by peer",
@@ -61,11 +62,16 @@ func main() {
app := &cli.App{}
app.Name = "cloudflared"
app.Usage = "Cloudflare's command-line tool and agent"
app.ArgsUsage = "origin-url"
app.Copyright = fmt.Sprintf(`(c) %d Cloudflare Inc.
Use is subject to the license agreement at %s`, time.Now().Year(), licenseUrl)
app.UsageText = "cloudflared [global options] [command] [command options]"
app.Copyright = fmt.Sprintf(
`(c) %d Cloudflare Inc.
Your installation of cloudflared software constitutes a symbol of your signature indicating that you accept
the terms of the Cloudflare License (https://developers.cloudflare.com/argo-tunnel/license/),
Terms (https://www.cloudflare.com/terms/) and Privacy Policy (https://www.cloudflare.com/privacypolicy/).`,
time.Now().Year(),
)
app.Version = fmt.Sprintf("%s (built %s)", Version, BuildTime)
app.Description = `cloudflared connects your machine or user identity to Cloudflare's global network.
app.Description = `cloudflared connects your machine or user identity to Cloudflare's global network.
You can use it to authenticate a session to reach an API behind Access, route web traffic to this machine,
and configure access control.`
app.Flags = flags()
@@ -118,16 +124,20 @@ func isEmptyInvocation(c *cli.Context) bool {
func action(version string, shutdownC, graceShutdownC chan struct{}) cli.ActionFunc {
return func(c *cli.Context) (err error) {
if isEmptyInvocation(c) {
cli.ShowAppHelpAndExit(c, 1)
return handleServiceMode(shutdownC)
}
tags := make(map[string]string)
tags["hostname"] = c.String("hostname")
raven.SetTagsContext(tags)
raven.CapturePanic(func() { err = tunnel.StartServer(c, version, shutdownC, graceShutdownC) }, nil)
exitCode := 0
if err != nil {
handleError(err)
exitCode = 1
}
return err
// we already handle error printing, so we pass an empty string so we
// don't have to print again.
return cli.Exit("", exitCode)
}
}
@@ -138,7 +148,6 @@ func userHomeDir() (string, error) {
// use with sudo.
homeDir, err := homedir.Dir()
if err != nil {
logger.WithError(err).Error("Cannot determine home directory for the user")
return "", errors.Wrap(err, "Cannot determine home directory for the user")
}
return homeDir, nil
@@ -154,3 +163,43 @@ func handleError(err error) {
}
raven.CaptureError(err, nil)
}
// cloudflared was started without any flags
func handleServiceMode(shutdownC chan struct{}) error {
defer log.SharedWriteManager.Shutdown()
logDirectory, logLevel := config.FindLogSettings()
logger, err := log.New(log.DefaultFile(logDirectory), log.LogLevelString(logLevel))
if err != nil {
return cliutil.PrintLoggerSetupError("error setting up logger", err)
}
logger.Infof("logging to directory: %s", logDirectory)
// start the main run loop that reads from the config file
f, err := watcher.NewFile()
if err != nil {
logger.Errorf("Cannot load config file: %s", err)
return err
}
configPath := config.FindDefaultConfigPath()
configManager, err := config.NewFileManager(f, configPath, logger)
if err != nil {
logger.Errorf("Cannot setup config file for monitoring: %s", err)
return err
}
serviceCallback := func(t string, name string, err error) {
if err != nil {
logger.Errorf("%s service: %s encountered an error: %s", t, name, err)
}
}
serviceManager := overwatch.NewAppManager(serviceCallback)
appService := NewAppService(configManager, serviceManager, shutdownC, logger)
if err := appService.Run(); err != nil {
logger.Errorf("Failed to start app service: %s", err)
return err
}
return nil
}
+30
View File
@@ -0,0 +1,30 @@
package path
import (
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/mitchellh/go-homedir"
)
// GenerateFilePathFromURL will return a filepath for given access application url
func GenerateFilePathFromURL(url *url.URL, suffix string) (string, error) {
configPath, err := homedir.Expand(config.DefaultConfigDirs[0])
if err != nil {
return "", err
}
ok, err := config.FileExists(configPath)
if !ok && err == nil {
// create config directory if doesn't already exist
err = os.Mkdir(configPath, 0700)
}
if err != nil {
return "", err
}
name := strings.Replace(fmt.Sprintf("%s%s-%s", url.Hostname(), url.EscapedPath(), suffix), "/", "-", -1)
return filepath.Join(configPath, name), nil
}
+3 -10
View File
@@ -73,21 +73,16 @@ func runCommand(command string, args ...string) error {
cmd := exec.Command(command, args...)
stderr, err := cmd.StderrPipe()
if err != nil {
logger.WithError(err).Infof("error getting stderr pipe")
return fmt.Errorf("error getting stderr pipe: %v", err)
}
err = cmd.Start()
if err != nil {
logger.WithError(err).Infof("error starting %s", command)
return fmt.Errorf("error starting %s: %v", command, err)
}
commandErr, _ := ioutil.ReadAll(stderr)
if len(commandErr) > 0 {
logger.Errorf("%s: %s", command, commandErr)
}
ioutil.ReadAll(stderr)
err = cmd.Wait()
if err != nil {
logger.WithError(err).Infof("%s returned error", command)
return fmt.Errorf("%s returned with error: %v", command, err)
}
return nil
@@ -148,8 +143,7 @@ func copyConfig(srcConfigPath, destConfigPath string) error {
// Copy or create config
destFile, exists, err := openFile(destConfigPath, true)
if err != nil {
logger.WithError(err).Infof("cannot open %s", destConfigPath)
return err
return fmt.Errorf("cannot open %s with error: %s", destConfigPath, err)
} else if exists {
// config already exists, do nothing
return nil
@@ -173,7 +167,6 @@ func copyConfig(srcConfigPath, destConfigPath string) error {
if err != nil {
return fmt.Errorf("unable to copy %s to %s: %v", srcConfigPath, destConfigPath, err)
}
logger.Infof("Copied %s to %s", srcConfigPath, destConfigPath)
}
return nil
@@ -0,0 +1,11 @@
//+build darwin
package shell
import (
"os/exec"
)
func getBrowserCmd(url string) *exec.Cmd {
return exec.Command("open", url)
}
@@ -0,0 +1,11 @@
//+build !windows,!darwin,!linux,!netbsd,!freebsd,!openbsd
package shell
import (
"os/exec"
)
func getBrowserCmd(url string) *exec.Cmd {
return nil
}
@@ -0,0 +1,11 @@
//+build linux freebsd openbsd netbsd
package shell
import (
"os/exec"
)
func getBrowserCmd(url string) *exec.Cmd {
return exec.Command("xdg-open", url)
}
@@ -0,0 +1,18 @@
//+build windows
package shell
import (
"fmt"
"os/exec"
"syscall"
)
func getBrowserCmd(url string) *exec.Cmd {
cmd := exec.Command("cmd")
// CmdLine is only defined when compiling for windows.
// Empty string is the cmd proc "Title". Needs to be included because the start command will interpret the first
// quoted string as that field and we want to quote the URL.
cmd.SysProcAttr = &syscall.SysProcAttr{CmdLine: fmt.Sprintf(`/c start "" "%s"`, url)}
return cmd
}
+1 -15
View File
@@ -4,25 +4,11 @@ import (
"io"
"os"
"os/exec"
"runtime"
)
// OpenBrowser opens the specified URL in the default browser of the user
func OpenBrowser(url string) error {
var cmd string
var args []string
switch runtime.GOOS {
case "windows":
cmd = "cmd"
args = []string{"/c", "start"}
case "darwin":
cmd = "open"
default: // "linux", "freebsd", "openbsd", "netbsd"
cmd = "xdg-open"
}
args = append(args, url)
return exec.Command(cmd, args...).Start()
return getBrowserCmd(url).Start()
}
// Run will kick off a shell task and pipe the results to the respective std pipes
+160 -31
View File
@@ -1,40 +1,172 @@
package token
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strings"
"os/signal"
"syscall"
"time"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/cmd/cloudflared/path"
"github.com/cloudflare/cloudflared/cmd/cloudflared/transfer"
"github.com/cloudflare/cloudflared/log"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/origin"
"github.com/coreos/go-oidc/jose"
"github.com/coreos/go-oidc/oidc"
homedir "github.com/mitchellh/go-homedir"
)
var logger = log.CreateLogger()
const (
keyName = "token"
)
type lock struct {
lockFilePath string
backoff *origin.BackoffHandler
sigHandler *signalHandler
}
type signalHandler struct {
sigChannel chan os.Signal
signals []os.Signal
}
type jwtPayload struct {
Aud []string `json:"aud"`
Email string `json:"email"`
Exp int `json:"exp"`
Iat int `json:"iat"`
Nbf int `json:"nbf"`
Iss string `json:"iss"`
Type string `json:"type"`
Subt string `json:"sub"`
}
func (p jwtPayload) isExpired() bool {
return int(time.Now().Unix()) > p.Exp
}
func (s *signalHandler) register(handler func()) {
s.sigChannel = make(chan os.Signal, 1)
signal.Notify(s.sigChannel, s.signals...)
go func(s *signalHandler) {
for range s.sigChannel {
handler()
}
}(s)
}
func (s *signalHandler) deregister() {
signal.Stop(s.sigChannel)
close(s.sigChannel)
}
func errDeleteTokenFailed(lockFilePath string) error {
return fmt.Errorf("failed to acquire a new Access token. Please try to delete %s", lockFilePath)
}
// newLock will get a new file lock
func newLock(path string) *lock {
lockPath := path + ".lock"
return &lock{
lockFilePath: lockPath,
backoff: &origin.BackoffHandler{MaxRetries: 7},
sigHandler: &signalHandler{
signals: []os.Signal{syscall.SIGINT, syscall.SIGTERM},
},
}
}
func (l *lock) Acquire() error {
// Intercept SIGINT and SIGTERM to release lock before exiting
l.sigHandler.register(func() {
l.deleteLockFile()
os.Exit(0)
})
// Check for a path.lock file
// if the lock file exists; start polling
// if not, create the lock file and go through the normal flow.
// See AUTH-1736 for the reason why we do all this
for isTokenLocked(l.lockFilePath) {
if l.backoff.Backoff(context.Background()) {
continue
}
if err := l.deleteLockFile(); err != nil {
return err
}
}
// Create a lock file so other processes won't also try to get the token at
// the same time
if err := ioutil.WriteFile(l.lockFilePath, []byte{}, 0600); err != nil {
return err
}
return nil
}
func (l *lock) deleteLockFile() error {
if err := os.Remove(l.lockFilePath); err != nil && !os.IsNotExist(err) {
return errDeleteTokenFailed(l.lockFilePath)
}
return nil
}
func (l *lock) Release() error {
defer l.sigHandler.deregister()
return l.deleteLockFile()
}
// isTokenLocked checks to see if there is another process attempting to get the token already
func isTokenLocked(lockFilePath string) bool {
exists, err := config.FileExists(lockFilePath)
return exists && err == nil
}
// FetchTokenWithRedirect will either load a stored token or generate a new one
// it appends a redirect URL to the access cli request if opening the browser
func FetchTokenWithRedirect(appURL *url.URL, logger logger.Service) (string, error) {
return getToken(appURL, true, logger)
}
// FetchToken will either load a stored token or generate a new one
func FetchToken(appURL *url.URL) (string, error) {
// it doesn't append a redirect URL to the access cli request if opening the browser
func FetchToken(appURL *url.URL, logger logger.Service) (string, error) {
return getToken(appURL, false, logger)
}
// getToken will either load a stored token or generate a new one
func getToken(appURL *url.URL, shouldRedirect bool, logger logger.Service) (string, error) {
if token, err := GetTokenIfExists(appURL); token != "" && err == nil {
return token, nil
}
path, err := generateFilePathForTokenURL(appURL)
path, err := path.GenerateFilePathFromURL(appURL, keyName)
if err != nil {
return "", err
}
fileLock := newLock(path)
err = fileLock.Acquire()
if err != nil {
return "", err
}
defer fileLock.Release()
// check to see if another process has gotten a token while we waited for the lock
if token, err := GetTokenIfExists(appURL); token != "" && err == nil {
return token, nil
}
// this weird parameter is the resource name (token) and the key/value
// we want to send to the transfer service. the key is token and the value
// is blank (basically just the id generated in the transfer service)
const resourceName, key, value = "token", "token", ""
token, err := transfer.Run(appURL, resourceName, key, value, path, true)
token, err := transfer.Run(appURL, keyName, keyName, "", path, true, shouldRedirect, logger)
if err != nil {
return "", err
}
@@ -42,9 +174,9 @@ func FetchToken(appURL *url.URL) (string, error) {
return string(token), nil
}
// GetTokenIfExists will return the token from local storage if it exists
// GetTokenIfExists will return the token from local storage if it exists and not expired
func GetTokenIfExists(url *url.URL) (string, error) {
path, err := generateFilePathForTokenURL(url)
path, err := path.GenerateFilePathFromURL(url, keyName)
if err != nil {
return "", err
}
@@ -57,32 +189,29 @@ func GetTokenIfExists(url *url.URL) (string, error) {
return "", err
}
claims, err := token.Claims()
var payload jwtPayload
err = json.Unmarshal(token.Payload, &payload)
if err != nil {
return "", err
}
ident, err := oidc.IdentityFromClaims(claims)
// AUTH-1404, reauth if the token is about to expire within 15 minutes
if err == nil && ident.ExpiresAt.After(time.Now().Add(time.Minute*15)) {
return token.Encode(), nil
if payload.isExpired() {
err := os.Remove(path)
return "", err
}
return "", err
return token.Encode(), nil
}
// generateFilePathForTokenURL will return a filepath for given access application url
func generateFilePathForTokenURL(url *url.URL) (string, error) {
configPath, err := homedir.Expand(config.DefaultConfigDirs[0])
// RemoveTokenIfExists removes the a token from local storage if it exists
func RemoveTokenIfExists(url *url.URL) error {
path, err := path.GenerateFilePathFromURL(url, keyName)
if err != nil {
return "", err
return err
}
ok, err := config.FileExists(configPath)
if !ok && err == nil {
// create config directory if doesn't already exist
err = os.Mkdir(configPath, 0700)
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
return err
}
if err != nil {
return "", err
}
name := strings.Replace(fmt.Sprintf("%s%s-token", url.Hostname(), url.EscapedPath()), "/", "-", -1)
return filepath.Join(configPath, name), nil
return nil
}
+52
View File
@@ -0,0 +1,52 @@
package token
import (
"os"
"syscall"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSignalHandler(t *testing.T) {
sigHandler := signalHandler{signals: []os.Signal{syscall.SIGUSR1}}
handlerRan := false
done := make(chan struct{})
timer := time.NewTimer(time.Second)
sigHandler.register(func(){
handlerRan = true
done <- struct{}{}
})
p, err := os.FindProcess(os.Getpid())
require.Nil(t, err)
p.Signal(syscall.SIGUSR1)
// Blocks for up to one second to make sure the handler callback runs before the assert.
select {
case <- done:
assert.True(t, handlerRan)
case <- timer.C:
t.Fail()
}
sigHandler.deregister()
}
func TestSignalHandlerClose(t *testing.T) {
sigHandler := signalHandler{signals: []os.Signal{syscall.SIGUSR1}}
done := make(chan struct{})
timer := time.NewTimer(time.Second)
sigHandler.register(func(){done <- struct{}{}})
sigHandler.deregister()
p, err := os.FindProcess(os.Getpid())
require.Nil(t, err)
p.Signal(syscall.SIGUSR1)
select {
case <- done:
t.Fail()
case <- timer.C:
}
}
+16 -16
View File
@@ -14,28 +14,26 @@ import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/encrypter"
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
"github.com/cloudflare/cloudflared/log"
"github.com/cloudflare/cloudflared/logger"
)
const (
baseStoreURL = "https://login.cloudflarewarp.com/"
baseStoreURL = "https://login.argotunnel.com/"
clientTimeout = time.Second * 60
)
var logger = log.CreateLogger()
// Run does the transfer "dance" with the end result downloading the supported resource.
// The expanded description is run is encapsulation of shared business logic needed
// to request a resource (token/cert/etc) from the transfer service (loginhelper).
// The "dance" we refer to is building a HTTP request, opening that in a browser waiting for
// the user to complete an action, while it long polls in the background waiting for an
// action to be completed to download the resource.
func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncrypt bool) ([]byte, error) {
func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncrypt bool, shouldRedirect bool, logger logger.Service) ([]byte, error) {
encrypterClient, err := encrypter.New("cloudflared_priv.pem", "cloudflared_pub.pem")
if err != nil {
return nil, err
}
requestURL, err := buildRequestURL(transferURL, key, value+encrypterClient.PublicKey(), shouldEncrypt)
requestURL, err := buildRequestURL(transferURL, key, value+encrypterClient.PublicKey(), shouldEncrypt, shouldRedirect)
if err != nil {
return nil, err
}
@@ -45,13 +43,13 @@ func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncr
if err != nil {
fmt.Fprintf(os.Stderr, "Please open the following URL and log in with your Cloudflare account:\n\n%s\n\nLeave cloudflared running to download the %s automatically.\n", requestURL, resourceName)
} else {
fmt.Fprintf(os.Stderr, "A browser window should have opened at the following URL:\n\n%s\n\nIf the browser failed to open, open it yourself and visit the URL above.\n", requestURL)
fmt.Fprintf(os.Stderr, "A browser window should have opened at the following URL:\n\n%s\n\nIf the browser failed to open, please visit the URL above directly in your browser.\n", requestURL)
}
var resourceData []byte
if shouldEncrypt {
buf, key, err := transferRequest(baseStoreURL + "transfer/" + encrypterClient.PublicKey())
buf, key, err := transferRequest(baseStoreURL+"transfer/"+encrypterClient.PublicKey(), logger)
if err != nil {
return nil, err
}
@@ -67,7 +65,7 @@ func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncr
resourceData = decrypted
} else {
buf, _, err := transferRequest(baseStoreURL + encrypterClient.PublicKey())
buf, _, err := transferRequest(baseStoreURL+encrypterClient.PublicKey(), logger)
if err != nil {
return nil, err
}
@@ -84,7 +82,7 @@ func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncr
// BuildRequestURL creates a request suitable for a resource transfer.
// it will return a constructed url based off the base url and query key/value provided.
// cli will build a url for cli transfer request.
func buildRequestURL(baseURL *url.URL, key, value string, cli bool) (string, error) {
func buildRequestURL(baseURL *url.URL, key, value string, cli, shouldRedirect bool) (string, error) {
q := baseURL.Query()
q.Set(key, value)
baseURL.RawQuery = q.Encode()
@@ -92,24 +90,26 @@ func buildRequestURL(baseURL *url.URL, key, value string, cli bool) (string, err
return baseURL.String(), nil
}
q.Set("redirect_url", baseURL.String()) // we add the token as a query param on both the redirect_url
baseURL.RawQuery = q.Encode() // and this actual baseURL.
if shouldRedirect {
q.Set("redirect_url", baseURL.String()) // we add the token as a query param on both the redirect_url and the main url
}
baseURL.RawQuery = q.Encode() // and this actual baseURL.
baseURL.Path = "cdn-cgi/access/cli"
return baseURL.String(), nil
}
// transferRequest downloads the requested resource from the request URL
func transferRequest(requestURL string) ([]byte, string, error) {
func transferRequest(requestURL string, logger logger.Service) ([]byte, string, error) {
client := &http.Client{Timeout: clientTimeout}
const pollAttempts = 10
// we do "long polling" on the endpoint to get the resource.
for i := 0; i < pollAttempts; i++ {
buf, key, err := poll(client, requestURL)
buf, key, err := poll(client, requestURL, logger)
if err != nil {
return nil, "", err
} else if len(buf) > 0 {
if err := putSuccess(client, requestURL); err != nil {
logger.WithError(err).Error("Failed to update resource success")
logger.Errorf("Failed to update resource success: %s", err)
}
return buf, key, nil
}
@@ -118,7 +118,7 @@ func transferRequest(requestURL string) ([]byte, string, error) {
}
// poll the endpoint for the request resource, waiting for the user interaction
func poll(client *http.Client, requestURL string) ([]byte, string, error) {
func poll(client *http.Client, requestURL string, logger logger.Service) ([]byte, string, error) {
resp, err := client.Get(requestURL)
if err != nil {
return nil, "", err
+524 -116
View File
@@ -1,36 +1,100 @@
package tunnel
import (
"bufio"
"context"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"reflect"
"runtime"
"runtime/trace"
"strings"
"sync"
"syscall"
"time"
"github.com/getsentry/raven-go"
"golang.org/x/crypto/ssh/terminal"
"github.com/cloudflare/cloudflared/awsuploader"
"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/updater"
"github.com/cloudflare/cloudflared/cmd/sqlgateway"
"github.com/cloudflare/cloudflared/dbconnect"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/hello"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/metrics"
"github.com/cloudflare/cloudflared/origin"
"github.com/cloudflare/cloudflared/signal"
"github.com/cloudflare/cloudflared/socks"
"github.com/cloudflare/cloudflared/sshlog"
"github.com/cloudflare/cloudflared/sshserver"
"github.com/cloudflare/cloudflared/tlsconfig"
"github.com/cloudflare/cloudflared/tunneldns"
"github.com/cloudflare/cloudflared/websocket"
"github.com/coreos/go-systemd/daemon"
"github.com/facebookgo/grace/gracenet"
"github.com/getsentry/raven-go"
"github.com/gliderlabs/ssh"
"github.com/google/uuid"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"gopkg.in/urfave/cli.v2"
"gopkg.in/urfave/cli.v2/altsrc"
)
const sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b:3e8827f6f9f740738eb11138f7bebb68@sentry.io/189878"
const (
sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b:3e8827f6f9f740738eb11138f7bebb68@sentry.io/189878"
sshLogFileDirectory = "/usr/local/var/log/cloudflared/"
// sshPortFlag is the port on localhost the cloudflared ssh server will run on
sshPortFlag = "local-ssh-port"
// sshIdleTimeoutFlag defines the duration a SSH session can remain idle before being closed
sshIdleTimeoutFlag = "ssh-idle-timeout"
// sshMaxTimeoutFlag defines the max duration a SSH session can remain open for
sshMaxTimeoutFlag = "ssh-max-timeout"
// bucketNameFlag is the bucket name to use for the SSH log uploader
bucketNameFlag = "bucket-name"
// regionNameFlag is the AWS region name to use for the SSH log uploader
regionNameFlag = "region-name"
// secretIDFlag is the Secret id of SSH log uploader
secretIDFlag = "secret-id"
// accessKeyIDFlag is the Access key id of SSH log uploader
accessKeyIDFlag = "access-key-id"
// sessionTokenIDFlag is the Session token of SSH log uploader
sessionTokenIDFlag = "session-token"
// s3URLFlag is the S3 URL of SSH log uploader (e.g. don't use AWS s3 and use google storage bucket instead)
s3URLFlag = "s3-url-host"
// hostKeyPath is the path of the dir to save SSH host keys too
hostKeyPath = "host-key-path"
//sshServerFlag enables cloudflared ssh proxy server
sshServerFlag = "ssh-server"
// socks5Flag is to enable the socks server to deframe
socks5Flag = "socks5"
// bastionFlag is to enable bastion, or jump host, operation
bastionFlag = "bastion"
logDirectoryFlag = "log-directory"
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."
)
var (
shutdownC chan struct{}
@@ -46,7 +110,7 @@ func Commands() []*cli.Command {
cmds := []*cli.Command{
{
Name: "login",
Action: login,
Action: cliutil.ErrorHandler(login),
Usage: "Generate a configuration file with your login details",
ArgsUsage: " ",
Flags: []cli.Flag{
@@ -59,7 +123,7 @@ func Commands() []*cli.Command {
},
{
Name: "proxy-dns",
Action: tunneldns.Run,
Action: cliutil.ErrorHandler(tunneldns.Run),
Usage: "Run a DNS over HTTPS proxy server.",
Flags: []cli.Flag{
&cli.StringFlag{
@@ -86,47 +150,17 @@ func Commands() []*cli.Command {
Value: cli.NewStringSlice("https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"),
EnvVars: []string{"TUNNEL_DNS_UPSTREAM"},
},
&cli.StringSliceFlag{
Name: "bootstrap",
Usage: "bootstrap endpoint URL, you can specify multiple endpoints for redundancy.",
Value: cli.NewStringSlice("https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"),
EnvVars: []string{"TUNNEL_DNS_BOOTSTRAP"},
},
},
ArgsUsage: " ", // can't be the empty string or we get the default output
Hidden: false,
},
{
Name: "db",
Action: func(c *cli.Context) error {
tags := make(map[string]string)
tags["hostname"] = c.String("hostname")
raven.SetTagsContext(tags)
fmt.Printf("\nSQL Database Password: ")
pass, err := terminal.ReadPassword(int(syscall.Stdin))
if err != nil {
logger.Error(err)
}
go sqlgateway.StartProxy(c, logger, string(pass))
raven.CapturePanic(func() { err = tunnel(c) }, nil)
if err != nil {
raven.CaptureError(err, nil)
}
return err
},
Before: Before,
Usage: "SQL Gateway is an SQL over HTTP reverse proxy",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "db",
Value: true,
Usage: "Enable the SQL Gateway Proxy",
},
&cli.StringFlag{
Name: "address",
Value: "",
Usage: "Database connection string: db://user:pass",
},
},
Hidden: true,
},
dbConnectCmd(),
}
var subcommands []*cli.Command
@@ -135,14 +169,17 @@ func Commands() []*cli.Command {
c.Hidden = false
subcommands = append(subcommands, &c)
}
subcommands = append(subcommands, buildCreateCommand())
subcommands = append(subcommands, buildListCommand())
subcommands = append(subcommands, buildDeleteCommand())
cmds = append(cmds, &cli.Command{
Name: "tunnel",
Action: tunnel,
Action: cliutil.ErrorHandler(tunnel),
Before: Before,
Category: "Tunnel",
Usage: "Make a locally-running web service accessible over the internet using Argo Tunnel.",
ArgsUsage: "[origin-url]",
ArgsUsage: " ",
Description: `Argo Tunnel asks you to specify a hostname on a Cloudflare-powered
domain you control and a local address. Traffic from that hostname is routed
(optionally via a Cloudflare Load Balancer) to this machine and appears on the
@@ -176,7 +213,36 @@ func Init(v string, s, g chan struct{}) {
version, shutdownC, graceShutdownC = v, s, g
}
func createLogger(c *cli.Context, isTransport bool) (logger.Service, error) {
loggerOpts := []logger.Option{}
logPath := c.String("logfile")
if logPath != "" {
logPath = c.String(logDirectoryFlag)
}
if logPath != "" {
loggerOpts = append(loggerOpts, logger.DefaultFile(logPath))
}
logLevel := c.String("loglevel")
if isTransport {
logLevel = c.String("transport-loglevel")
if logLevel == "" {
logLevel = "fatal"
}
}
loggerOpts = append(loggerOpts, logger.LogLevelString(logLevel))
return logger.New(loggerOpts...)
}
func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan struct{}) error {
logger, err := createLogger(c, false)
if err != nil {
return cliutil.PrintLoggerSetupError("error setting up logger", err)
}
_ = raven.SetDSN(sentryDSN)
var wg sync.WaitGroup
listeners := gracenet.Net{}
@@ -185,65 +251,45 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
dnsReadySignal := make(chan struct{})
if c.String("config") == "" {
logger.Warnf("Cannot determine default configuration path. No file %v in %v", config.DefaultConfigFiles, config.DefaultConfigDirs)
}
if err := configMainLogger(c); err != nil {
return errors.Wrap(err, "Error configuring logger")
}
transportLogger, err := configTransportLogger(c)
if err != nil {
return errors.Wrap(err, "Error configuring transport logger")
logger.Infof("Cannot determine default configuration path. No file %v in %v", config.DefaultConfigFiles, config.DefaultConfigDirs)
}
if c.IsSet("trace-output") {
tmpTraceFile, err := ioutil.TempFile("", "trace")
if err != nil {
logger.WithError(err).Error("Failed to create new temporary file to save trace output")
logger.Errorf("Failed to create new temporary file to save trace output: %s", err)
}
defer func() {
if err := tmpTraceFile.Close(); err != nil {
logger.WithError(err).Errorf("Failed to close trace output file %s", tmpTraceFile.Name())
logger.Errorf("Failed to close trace output file %s with error: %s", tmpTraceFile.Name(), err)
}
if err := os.Rename(tmpTraceFile.Name(), c.String("trace-output")); err != nil {
logger.WithError(err).Errorf("Failed to rename temporary trace output file %s to %s", tmpTraceFile.Name(), c.String("trace-output"))
logger.Errorf("Failed to rename temporary trace output file %s to %s with error: %s", tmpTraceFile.Name(), c.String("trace-output"), err)
} else {
err := os.Remove(tmpTraceFile.Name())
if err != nil {
logger.WithError(err).Errorf("Failed to remove the temporary trace file %s", tmpTraceFile.Name())
logger.Errorf("Failed to remove the temporary trace file %s with error: %s", tmpTraceFile.Name(), err)
}
}
}()
if err := trace.Start(tmpTraceFile); err != nil {
logger.WithError(err).Error("Failed to start trace")
logger.Errorf("Failed to start trace: %s", err)
return errors.Wrap(err, "Error starting tracing")
}
defer trace.Stop()
}
if c.String("logfile") != "" {
if err := initLogFile(c, logger, transportLogger); err != nil {
logger.Error(err)
}
}
if err := handleDeprecatedOptions(c); err != nil {
return err
}
buildInfo := origin.GetBuildInfo()
logger.Infof("Build info: %+v", *buildInfo)
logger.Infof("Version %s", version)
logClientOptions(c)
buildInfo := buildinfo.GetBuildInfo(version)
buildInfo.Log(logger)
logClientOptions(c, logger)
if c.IsSet("proxy-dns") {
wg.Add(1)
go func() {
defer wg.Done()
errC <- runDNSProxyServer(c, dnsReadySignal, shutdownC)
errC <- runDNSProxyServer(c, dnsReadySignal, shutdownC, logger)
}()
} else {
close(dnsReadySignal)
@@ -252,19 +298,9 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
// Wait for proxy-dns to come up (if used)
<-dnsReadySignal
// update needs to be after DNS proxy is up to resolve equinox server address
if updater.IsAutoupdateEnabled(c) {
logger.Infof("Autoupdate frequency is set to %v", c.Duration("autoupdate-freq"))
wg.Add(1)
go func() {
defer wg.Done()
errC <- updater.Autoupdate(c.Duration("autoupdate-freq"), &listeners, shutdownC)
}()
}
metricsListener, err := listeners.Listen("tcp", c.String("metrics"))
if err != nil {
logger.WithError(err).Error("Error opening metrics server listener")
logger.Errorf("Error opening metrics server listener: %s", err)
return errors.Wrap(err, "Error opening metrics server listener")
}
defer metricsListener.Close()
@@ -276,20 +312,44 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
go notifySystemd(connectedSignal)
if c.IsSet("pidfile") {
go writePidFile(connectedSignal, c.String("pidfile"))
go writePidFile(connectedSignal, c.String("pidfile"), logger)
}
cloudflaredID, err := uuid.NewRandom()
if err != nil {
logger.Errorf("Cannot generate cloudflared ID: %s", err)
return err
}
ctx, cancel := context.WithCancel(context.Background())
go func() {
<-shutdownC
cancel()
}()
// update needs to be after DNS proxy is up to resolve equinox server address
if updater.IsAutoupdateEnabled(c, logger) {
logger.Infof("Autoupdate frequency is set to %v", c.Duration("autoupdate-freq"))
wg.Add(1)
go func() {
defer wg.Done()
autoupdater := updater.NewAutoUpdater(c.Duration("autoupdate-freq"), &listeners, logger)
errC <- autoupdater.Run(ctx)
}()
}
// Serve DNS proxy stand-alone if no hostname or tag or app is going to run
if dnsProxyStandAlone(c) {
connectedSignal.Notify()
// no grace period, handle SIGINT/SIGTERM immediately
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, 0)
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, 0, logger)
}
if c.IsSet("hello-world") {
logger.Infof("hello-world set")
helloListener, err := hello.CreateTLSListener("127.0.0.1:")
if err != nil {
logger.WithError(err).Error("Cannot start Hello World Server")
logger.Errorf("Cannot start Hello World Server: %s", err)
return errors.Wrap(err, "Cannot start Hello World Server")
}
defer helloListener.Close()
@@ -301,46 +361,142 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
c.Set("url", "https://"+helloListener.Addr().String())
}
if host := hostnameFromURI(c.String("url")); host != "" {
if c.IsSet(sshServerFlag) {
if runtime.GOOS != "darwin" && runtime.GOOS != "linux" {
msg := fmt.Sprintf("--ssh-server is not supported on %s", runtime.GOOS)
logger.Error(msg)
return errors.New(msg)
}
logger.Infof("ssh-server set")
logManager := sshlog.NewEmptyManager()
if c.IsSet(bucketNameFlag) && c.IsSet(regionNameFlag) && c.IsSet(accessKeyIDFlag) && c.IsSet(secretIDFlag) {
uploader, err := awsuploader.NewFileUploader(c.String(bucketNameFlag), c.String(regionNameFlag),
c.String(accessKeyIDFlag), c.String(secretIDFlag), c.String(sessionTokenIDFlag), c.String(s3URLFlag))
if err != nil {
msg := "Cannot create uploader for SSH Server"
logger.Errorf("%s: %s", msg, err)
return errors.Wrap(err, msg)
}
if err := os.MkdirAll(sshLogFileDirectory, 0700); err != nil {
msg := fmt.Sprintf("Cannot create SSH log file directory %s", sshLogFileDirectory)
logger.Errorf("%s: %s", msg, err)
return errors.Wrap(err, msg)
}
logManager = sshlog.New(sshLogFileDirectory)
uploadManager := awsuploader.NewDirectoryUploadManager(logger, uploader, sshLogFileDirectory, 30*time.Minute, shutdownC)
uploadManager.Start()
}
localServerAddress := "127.0.0.1:" + c.String(sshPortFlag)
server, err := sshserver.New(logManager, logger, version, localServerAddress, c.String("hostname"), c.Path(hostKeyPath), shutdownC, c.Duration(sshIdleTimeoutFlag), c.Duration(sshMaxTimeoutFlag))
if err != nil {
msg := "Cannot create new SSH Server"
logger.Errorf("%s: %s", msg, err)
return errors.Wrap(err, msg)
}
wg.Add(1)
go func() {
defer wg.Done()
if err = server.Start(); err != nil && err != ssh.ErrServerClosed {
logger.Errorf("SSH server error: %s", err)
// TODO: remove when declarative tunnels are implemented.
close(shutdownC)
}
}()
c.Set("url", "ssh://"+localServerAddress)
}
url := c.String("url")
hostname := c.String("hostname")
if url == hostname && url != "" && hostname != "" {
errText := "hostname and url shouldn't match. See --help for more information"
logger.Error(errText)
return fmt.Errorf(errText)
}
if staticHost := hostnameFromURI(c.String("url")); isProxyDestinationConfigured(staticHost, c) {
listener, err := net.Listen("tcp", "127.0.0.1:")
if err != nil {
logger.WithError(err).Error("Cannot start Websocket Proxy Server")
logger.Errorf("Cannot start Websocket Proxy Server: %s", err)
return errors.Wrap(err, "Cannot start Websocket Proxy Server")
}
wg.Add(1)
go func() {
defer wg.Done()
errC <- websocket.StartProxyServer(logger, listener, host, shutdownC)
streamHandler := websocket.DefaultStreamHandler
if c.IsSet(socks5Flag) {
logger.Info("SOCKS5 server started")
streamHandler = func(wsConn *websocket.Conn, remoteConn net.Conn, _ http.Header) {
dialer := socks.NewConnDialer(remoteConn)
requestHandler := socks.NewRequestHandler(dialer)
socksServer := socks.NewConnectionHandler(requestHandler)
socksServer.Serve(wsConn)
}
} else if c.IsSet(sshServerFlag) {
streamHandler = func(wsConn *websocket.Conn, remoteConn net.Conn, requestHeaders http.Header) {
if finalDestination := requestHeaders.Get(h2mux.CFJumpDestinationHeader); finalDestination != "" {
token := requestHeaders.Get(h2mux.CFAccessTokenHeader)
if err := websocket.SendSSHPreamble(remoteConn, finalDestination, token); err != nil {
logger.Errorf("Failed to send SSH preamble: %s", err)
return
}
}
websocket.DefaultStreamHandler(wsConn, remoteConn, requestHeaders)
}
}
errC <- websocket.StartProxyServer(logger, listener, staticHost, shutdownC, streamHandler)
}()
c.Set("url", "http://"+listener.Addr().String())
}
transportLogger, err := createLogger(c, true)
if err != nil {
return errors.Wrap(err, "error setting up transport logger")
}
tunnelConfig, err := prepareTunnelConfig(c, buildInfo, version, logger, transportLogger)
if err != nil {
return err
}
reconnectCh := make(chan origin.ReconnectSignal, 1)
if c.IsSet("stdin-control") {
logger.Info("Enabling control through stdin")
go stdinControl(reconnectCh, logger)
}
wg.Add(1)
go func() {
defer wg.Done()
errC <- origin.StartTunnelDaemon(tunnelConfig, graceShutdownC, connectedSignal)
errC <- origin.StartTunnelDaemon(ctx, tunnelConfig, connectedSignal, cloudflaredID, reconnectCh)
}()
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, c.Duration("grace-period"))
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, c.Duration("grace-period"), logger)
}
func Before(c *cli.Context) error {
logger, err := createLogger(c, false)
if err != nil {
return cliutil.PrintLoggerSetupError("error setting up logger", err)
}
if c.String("config") == "" {
logger.Debugf("Cannot determine default configuration path. No file %v in %v", config.DefaultConfigFiles, config.DefaultConfigDirs)
}
inputSource, err := config.FindInputSourceContext(c)
if err != nil {
logger.WithError(err).Errorf("Cannot load configuration from %s", c.String("config"))
logger.Errorf("Cannot load configuration from %s: %s", c.String("config"), err)
return err
} else if inputSource != nil {
err := altsrc.ApplyInputSourceValues(c, inputSource, c.App.Flags)
if err != nil {
logger.WithError(err).Errorf("Cannot apply configuration from %s", c.String("config"))
logger.Errorf("Cannot apply configuration from %s: %s", c.String("config"), err)
return err
}
logger.Debugf("Applied configuration from %s", c.String("config"))
@@ -348,21 +504,27 @@ func Before(c *cli.Context) error {
return nil
}
// isProxyDestinationConfigured returns true if there is a static host set or if bastion mode is set.
func isProxyDestinationConfigured(staticHost string, c *cli.Context) bool {
return staticHost != "" || c.IsSet(bastionFlag)
}
func waitToShutdown(wg *sync.WaitGroup,
errC chan error,
shutdownC, graceShutdownC chan struct{},
gracePeriod time.Duration,
logger logger.Service,
) error {
var err error
if gracePeriod > 0 {
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceShutdownC, gracePeriod)
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceShutdownC, gracePeriod, logger)
} else {
err = waitForSignal(errC, shutdownC)
err = waitForSignal(errC, shutdownC, logger)
close(graceShutdownC)
}
if err != nil {
logger.WithError(err).Error("Quitting due to error")
logger.Errorf("Quitting due to error: %s", err)
} else {
logger.Info("Quitting...")
}
@@ -380,11 +542,17 @@ func notifySystemd(waitForSignal *signal.Signal) {
daemon.SdNotify(false, "READY=1")
}
func writePidFile(waitForSignal *signal.Signal, pidFile string) {
func writePidFile(waitForSignal *signal.Signal, pidFile string, logger logger.Service) {
<-waitForSignal.Wait()
file, err := os.Create(pidFile)
expandedPath, err := homedir.Expand(pidFile)
if err != nil {
logger.WithError(err).Errorf("Unable to write pid to %s", pidFile)
logger.Errorf("Unable to expand %s, try to use absolute path in --pidfile: %s", pidFile, err)
return
}
file, err := os.Create(expandedPath)
if err != nil {
logger.Errorf("Unable to write pid to %s: %s", expandedPath, err)
return
}
defer file.Close()
fmt.Fprintf(file, "%d", os.Getpid())
@@ -400,6 +568,10 @@ func hostnameFromURI(uri string) string {
return addPortIfMissing(u, 22)
case "rdp":
return addPortIfMissing(u, 3389)
case "smb":
return addPortIfMissing(u, 445)
case "tcp":
return addPortIfMissing(u, 7864) // just a random port since there isn't a default in this case
}
return ""
}
@@ -411,6 +583,60 @@ 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 := Before(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 = tunnel(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 {
return []cli.Flag{
&cli.StringFlag{
@@ -421,15 +647,16 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
},
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "autoupdate-freq",
Usage: "Autoupdate frequency. Default is 24h.",
Value: time.Hour * 24,
Usage: fmt.Sprintf("Autoupdate frequency. Default is %v.", updater.DefaultCheckUpdateFreq),
Value: updater.DefaultCheckUpdateFreq,
Hidden: shouldHide,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "no-autoupdate",
Usage: "Disable periodic check for updates, restarting the server with the new version.",
Value: false,
Hidden: shouldHide,
Name: "no-autoupdate",
Usage: "Disable periodic check for updates, restarting the server with the new version.",
EnvVars: []string{"NO_AUTOUPDATE"},
Value: false,
Hidden: shouldHide,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "is-autoupdated",
@@ -439,12 +666,12 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: "edge",
Usage: "Address of the Cloudflare tunnel server.",
Usage: "Address of the Cloudflare tunnel server. Only works in Cloudflare's internal testing environment.",
EnvVars: []string{"TUNNEL_EDGE"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "cacert",
Name: tlsconfig.CaCertFlag,
Usage: "Certificate Authority authenticating connections with Cloudflare's edge network.",
EnvVars: []string{"TUNNEL_CACERT"},
Hidden: true,
@@ -463,7 +690,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "origin-ca-pool",
Name: tlsconfig.OriginCAPoolFlag,
Usage: "Path to the CA for the certificate of your origin. This option should be used only if your certificate is not signed by Cloudflare.",
EnvVars: []string{"TUNNEL_ORIGIN_CA_POOL"},
Hidden: shouldHide,
@@ -487,6 +714,12 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
EnvVars: []string{"TUNNEL_HOSTNAME"},
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "http-host-header",
Usage: "Sets the HTTP Host header for the local webserver.",
EnvVars: []string{"TUNNEL_HTTP_HOST_HEADER"},
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "origin-server-name",
Usage: "Hostname on the origin server certificate.",
@@ -523,6 +756,13 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
EnvVars: []string{"TUNNEL_API_CA_KEY"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "api-url",
Usage: "Base URL for Cloudflare API v4",
EnvVars: []string{"TUNNEL_API_URL"},
Value: "https://api.cloudflare.com/client/v4",
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "metrics",
Value: "localhost:",
@@ -558,15 +798,15 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
altsrc.NewStringFlag(&cli.StringFlag{
Name: "loglevel",
Value: "info",
Usage: "Application logging level {panic, fatal, error, warn, info, debug}. " + debugLevelWarning,
Usage: "Application logging level {fatal, error, info, debug}. " + debugLevelWarning,
EnvVars: []string{"TUNNEL_LOGLEVEL"},
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "transport-loglevel",
Aliases: []string{"proto-loglevel"}, // This flag used to be called proto-loglevel
Value: "warn",
Usage: "Transport logging level(previously called protocol logging level) {panic, fatal, error, warn, info, debug}",
Value: "fatal",
Usage: "Transport logging level(previously called protocol logging level) {fatal, error, info, debug}",
EnvVars: []string{"TUNNEL_PROTO_LOGLEVEL", "TUNNEL_TRANSPORT_LOGLEVEL"},
Hidden: shouldHide,
}),
@@ -584,6 +824,13 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
EnvVars: []string{"TUNNEL_HELLO_WORLD"},
Hidden: shouldHide,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: sshServerFlag,
Value: false,
Usage: "Run an SSH Server",
EnvVars: []string{"TUNNEL_SSH_SERVER"},
Hidden: true, // TODO: remove when feature is complete
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "pidfile",
Usage: "Write the application's PID to this file after first successful connection.",
@@ -596,6 +843,12 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
EnvVars: []string{"TUNNEL_LOGFILE"},
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: logDirectoryFlag,
Usage: "Save application log to this directory for reporting issues.",
EnvVars: []string{"TUNNEL_LOGDIRECTORY"},
Hidden: shouldHide,
}),
altsrc.NewIntFlag(&cli.IntFlag{
Name: "ha-connections",
Value: 4,
@@ -636,6 +889,18 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
Value: time.Second * 90,
Hidden: shouldHide,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "proxy-connection-timeout",
Usage: "HTTP proxy timeout for closing an idle connection",
Value: time.Second * 90,
Hidden: shouldHide,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "proxy-expect-continue-timeout",
Usage: "HTTP proxy timeout for closing an idle connection",
Value: time.Second * 90,
Hidden: shouldHide,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "proxy-dns",
Usage: "Run a DNS over HTTPS proxy server.",
@@ -663,6 +928,13 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
EnvVars: []string{"TUNNEL_DNS_UPSTREAM"},
Hidden: shouldHide,
}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: "proxy-dns-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"},
Hidden: shouldHide,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "grace-period",
Usage: "Duration to accept new requests after cloudflared receives first SIGINT/SIGTERM. A second SIGINT/SIGTERM will force cloudflared to shutdown immediately.",
@@ -689,5 +961,141 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
EnvVars: []string{"TUNNEL_TRACE_OUTPUT"},
Hidden: shouldHide,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "use-reconnect-token",
Usage: "Test reestablishing connections with the new 'reconnect token' flow.",
Value: true,
EnvVars: []string{"TUNNEL_USE_RECONNECT_TOKEN"},
Hidden: true,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "use-quick-reconnects",
Usage: "Test reestablishing connections with the new 'connection digest' flow.",
Value: true,
EnvVars: []string{"TUNNEL_USE_QUICK_RECONNECTS"},
Hidden: true,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "dial-edge-timeout",
Usage: "Maximum wait time to set up a connection with the edge",
Value: time.Second * 15,
EnvVars: []string{"DIAL_EDGE_TIMEOUT"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: sshPortFlag,
Usage: "Localhost port that cloudflared SSH server will run on",
Value: "2222",
EnvVars: []string{"LOCAL_SSH_PORT"},
Hidden: true,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: sshIdleTimeoutFlag,
Usage: "Connection timeout after no activity",
EnvVars: []string{"SSH_IDLE_TIMEOUT"},
Hidden: true,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: sshMaxTimeoutFlag,
Usage: "Absolute connection timeout",
EnvVars: []string{"SSH_MAX_TIMEOUT"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: bucketNameFlag,
Usage: "Bucket name of where to upload SSH logs",
EnvVars: []string{"BUCKET_ID"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: regionNameFlag,
Usage: "Region name of where to upload SSH logs",
EnvVars: []string{"REGION_ID"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: accessKeyIDFlag,
Usage: "Access Key ID of where to upload SSH logs",
EnvVars: []string{"ACCESS_CLIENT_ID"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: secretIDFlag,
Usage: "Secret ID of where to upload SSH logs",
EnvVars: []string{"SECRET_ID"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: sessionTokenIDFlag,
Usage: "Session Token to use in the configuration of SSH logs uploading",
EnvVars: []string{"SESSION_TOKEN_ID"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: s3URLFlag,
Usage: "S3 url of where to upload SSH logs",
EnvVars: []string{"S3_URL"},
Hidden: true,
}),
altsrc.NewPathFlag(&cli.PathFlag{
Name: hostKeyPath,
Usage: "Absolute path of directory to save SSH host keys in",
EnvVars: []string{"HOST_KEY_PATH"},
Hidden: true,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "stdin-control",
Usage: "Control the process using commands sent through stdin",
EnvVars: []string{"STDIN-CONTROL"},
Hidden: true,
Value: false,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: socks5Flag,
Usage: "specify if this tunnel is running as a SOCK5 Server",
EnvVars: []string{"TUNNEL_SOCKS"},
Value: false,
Hidden: shouldHide,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: bastionFlag,
Value: false,
Usage: "Runs as jump host",
EnvVars: []string{"TUNNEL_BASTION"},
Hidden: shouldHide,
}),
}
}
func stdinControl(reconnectCh chan origin.ReconnectSignal, logger logger.Service) {
for {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
command := scanner.Text()
parts := strings.SplitN(command, " ", 2)
switch parts[0] {
case "":
break
case "reconnect":
var reconnect origin.ReconnectSignal
if len(parts) > 1 {
var err error
if reconnect.Delay, err = time.ParseDuration(parts[1]); err != nil {
logger.Error(err.Error())
continue
}
}
logger.Infof("Sending reconnect signal %+v", reconnect)
reconnectCh <- reconnect
default:
logger.Infof("Unknown command: %s", command)
fallthrough
case "help":
logger.Info(`Supported command:
reconnect [delay]
- restarts one randomly chosen connection with optional delay before reconnect`)
}
}
}
}
+6 -4
View File
@@ -6,10 +6,12 @@ import (
"github.com/stretchr/testify/assert"
)
func TesthostnameFromURI(t *testing.T) {
assert.Equal(t, "ssh://awesome.warptunnels.horse:22", hostnameFromURI("ssh://awesome.warptunnels.horse:22"))
assert.Equal(t, "ssh://awesome.warptunnels.horse:22", hostnameFromURI("ssh://awesome.warptunnels.horse"))
assert.Equal(t, "rdp://localhost:3389", hostnameFromURI("rdp://localhost"))
func TestHostnameFromURI(t *testing.T) {
assert.Equal(t, "awesome.warptunnels.horse:22", hostnameFromURI("ssh://awesome.warptunnels.horse:22"))
assert.Equal(t, "awesome.warptunnels.horse:22", hostnameFromURI("ssh://awesome.warptunnels.horse"))
assert.Equal(t, "awesome.warptunnels.horse:2222", hostnameFromURI("ssh://awesome.warptunnels.horse:2222"))
assert.Equal(t, "localhost:3389", hostnameFromURI("rdp://localhost"))
assert.Equal(t, "localhost:3390", hostnameFromURI("rdp://localhost:3390"))
assert.Equal(t, "", hostnameFromURI("trash"))
assert.Equal(t, "", hostnameFromURI("https://awesomesauce.com"))
}
+92 -182
View File
@@ -3,18 +3,18 @@ package tunnel
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/origin"
"github.com/cloudflare/cloudflared/tlsconfig"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
@@ -23,7 +23,6 @@ import (
"github.com/google/uuid"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh/terminal"
"gopkg.in/urfave/cli.v2"
)
@@ -47,25 +46,16 @@ func findDefaultOriginCertPath() string {
return ""
}
func generateRandomClientID(logger *logrus.Logger) (string, error) {
func generateRandomClientID(logger logger.Service) (string, error) {
u, err := uuid.NewRandom()
if err != nil {
logger.WithError(err).Error("couldn't create UUID for client ID")
logger.Errorf("couldn't create UUID for client ID %s", err)
return "", err
}
return u.String(), nil
}
func handleDeprecatedOptions(c *cli.Context) error {
// Fail if the user provided an old authentication method
if c.IsSet("api-key") || c.IsSet("api-email") || c.IsSet("api-ca-key") {
logger.Error("You don't need to give us your api-key anymore. Please use the new login method. Just run cloudflared login")
return fmt.Errorf("Client provided deprecated options")
}
return nil
}
func logClientOptions(c *cli.Context) {
func logClientOptions(c *cli.Context, logger logger.Service) {
flags := make(map[string]interface{})
for _, flag := range c.LocalFlagNames() {
flags[flag] = c.Generic(flag)
@@ -79,7 +69,7 @@ func logClientOptions(c *cli.Context) {
}
if len(flags) > 0 {
logger.WithFields(flags).Info("Flags")
logger.Infof("Environment variables %v", flags)
}
envs := make(map[string]string)
@@ -102,27 +92,29 @@ func dnsProxyStandAlone(c *cli.Context) bool {
return c.IsSet("proxy-dns") && (!c.IsSet("hostname") && !c.IsSet("tag") && !c.IsSet("hello-world"))
}
func getOriginCert(c *cli.Context) ([]byte, error) {
if c.String("origincert") == "" {
logger.Warnf("Cannot determine default origin certificate path. No file %s in %v", config.DefaultCredentialFile, config.DefaultConfigDirs)
func findOriginCert(c *cli.Context, logger logger.Service) (string, error) {
originCertPath := c.String("origincert")
if originCertPath == "" {
logger.Infof("Cannot determine default origin certificate path. No file %s in %v", config.DefaultCredentialFile, config.DefaultConfigDirs)
if isRunningFromTerminal() {
logger.Errorf("You need to specify the origin certificate path with --origincert option, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", argumentsUrl)
return nil, fmt.Errorf("Client didn't specify origincert path when running from terminal")
return "", fmt.Errorf("Client didn't specify origincert path when running from terminal")
} else {
logger.Errorf("You need to specify the origin certificate path by specifying the origincert option in the configuration file, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", serviceUrl)
return nil, fmt.Errorf("Client didn't specify origincert path")
return "", fmt.Errorf("Client didn't specify origincert path")
}
}
// Check that the user has acquired a certificate using the login command
originCertPath, err := homedir.Expand(c.String("origincert"))
var err error
originCertPath, err = homedir.Expand(originCertPath)
if err != nil {
logger.WithError(err).Errorf("Cannot resolve path %s", c.String("origincert"))
return nil, fmt.Errorf("Cannot resolve path %s", c.String("origincert"))
logger.Errorf("Cannot resolve path %s: %s", originCertPath, err)
return "", fmt.Errorf("Cannot resolve path %s", originCertPath)
}
// Check that the user has acquired a certificate using the login command
ok, err := config.FileExists(originCertPath)
if err != nil {
logger.Errorf("Cannot check if origin cert exists at path %s", c.String("origincert"))
return nil, fmt.Errorf("Cannot check if origin cert exists at path %s", c.String("origincert"))
logger.Errorf("Cannot check if origin cert exists at path %s", originCertPath)
return "", fmt.Errorf("Cannot check if origin cert exists at path %s", originCertPath)
}
if !ok {
logger.Errorf(`Cannot find a valid certificate for your origin at the path:
@@ -134,26 +126,42 @@ If you don't have a certificate signed by Cloudflare, run the command:
%s login
`, originCertPath, os.Args[0])
return nil, fmt.Errorf("Cannot find a valid certificate at the path %s", originCertPath)
return "", fmt.Errorf("Cannot find a valid certificate at the path %s", originCertPath)
}
return originCertPath, nil
}
func readOriginCert(originCertPath string, logger logger.Service) ([]byte, error) {
logger.Debugf("Reading origin cert from %s", originCertPath)
// Easier to send the certificate as []byte via RPC than decoding it at this point
originCert, err := ioutil.ReadFile(originCertPath)
if err != nil {
logger.WithError(err).Errorf("Cannot read %s to load origin certificate", originCertPath)
logger.Errorf("Cannot read %s to load origin certificate: %s", originCertPath, err)
return nil, fmt.Errorf("Cannot read %s to load origin certificate", originCertPath)
}
return originCert, nil
}
func getOriginCert(c *cli.Context, logger logger.Service) ([]byte, error) {
if originCertPath, err := findOriginCert(c, logger); err != nil {
return nil, err
} else {
return readOriginCert(originCertPath, logger)
}
}
func prepareTunnelConfig(
c *cli.Context,
buildInfo *origin.BuildInfo,
version string, logger,
transportLogger *logrus.Logger,
buildInfo *buildinfo.BuildInfo,
version string,
logger logger.Service,
transportLogger logger.Service,
) (*origin.TunnelConfig, error) {
hostname, err := validation.ValidateHostname(c.String("hostname"))
if err != nil {
logger.WithError(err).Error("Invalid hostname")
logger.Errorf("Invalid hostname: %s", err)
return nil, errors.Wrap(err, "Invalid hostname")
}
isFreeTunnel := hostname == ""
@@ -167,7 +175,7 @@ func prepareTunnelConfig(
tags, err := NewTagSliceFromCLI(c.StringSlice("tag"))
if err != nil {
logger.WithError(err).Error("Tag parse failure")
logger.Errorf("Tag parse failure: %s", err)
return nil, errors.Wrap(err, "Tag parse failure")
}
@@ -175,21 +183,21 @@ func prepareTunnelConfig(
originURL, err := config.ValidateUrl(c)
if err != nil {
logger.WithError(err).Error("Error validating origin URL")
logger.Errorf("Error validating origin URL: %s", err)
return nil, errors.Wrap(err, "Error validating origin URL")
}
var originCert []byte
if !isFreeTunnel {
originCert, err = getOriginCert(c)
originCert, err = getOriginCert(c, logger)
if err != nil {
return nil, errors.Wrap(err, "Error getting origin cert")
}
}
originCertPool, err := loadCertPool(c, logger)
originCertPool, err := tlsconfig.LoadOriginCA(c, logger)
if err != nil {
logger.WithError(err).Error("Error loading cert pool")
logger.Errorf("Error loading cert pool: %s", err)
return nil, errors.Wrap(err, "Error loading cert pool")
}
@@ -197,22 +205,26 @@ func prepareTunnelConfig(
httpTransport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
MaxIdleConns: c.Int("proxy-keepalive-connections"),
MaxIdleConnsPerHost: c.Int("proxy-keepalive-connections"),
IdleConnTimeout: c.Duration("proxy-keepalive-timeout"),
TLSHandshakeTimeout: c.Duration("proxy-tls-timeout"),
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{RootCAs: originCertPool, InsecureSkipVerify: c.IsSet("no-tls-verify")},
}
dialContext := (&net.Dialer{
dialer := &net.Dialer{
Timeout: c.Duration("proxy-connect-timeout"),
KeepAlive: c.Duration("proxy-tcp-keepalive"),
DualStack: !c.Bool("proxy-no-happy-eyeballs"),
}).DialContext
}
if c.Bool("proxy-no-happy-eyeballs") {
dialer.FallbackDelay = -1 // As of Golang 1.12, a negative delay disables "happy eyeballs"
}
dialContext := dialer.DialContext
if c.IsSet("unix-socket") {
unixSocket, err := config.ValidateUnixSocket(c)
if err != nil {
logger.WithError(err).Error("Error validating --unix-socket")
logger.Errorf("Error validating --unix-socket: %s", err)
return nil, errors.Wrap(err, "Error validating --unix-socket")
}
@@ -229,156 +241,54 @@ func prepareTunnelConfig(
if !c.IsSet("hello-world") && c.IsSet("origin-server-name") {
httpTransport.TLSClientConfig.ServerName = c.String("origin-server-name")
}
err = validation.ValidateHTTPService(originURL, hostname, httpTransport)
if err != nil {
logger.WithError(err).Error("unable to connect to the origin")
return nil, errors.Wrap(err, "unable to connect to the origin")
// If tunnel running in bastion mode, a connection to origin will not exist until initiated by the client.
if !c.IsSet(bastionFlag) {
if err = validation.ValidateHTTPService(originURL, hostname, httpTransport); err != nil {
logger.Errorf("unable to connect to the origin: %s", err)
}
}
toEdgeTLSConfig, err := createTunnelConfig(c)
toEdgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c)
if err != nil {
logger.WithError(err).Error("unable to create TLS config to connect with edge")
logger.Errorf("unable to create TLS config to connect with edge: %s", err)
return nil, errors.Wrap(err, "unable to create TLS config to connect with edge")
}
return &origin.TunnelConfig{
EdgeAddrs: c.StringSlice("edge"),
OriginUrl: originURL,
Hostname: hostname,
OriginCert: originCert,
TlsConfig: toEdgeTLSConfig,
ClientTlsConfig: httpTransport.TLSClientConfig,
Retries: c.Uint("retries"),
HeartbeatInterval: c.Duration("heartbeat-interval"),
MaxHeartbeats: c.Uint64("heartbeat-count"),
ClientID: clientID,
BuildInfo: buildInfo,
ReportedVersion: version,
LBPool: c.String("lb-pool"),
Tags: tags,
HAConnections: c.Int("ha-connections"),
HTTPTransport: httpTransport,
Metrics: tunnelMetrics,
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
TransportLogger: transportLogger,
Logger: logger,
IsAutoupdated: c.Bool("is-autoupdated"),
GracePeriod: c.Duration("grace-period"),
RunFromTerminal: isRunningFromTerminal(),
NoChunkedEncoding: c.Bool("no-chunked-encoding"),
CompressionQuality: c.Uint64("compression-quality"),
IncidentLookup: origin.NewIncidentLookup(),
IsFreeTunnel: isFreeTunnel,
BuildInfo: buildInfo,
ClientID: clientID,
ClientTlsConfig: httpTransport.TLSClientConfig,
CompressionQuality: c.Uint64("compression-quality"),
EdgeAddrs: c.StringSlice("edge"),
GracePeriod: c.Duration("grace-period"),
HAConnections: c.Int("ha-connections"),
HTTPTransport: httpTransport,
HeartbeatInterval: c.Duration("heartbeat-interval"),
Hostname: hostname,
HTTPHostHeader: c.String("http-host-header"),
IncidentLookup: origin.NewIncidentLookup(),
IsAutoupdated: c.Bool("is-autoupdated"),
IsFreeTunnel: isFreeTunnel,
LBPool: c.String("lb-pool"),
Logger: logger,
TransportLogger: transportLogger,
MaxHeartbeats: c.Uint64("heartbeat-count"),
Metrics: tunnelMetrics,
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
NoChunkedEncoding: c.Bool("no-chunked-encoding"),
OriginCert: originCert,
OriginUrl: originURL,
ReportedVersion: version,
Retries: c.Uint("retries"),
RunFromTerminal: isRunningFromTerminal(),
Tags: tags,
TlsConfig: toEdgeTLSConfig,
UseDeclarativeTunnel: c.Bool("use-declarative-tunnels"),
UseReconnectToken: c.Bool("use-reconnect-token"),
UseQuickReconnects: c.Bool("use-quick-reconnects"),
}, nil
}
func loadCertPool(c *cli.Context, logger *logrus.Logger) (*x509.CertPool, error) {
const originCAPoolFlag = "origin-ca-pool"
originCAPoolFilename := c.String(originCAPoolFlag)
var originCustomCAPool []byte
if originCAPoolFilename != "" {
var err error
originCustomCAPool, err = ioutil.ReadFile(originCAPoolFilename)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("unable to read the file %s for --%s", originCAPoolFilename, originCAPoolFlag))
}
}
originCertPool, err := loadOriginCertPool(originCustomCAPool)
if err != nil {
return nil, errors.Wrap(err, "error loading the certificate pool")
}
// Windows users should be notified that they can use the flag
if runtime.GOOS == "windows" && originCAPoolFilename == "" {
logger.Infof("cloudflared does not support loading the system root certificate pool on Windows. Please use the --%s to specify it", originCAPoolFlag)
}
return originCertPool, nil
}
func loadOriginCertPool(originCAPoolPEM []byte) (*x509.CertPool, error) {
// Get the global pool
certPool, err := loadGlobalCertPool()
if err != nil {
return nil, err
}
// Then, add any custom origin CA pool the user may have passed
if originCAPoolPEM != nil {
if !certPool.AppendCertsFromPEM(originCAPoolPEM) {
logger.Warn("could not append the provided origin CA to the cloudflared certificate pool")
}
}
return certPool, nil
}
func loadGlobalCertPool() (*x509.CertPool, error) {
// First, obtain the system certificate pool
certPool, err := x509.SystemCertPool()
if err != nil {
if runtime.GOOS != "windows" {
logger.WithError(err).Warn("error obtaining the system certificates")
}
certPool = x509.NewCertPool()
}
// Next, append the Cloudflare CAs into the system pool
cfRootCA, err := tlsconfig.GetCloudflareRootCA()
if err != nil {
return nil, errors.Wrap(err, "could not append Cloudflare Root CAs to cloudflared certificate pool")
}
for _, cert := range cfRootCA {
certPool.AddCert(cert)
}
// Finally, add the Hello certificate into the pool (since it's self-signed)
helloCert, err := tlsconfig.GetHelloCertificateX509()
if err != nil {
return nil, errors.Wrap(err, "could not append Hello server certificate to cloudflared certificate pool")
}
certPool.AddCert(helloCert)
return certPool, nil
}
func createTunnelConfig(c *cli.Context) (*tls.Config, error) {
var rootCAs []string
if c.String("cacert") != "" {
rootCAs = append(rootCAs, c.String("cacert"))
}
edgeAddrs := c.StringSlice("edge")
userConfig := &tlsconfig.TLSParameters{RootCAs: rootCAs}
tlsConfig, err := tlsconfig.GetConfig(userConfig)
if err != nil {
return nil, err
}
if tlsConfig.RootCAs == nil {
rootCAPool := x509.NewCertPool()
cfRootCA, err := tlsconfig.GetCloudflareRootCA()
if err != nil {
return nil, errors.Wrap(err, "could not append Cloudflare Root CAs to cloudflared certificate pool")
}
for _, cert := range cfRootCA {
rootCAPool.AddCert(cert)
}
tlsConfig.RootCAs = rootCAPool
tlsConfig.ServerName = "cftunnel.com"
} else if len(edgeAddrs) > 0 {
// Set for development environments and for testing specific origintunneld instances
tlsConfig.ServerName, _, _ = net.SplitHostPort(edgeAddrs[0])
}
if tlsConfig.ServerName == "" && !tlsConfig.InsecureSkipVerify {
return nil, fmt.Errorf("either ServerName or InsecureSkipVerify must be specified in the tls.Config")
}
return tlsConfig, nil
}
func isRunningFromTerminal() bool {
return terminal.IsTerminal(int(os.Stdout.Fd()))
}
-73
View File
@@ -1,73 +0,0 @@
package tunnel
import (
"fmt"
"os"
"github.com/cloudflare/cloudflared/log"
"github.com/rifflock/lfshook"
"github.com/sirupsen/logrus"
"gopkg.in/urfave/cli.v2"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
)
const 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."
var logger = log.CreateLogger()
func configMainLogger(c *cli.Context) error {
logLevel, err := logrus.ParseLevel(c.String("loglevel"))
if err != nil {
logger.WithError(err).Error("Unknown logging level specified")
return errors.Wrap(err, "Unknown logging level specified")
}
logger.SetLevel(logLevel)
if logLevel == logrus.DebugLevel {
logger.Warn(debugLevelWarning)
}
return nil
}
func configTransportLogger(c *cli.Context) (*logrus.Logger, error) {
transportLogLevel, err := logrus.ParseLevel(c.String("transport-loglevel"))
if err != nil {
logger.WithError(err).Fatal("Unknown transport logging level specified")
return nil, errors.Wrap(err, "Unknown transport logging level specified")
}
transportLogger := logrus.New()
transportLogger.Level = transportLogLevel
return transportLogger, nil
}
func initLogFile(c *cli.Context, loggers ...*logrus.Logger) error {
filePath, err := homedir.Expand(c.String("logfile"))
if err != nil {
return errors.Wrap(err, "Cannot resolve logfile path")
}
fileMode := os.O_WRONLY | os.O_APPEND | os.O_CREATE | os.O_TRUNC
// do not truncate log file if the client has been autoupdated
if c.Bool("is-autoupdated") {
fileMode = os.O_WRONLY | os.O_APPEND | os.O_CREATE
}
f, err := os.OpenFile(filePath, fileMode, 0664)
if err != nil {
errors.Wrap(err, fmt.Sprintf("Cannot open file %s", filePath))
}
defer f.Close()
pathMap := lfshook.PathMap{
logrus.InfoLevel: filePath,
logrus.ErrorLevel: filePath,
logrus.FatalLevel: filePath,
logrus.PanicLevel: filePath,
}
for _, l := range loggers {
l.Hooks.Add(lfshook.NewHook(pathMap, &logrus.JSONFormatter{}))
}
return nil
}
+9 -2
View File
@@ -9,16 +9,23 @@ import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/cmd/cloudflared/transfer"
"github.com/cloudflare/cloudflared/logger"
homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
cli "gopkg.in/urfave/cli.v2"
)
const (
baseLoginURL = "https://dash.cloudflare.com/argotunnel"
callbackStoreURL = "https://login.cloudflarewarp.com/"
callbackStoreURL = "https://login.argotunnel.com/"
)
func login(c *cli.Context) error {
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
path, ok, err := checkForExistingCert()
if ok {
fmt.Fprintf(os.Stdout, "You have an existing certificate at %s which login would overwrite.\nIf this is intentional, please move or delete that file then run this command again.\n", path)
@@ -33,7 +40,7 @@ func login(c *cli.Context) error {
return err
}
_, err = transfer.Run(loginURL, "cert", "callback", callbackStoreURL, path, false)
_, err = transfer.Run(loginURL, "cert", "callback", callbackStoreURL, path, false, true, logger)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to write the certificate due to the following error:\n%v\n\nYour browser will download the certificate instead. You will have to manually\ncopy it to the following path:\n\n%s\n", err, path)
return err
+3 -5
View File
@@ -1,6 +1,7 @@
package tunnel
import (
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tunneldns"
"gopkg.in/urfave/cli.v2"
@@ -8,23 +9,20 @@ import (
"github.com/pkg/errors"
)
func runDNSProxyServer(c *cli.Context, dnsReadySignal, shutdownC chan struct{}) error {
func runDNSProxyServer(c *cli.Context, dnsReadySignal, shutdownC chan struct{}, logger logger.Service) error {
port := c.Int("proxy-dns-port")
if port <= 0 || port > 65535 {
logger.Errorf("The 'proxy-dns-port' must be a valid port number in <1, 65535> range.")
return errors.New("The 'proxy-dns-port' must be a valid port number in <1, 65535> range.")
}
listener, err := tunneldns.CreateListener(c.String("proxy-dns-address"), uint16(port), c.StringSlice("proxy-dns-upstream"))
listener, err := tunneldns.CreateListener(c.String("proxy-dns-address"), uint16(port), c.StringSlice("proxy-dns-upstream"), c.StringSlice("proxy-dns-bootstrap"), logger)
if err != nil {
close(dnsReadySignal)
listener.Stop()
logger.WithError(err).Error("Cannot create the DNS over HTTPS proxy server")
return errors.Wrap(err, "Cannot create the DNS over HTTPS proxy server")
}
err = listener.Start(dnsReadySignal)
if err != nil {
logger.WithError(err).Error("Cannot start the DNS over HTTPS proxy server")
return errors.Wrap(err, "Cannot start the DNS over HTTPS proxy server")
}
<-shutdownC
+13 -6
View File
@@ -5,21 +5,25 @@ import (
"os/signal"
"syscall"
"time"
"github.com/cloudflare/cloudflared/logger"
)
// waitForSignal notifies all routines to shutdownC immediately by closing the
// shutdownC when one of the routines in main exits, or when this process receives
// SIGTERM/SIGINT
func waitForSignal(errC chan error, shutdownC chan struct{}) error {
func waitForSignal(errC chan error, shutdownC chan struct{}, logger logger.Service) error {
signals := make(chan os.Signal, 10)
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
defer signal.Stop(signals)
select {
case err := <-errC:
logger.Infof("terminating due to error: %v", err)
close(shutdownC)
return err
case <-signals:
case s := <-signals:
logger.Infof("terminating due to signal %s", s)
close(shutdownC)
case <-shutdownC:
}
@@ -37,6 +41,7 @@ func waitForSignal(errC chan error, shutdownC chan struct{}) error {
func waitForSignalWithGraceShutdown(errC chan error,
shutdownC, graceShutdownC chan struct{},
gracePeriod time.Duration,
logger logger.Service,
) error {
signals := make(chan os.Signal, 10)
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
@@ -44,14 +49,16 @@ func waitForSignalWithGraceShutdown(errC chan error,
select {
case err := <-errC:
logger.Infof("Initiating graceful shutdown due to %v ...", err)
close(graceShutdownC)
close(shutdownC)
return err
case <-signals:
case s := <-signals:
logger.Infof("Initiating graceful shutdown due to signal %s ...", s)
close(graceShutdownC)
waitForGracePeriod(signals, errC, shutdownC, gracePeriod)
waitForGracePeriod(signals, errC, shutdownC, gracePeriod, logger)
case <-graceShutdownC:
waitForGracePeriod(signals, errC, shutdownC, gracePeriod)
waitForGracePeriod(signals, errC, shutdownC, gracePeriod, logger)
case <-shutdownC:
close(graceShutdownC)
}
@@ -63,8 +70,8 @@ func waitForGracePeriod(signals chan os.Signal,
errC chan error,
shutdownC chan struct{},
gracePeriod time.Duration,
logger logger.Service,
) {
logger.Infof("Initiating graceful shutdown...")
// Unregister signal handler early, so the client can send a second SIGTERM/SIGINT
// to force shutdown cloudflared
signal.Stop(signals)
+12 -7
View File
@@ -6,6 +6,7 @@ import (
"testing"
"time"
"github.com/cloudflare/cloudflared/logger"
"github.com/stretchr/testify/assert"
)
@@ -27,6 +28,8 @@ func testChannelClosed(t *testing.T, c chan struct{}) {
}
func TestWaitForSignal(t *testing.T) {
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
// Test handling server error
errC := make(chan error)
shutdownC := make(chan struct{})
@@ -36,7 +39,7 @@ func TestWaitForSignal(t *testing.T) {
}()
// received error, shutdownC should be closed
err := waitForSignal(errC, shutdownC)
err := waitForSignal(errC, shutdownC, logger)
assert.Equal(t, serverErr, err)
testChannelClosed(t, shutdownC)
@@ -56,7 +59,7 @@ func TestWaitForSignal(t *testing.T) {
syscall.Kill(syscall.Getpid(), sig)
}(sig)
err = waitForSignal(errC, shutdownC)
err = waitForSignal(errC, shutdownC, logger)
assert.Equal(t, nil, err)
assert.Equal(t, shutdownErr, <-errC)
testChannelClosed(t, shutdownC)
@@ -73,8 +76,10 @@ func TestWaitForSignalWithGraceShutdown(t *testing.T) {
errC <- serverErr
}()
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
// received error, both shutdownC and graceshutdownC should be closed
err := waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick)
err := waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
assert.Equal(t, serverErr, err)
testChannelClosed(t, shutdownC)
testChannelClosed(t, graceshutdownC)
@@ -84,7 +89,7 @@ func TestWaitForSignalWithGraceShutdown(t *testing.T) {
shutdownC = make(chan struct{})
graceshutdownC = make(chan struct{})
close(shutdownC)
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick)
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
assert.NoError(t, err)
testChannelClosed(t, shutdownC)
testChannelClosed(t, graceshutdownC)
@@ -94,7 +99,7 @@ func TestWaitForSignalWithGraceShutdown(t *testing.T) {
shutdownC = make(chan struct{})
graceshutdownC = make(chan struct{})
close(graceshutdownC)
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick)
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
assert.NoError(t, err)
testChannelClosed(t, shutdownC)
testChannelClosed(t, graceshutdownC)
@@ -117,7 +122,7 @@ func TestWaitForSignalWithGraceShutdown(t *testing.T) {
syscall.Kill(syscall.Getpid(), sig)
}(sig)
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick)
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
assert.Equal(t, nil, err)
assert.Equal(t, graceShutdownErr, <-errC)
testChannelClosed(t, shutdownC)
@@ -143,7 +148,7 @@ func TestWaitForSignalWithGraceShutdown(t *testing.T) {
syscall.Kill(syscall.Getpid(), sig)
}(sig)
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick)
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
assert.Equal(t, nil, err)
assert.Equal(t, shutdownErr, <-errC)
testChannelClosed(t, shutdownC)
+276
View File
@@ -0,0 +1,276 @@
package tunnel
import (
"crypto/rand"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/pkg/errors"
"gopkg.in/urfave/cli.v2"
"gopkg.in/yaml.v2"
"github.com/cloudflare/cloudflared/certutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/cloudflare/cloudflared/tunnelstore"
)
var (
outputFormatFlag = &cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "Render output using given `FORMAT`. Valid options are 'json' or 'yaml'",
}
)
const hideSubcommands = true
func buildCreateCommand() *cli.Command {
return &cli.Command{
Name: "create",
Action: cliutil.ErrorHandler(createTunnel),
Usage: "Create a new tunnel with given name",
ArgsUsage: "TUNNEL-NAME",
Hidden: hideSubcommands,
Flags: []cli.Flag{outputFormatFlag},
}
}
// generateTunnelSecret as an array of 32 bytes using secure random number generator
func generateTunnelSecret() ([]byte, error) {
randomBytes := make([]byte, 32)
_, err := rand.Read(randomBytes)
return randomBytes, err
}
func createTunnel(c *cli.Context) error {
if c.NArg() != 1 {
return cliutil.UsageError(`"cloudflared tunnel create" requires exactly 1 argument, the name of tunnel to create.`)
}
name := c.Args().First()
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
tunnelSecret, err := generateTunnelSecret()
if err != nil {
return err
}
originCertPath, err := findOriginCert(c, logger)
if err != nil {
return errors.Wrap(err, "Error locating origin cert")
}
cert, err := getOriginCertFromContext(originCertPath, logger)
if err != nil {
return err
}
client := newTunnelstoreClient(c, cert, logger)
tunnel, err := client.CreateTunnel(name, tunnelSecret)
if err != nil {
return errors.Wrap(err, "Error creating a new tunnel")
}
if writeFileErr := writeTunnelCredentials(tunnel.ID, cert.AccountID, originCertPath, tunnelSecret, logger); err != nil {
var errorLines []string
errorLines = append(errorLines, fmt.Sprintf("Your tunnel '%v' was created with ID %v. However, cloudflared couldn't write to the tunnel credentials file at %v.json.", tunnel.Name, tunnel.ID, tunnel.ID))
errorLines = append(errorLines, fmt.Sprintf("The file-writing error is: %v", writeFileErr))
if deleteErr := client.DeleteTunnel(tunnel.ID); deleteErr != nil {
errorLines = append(errorLines, fmt.Sprintf("Cloudflared tried to delete the tunnel for you, but encountered an error. You should use `cloudflared tunnel delete %v` to delete the tunnel yourself, because the tunnel can't be run without the tunnelfile.", tunnel.ID))
errorLines = append(errorLines, fmt.Sprintf("The delete tunnel error is: %v", deleteErr))
} else {
errorLines = append(errorLines, fmt.Sprintf("The tunnel was deleted, because the tunnel can't be run without the tunnelfile"))
}
errorMsg := strings.Join(errorLines, "\n")
return errors.New(errorMsg)
}
if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" {
return renderOutput(outputFormat, &tunnel)
}
logger.Infof("Created tunnel %s with id %s", tunnel.Name, tunnel.ID)
return nil
}
func tunnelFilePath(tunnelID, originCertPath string) (string, error) {
fileName := fmt.Sprintf("%v.json", tunnelID)
return filepath.Clean(fmt.Sprintf("%v/../%v", originCertPath, fileName)), nil
}
func writeTunnelCredentials(tunnelID, accountID, originCertPath string, tunnelSecret []byte, logger logger.Service) error {
filePath, err := tunnelFilePath(tunnelID, originCertPath)
if err != nil {
return err
}
logger.Infof("Writing tunnel credentials to %v. cloudflared chose this file based on where your origin certificate was found.", filePath)
logger.Infof("Keep this file secret. To revoke these credentials, delete the tunnel.")
file, err := os.Create(filePath)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("Unable to write to %s", filePath))
}
defer file.Close()
body, err := json.Marshal(pogs.TunnelAuth{
AccountTag: accountID,
TunnelSecret: tunnelSecret,
})
if err != nil {
return errors.Wrap(err, "Unable to marshal tunnel credentials to JSON")
}
fmt.Fprintf(file, "%d", body)
return nil
}
func buildListCommand() *cli.Command {
return &cli.Command{
Name: "list",
Action: cliutil.ErrorHandler(listTunnels),
Usage: "List existing tunnels",
ArgsUsage: " ",
Hidden: hideSubcommands,
Flags: []cli.Flag{outputFormatFlag},
}
}
func listTunnels(c *cli.Context) error {
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
originCertPath, err := findOriginCert(c, logger)
if err != nil {
return errors.Wrap(err, "Error locating origin cert")
}
cert, err := getOriginCertFromContext(originCertPath, logger)
if err != nil {
return err
}
client := newTunnelstoreClient(c, cert, logger)
tunnels, err := client.ListTunnels()
if err != nil {
return errors.Wrap(err, "Error listing tunnels")
}
if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" {
return renderOutput(outputFormat, tunnels)
}
if len(tunnels) > 0 {
const listFormat = "%-40s%-30s%-30s%s\n"
fmt.Printf(listFormat, "ID", "NAME", "CREATED", "CONNECTIONS")
for _, t := range tunnels {
fmt.Printf(listFormat, t.ID, t.Name, t.CreatedAt.Format(time.RFC3339), fmtConnections(t.Connections))
}
} else {
fmt.Println("You have no tunnels, use 'cloudflared tunnel create' to define a new tunnel")
}
return nil
}
func fmtConnections(connections []tunnelstore.Connection) string {
// Count connections per colo
numConnsPerColo := make(map[string]uint, len(connections))
for _, connection := range connections {
numConnsPerColo[connection.ColoName]++
}
// Get sorted list of colos
sortedColos := []string{}
for coloName := range numConnsPerColo {
sortedColos = append(sortedColos, coloName)
}
sort.Strings(sortedColos)
// Map each colo to its frequency, combine into output string.
var output []string
for _, coloName := range sortedColos {
output = append(output, fmt.Sprintf("%dx%s", numConnsPerColo[coloName], coloName))
}
return strings.Join(output, ", ")
}
func buildDeleteCommand() *cli.Command {
return &cli.Command{
Name: "delete",
Action: cliutil.ErrorHandler(deleteTunnel),
Usage: "Delete existing tunnel with given ID",
ArgsUsage: "TUNNEL-ID",
Hidden: hideSubcommands,
}
}
func deleteTunnel(c *cli.Context) error {
if c.NArg() != 1 {
return cliutil.UsageError(`"cloudflared tunnel delete" requires exactly 1 argument, the ID of the tunnel to delete.`)
}
id := c.Args().First()
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
originCertPath, err := findOriginCert(c, logger)
if err != nil {
return errors.Wrap(err, "Error locating origin cert")
}
cert, err := getOriginCertFromContext(originCertPath, logger)
if err != nil {
return err
}
client := newTunnelstoreClient(c, cert, logger)
if err := client.DeleteTunnel(id); err != nil {
return errors.Wrapf(err, "Error deleting tunnel %s", id)
}
return nil
}
func renderOutput(format string, v interface{}) error {
switch format {
case "json":
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
return encoder.Encode(v)
case "yaml":
return yaml.NewEncoder(os.Stdout).Encode(v)
default:
return errors.Errorf("Unknown output format '%s'", format)
}
}
func newTunnelstoreClient(c *cli.Context, cert *certutil.OriginCert, logger logger.Service) tunnelstore.Client {
client := tunnelstore.NewRESTClient(c.String("api-url"), cert.AccountID, cert.ServiceKey, logger)
return client
}
func getOriginCertFromContext(originCertPath string, logger logger.Service) (*certutil.OriginCert, error) {
blocks, err := readOriginCert(originCertPath, logger)
if err != nil {
return nil, errors.Wrapf(err, "Can't read origin cert from %s", originCertPath)
}
cert, err := certutil.DecodeOriginCert(blocks)
if err != nil {
return nil, errors.Wrap(err, "Error decoding origin cert")
}
if cert.AccountID == "" {
return nil, errors.Errorf(`Origin certificate needs to be refreshed before creating new tunnels.\nDelete %s and run "cloudflared login" to obtain a new cert.`, originCertPath)
}
return cert, nil
}
@@ -0,0 +1,79 @@
package tunnel
import (
"testing"
"github.com/cloudflare/cloudflared/tunnelstore"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func Test_fmtConnections(t *testing.T) {
type args struct {
connections []tunnelstore.Connection
}
tests := []struct {
name string
args args
want string
}{
{
name: "empty",
args: args{
connections: []tunnelstore.Connection{},
},
want: "",
},
{
name: "trivial",
args: args{
connections: []tunnelstore.Connection{
{
ColoName: "DFW",
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
},
},
},
want: "1xDFW",
},
{
name: "many colos",
args: args{
connections: []tunnelstore.Connection{
{
ColoName: "YRV",
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
},
{
ColoName: "DFW",
ID: uuid.MustParse("c13c0b3b-0fbf-453c-8169-a1990fced6d0"),
},
{
ColoName: "ATL",
ID: uuid.MustParse("70c90639-e386-4e8d-9a4e-7f046d70e63f"),
},
{
ColoName: "DFW",
ID: uuid.MustParse("30ad6251-0305-4635-a670-d3994f474981"),
},
},
},
want: "1xATL, 2xDFW, 1xYRV",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := fmtConnections(tt.args.connections); got != tt.want {
t.Errorf("fmtConnections() = %v, want %v", got, tt.want)
}
})
}
}
func TestTunnelfilePath(t *testing.T) {
actual, err := tunnelFilePath("tunnel", "~/.cloudflared/cert.pem")
assert.NoError(t, err)
expected := "~/.cloudflared/tunnel.json"
assert.Equal(t, expected, actual)
}
+143 -34
View File
@@ -1,6 +1,8 @@
package updater
import (
"context"
"fmt"
"os"
"runtime"
"time"
@@ -8,12 +10,14 @@ import (
"golang.org/x/crypto/ssh/terminal"
"gopkg.in/urfave/cli.v2"
"github.com/cloudflare/cloudflared/log"
"github.com/cloudflare/cloudflared/logger"
"github.com/equinox-io/equinox"
"github.com/facebookgo/grace/gracenet"
"github.com/pkg/errors"
)
const (
DefaultCheckUpdateFreq = time.Hour * 24
appID = "app_idCzgxYerVD"
noUpdateInShellMessage = "cloudflared will not automatically update when run from the shell. To enable auto-updates, run cloudflared as a service: https://developers.cloudflare.com/argo-tunnel/reference/service/"
noUpdateOnWindowsMessage = "cloudflared will not automatically update on Windows systems."
@@ -27,9 +31,35 @@ dsCmJ/QZ6aw0w9qkkwEpne1Lmo6+0pGexZzFZOH6w5amShn+RXt7qkSid9iWlzGq
EKx0BZogHSor9Wy5VztdFaAaVbsJiCbO
-----END ECDSA PUBLIC KEY-----
`)
logger = log.CreateLogger()
)
// BinaryUpdated implements ExitCoder interface, the app will exit with status code 11
// https://pkg.go.dev/gopkg.in/urfave/cli.v2?tab=doc#ExitCoder
type statusSuccess struct {
newVersion string
}
func (u *statusSuccess) Error() string {
return fmt.Sprintf("cloudflared has been updated to version %s", u.newVersion)
}
func (u *statusSuccess) ExitCode() int {
return 11
}
// UpdateErr implements ExitCoder interface, the app will exit with status code 10
type statusErr struct {
err error
}
func (e *statusErr) Error() string {
return fmt.Sprintf("failed to update cloudflared: %v", e.err)
}
func (e *statusErr) ExitCode() int {
return 10
}
type UpdateOutcome struct {
Updated bool
Version string
@@ -63,56 +93,125 @@ func checkForUpdateAndApply() UpdateOutcome {
}
func Update(_ *cli.Context) error {
updateOutcome := loggedUpdate()
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
updateOutcome := loggedUpdate(logger)
if updateOutcome.Error != nil {
os.Exit(10)
return &statusErr{updateOutcome.Error}
}
if updateOutcome.noUpdate() {
logger.Infof("cloudflared is up to date (%s)", updateOutcome.Version)
return nil
}
return updateOutcome.Error
}
func Autoupdate(freq time.Duration, listeners *gracenet.Net, shutdownC chan struct{}) error {
tickC := time.Tick(freq)
for {
updateOutcome := loggedUpdate()
if updateOutcome.Updated {
os.Args = append(os.Args, "--is-autoupdated=true")
pid, err := listeners.StartProcess()
if err != nil {
logger.WithError(err).Error("Unable to restart server automatically")
return err
}
// stop old process after autoupdate. Otherwise we create a new process
// after each update
logger.Infof("PID of the new process is %d", pid)
return nil
}
select {
case <-tickC:
case <-shutdownC:
return nil
}
}
return &statusSuccess{newVersion: updateOutcome.Version}
}
// Checks for an update and applies it if one is available
func loggedUpdate() UpdateOutcome {
func loggedUpdate(logger logger.Service) UpdateOutcome {
updateOutcome := checkForUpdateAndApply()
if updateOutcome.Updated {
logger.Infof("cloudflared has been updated to version %s", updateOutcome.Version)
}
if updateOutcome.Error != nil {
logger.WithError(updateOutcome.Error).Error("update check failed")
logger.Errorf("update check failed: %s", updateOutcome.Error)
}
return updateOutcome
}
func IsAutoupdateEnabled(c *cli.Context) bool {
// AutoUpdater periodically checks for new version of cloudflared.
type AutoUpdater struct {
configurable *configurable
listeners *gracenet.Net
updateConfigChan chan *configurable
logger logger.Service
}
// AutoUpdaterConfigurable is the attributes of AutoUpdater that can be reconfigured during runtime
type configurable struct {
enabled bool
freq time.Duration
}
func NewAutoUpdater(freq time.Duration, listeners *gracenet.Net, logger logger.Service) *AutoUpdater {
updaterConfigurable := &configurable{
enabled: true,
freq: freq,
}
if freq == 0 {
updaterConfigurable.enabled = false
updaterConfigurable.freq = DefaultCheckUpdateFreq
}
return &AutoUpdater{
configurable: updaterConfigurable,
listeners: listeners,
updateConfigChan: make(chan *configurable),
logger: logger,
}
}
func (a *AutoUpdater) Run(ctx context.Context) error {
ticker := time.NewTicker(a.configurable.freq)
for {
if a.configurable.enabled {
updateOutcome := loggedUpdate(a.logger)
if updateOutcome.Updated {
os.Args = append(os.Args, "--is-autoupdated=true")
if IsSysV() {
// SysV doesn't have a mechanism to keep service alive, we have to restart the process
a.logger.Info("Restarting service managed by SysV...")
pid, err := a.listeners.StartProcess()
if err != nil {
a.logger.Errorf("Unable to restart server automatically: %s", err)
return &statusErr{err: err}
}
// stop old process after autoupdate. Otherwise we create a new process
// after each update
a.logger.Infof("PID of the new process is %d", pid)
}
return &statusSuccess{newVersion: updateOutcome.Version}
}
}
select {
case <-ctx.Done():
return ctx.Err()
case newConfigurable := <-a.updateConfigChan:
ticker.Stop()
a.configurable = newConfigurable
ticker = time.NewTicker(a.configurable.freq)
// Check if there is new version of cloudflared after receiving new AutoUpdaterConfigurable
case <-ticker.C:
}
}
}
// Update is the method to pass new AutoUpdaterConfigurable to a running AutoUpdater. It is safe to be called concurrently
func (a *AutoUpdater) Update(newFreq time.Duration) {
newConfigurable := &configurable{
enabled: true,
freq: newFreq,
}
// A ero duration means autoupdate is disabled
if newFreq == 0 {
newConfigurable.enabled = false
newConfigurable.freq = DefaultCheckUpdateFreq
}
a.updateConfigChan <- newConfigurable
}
func IsAutoupdateEnabled(c *cli.Context, l logger.Service) bool {
if !SupportAutoUpdate(l) {
return false
}
return !c.Bool("no-autoupdate") && c.Duration("autoupdate-freq") != 0
}
func SupportAutoUpdate(logger logger.Service) bool {
if runtime.GOOS == "windows" {
logger.Info(noUpdateOnWindowsMessage)
return false
@@ -122,10 +221,20 @@ func IsAutoupdateEnabled(c *cli.Context) bool {
logger.Info(noUpdateInShellMessage)
return false
}
return !c.Bool("no-autoupdate") && c.Duration("autoupdate-freq") != 0
return true
}
func isRunningFromTerminal() bool {
return terminal.IsTerminal(int(os.Stdout.Fd()))
}
func IsSysV() bool {
if runtime.GOOS != "linux" {
return false
}
if _, err := os.Stat("/run/systemd/system"); err == nil {
return false
}
return true
}
+28
View File
@@ -0,0 +1,28 @@
package updater
import (
"context"
"testing"
"github.com/cloudflare/cloudflared/logger"
"github.com/facebookgo/grace/gracenet"
"github.com/stretchr/testify/assert"
)
func TestDisabledAutoUpdater(t *testing.T) {
listeners := &gracenet.Net{}
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
autoupdater := NewAutoUpdater(0, listeners, logger)
ctx, cancel := context.WithCancel(context.Background())
errC := make(chan error)
go func() {
errC <- autoupdater.Run(ctx)
}()
assert.False(t, autoupdater.configurable.enabled)
assert.Equal(t, DefaultCheckUpdateFreq, autoupdater.configurable.freq)
cancel()
// Make sure that autoupdater terminates after canceling the context
assert.Equal(t, context.Canceled, <-errC)
}
+28 -7
View File
@@ -12,6 +12,8 @@ import (
"time"
"unsafe"
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
cli "gopkg.in/urfave/cli.v2"
"golang.org/x/sys/windows"
@@ -65,6 +67,12 @@ func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
// 2. get ERROR_FAILED_SERVICE_CONTROLLER_CONNECT
// This involves actually trying to start the service.
logger, err := logger.New()
if err != nil {
os.Exit(1)
return
}
isIntSess, err := svc.IsAnInteractiveSession()
if err != nil {
logger.Fatalf("failed to determine if we are running in an interactive session: %v", err)
@@ -97,9 +105,15 @@ type windowsService struct {
// called by the package code at the start of the service
func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeRequest, statusChan chan<- svc.Status) (ssec bool, errno uint32) {
logger, err := logger.New()
if err != nil {
os.Exit(1)
return
}
elog, err := eventlog.Open(windowsServiceName)
if err != nil {
logger.WithError(err).Errorf("Cannot open event log for %s", windowsServiceName)
logger.Errorf("Cannot open event log for %s with error: %s", windowsServiceName, err)
return
}
defer elog.Close()
@@ -135,14 +149,11 @@ func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeReques
case c := <-r:
switch c.Cmd {
case svc.Interrogate:
elog.Info(1, fmt.Sprintf("control request 1 #%d", c))
statusChan <- c.CurrentStatus
case svc.Stop:
elog.Info(1, "received stop control request")
close(s.graceShutdownC)
statusChan <- svc.Status{State: svc.StopPending}
case svc.Shutdown:
elog.Info(1, "received shutdown control request")
close(s.shutdownC)
statusChan <- svc.Status{State: svc.StopPending}
default:
@@ -163,6 +174,11 @@ func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeReques
}
func installWindowsService(c *cli.Context) error {
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
logger.Infof("Installing Argo Tunnel Windows service")
exepath, err := os.Executable()
if err != nil {
@@ -171,7 +187,7 @@ func installWindowsService(c *cli.Context) error {
}
m, err := mgr.Connect()
if err != nil {
logger.WithError(err).Errorf("Cannot establish a connection to the service control manager")
logger.Errorf("Cannot establish a connection to the service control manager: %s", err)
return err
}
defer m.Disconnect()
@@ -192,18 +208,23 @@ func installWindowsService(c *cli.Context) error {
err = eventlog.InstallAsEventCreate(windowsServiceName, eventlog.Error|eventlog.Warning|eventlog.Info)
if err != nil {
s.Delete()
logger.WithError(err).Errorf("Cannot install event logger")
logger.Errorf("Cannot install event logger: %s", err)
return fmt.Errorf("SetupEventLogSource() failed: %s", err)
}
err = configRecoveryOption(s.Handle)
if err != nil {
logger.WithError(err).Errorf("Cannot set service recovery actions")
logger.Errorf("Cannot set service recovery actions: %s", err)
logger.Infof("See %s to manually configure service recovery actions", windowsServiceUrl)
}
return nil
}
func uninstallWindowsService(c *cli.Context) error {
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
logger.Infof("Uninstalling Argo Tunnel Windows Service")
m, err := mgr.Connect()
if err != nil {
-148
View File
@@ -1,148 +0,0 @@
package sqlgateway
import (
"database/sql"
"encoding/json"
"fmt"
"math/rand"
"net/http"
"strings"
"time"
_ "github.com/lib/pq"
cli "gopkg.in/urfave/cli.v2"
"github.com/elgs/gosqljson"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
)
type Message struct {
Connection Connection `json:"connection"`
Command string `json:"command"`
Params []interface{} `json:"params"`
}
type Connection struct {
SSLMode string `json:"sslmode"`
Token string `json:"token"`
}
type Response struct {
Columns []string `json:"columns"`
Rows [][]string `json:"rows"`
Error string `json:"error"`
}
type Proxy struct {
Context *cli.Context
Router *mux.Router
Token string
User string
Password string
Driver string
Database string
Logger *logrus.Logger
}
func StartProxy(c *cli.Context, logger *logrus.Logger, password string) error {
proxy := NewProxy(c, logger, password)
logger.Infof("Starting SQL Gateway Proxy on port %s", strings.Split(c.String("url"), ":")[1])
err := http.ListenAndServe(":"+strings.Split(c.String("url"), ":")[1], proxy.Router)
if err != nil {
return err
}
return nil
}
func randID(n int, c *cli.Context) string {
charBytes := []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
b := make([]byte, n)
for i := range b {
b[i] = charBytes[rand.Intn(len(charBytes))]
}
return fmt.Sprintf("%s&%s", c.String("hostname"), b)
}
// db://user@dbname
func parseInfo(input string) (string, string, string) {
p1 := strings.Split(input, "://")
p2 := strings.Split(p1[1], "@")
return p1[0], p2[0], p2[1]
}
func NewProxy(c *cli.Context, logger *logrus.Logger, pass string) *Proxy {
rand.Seed(time.Now().UnixNano())
driver, user, dbname := parseInfo(c.String("address"))
proxy := Proxy{
Context: c,
Router: mux.NewRouter(),
Token: randID(64, c),
Logger: logger,
User: user,
Password: pass,
Database: dbname,
Driver: driver,
}
logger.Info(fmt.Sprintf(`
--------------------
SQL Gateway Proxy
Token: %s
--------------------
`, proxy.Token))
proxy.Router.HandleFunc("/", proxy.proxyRequest).Methods("POST")
return &proxy
}
func (proxy *Proxy) proxyRequest(rw http.ResponseWriter, req *http.Request) {
var message Message
response := Response{}
err := json.NewDecoder(req.Body).Decode(&message)
if err != nil {
proxy.Logger.Error(err)
http.Error(rw, fmt.Sprintf("400 - %s", err.Error()), http.StatusBadRequest)
return
}
if message.Connection.Token != proxy.Token {
proxy.Logger.Error("Invalid token")
http.Error(rw, "400 - Invalid token", http.StatusBadRequest)
return
}
connStr := fmt.Sprintf("user=%s password=%s dbname=%s sslmode=%s", proxy.User, proxy.Password, proxy.Database, message.Connection.SSLMode)
db, err := sql.Open(proxy.Driver, connStr)
defer db.Close()
if err != nil {
proxy.Logger.Error(err)
http.Error(rw, fmt.Sprintf("400 - %s", err.Error()), http.StatusBadRequest)
return
} else {
proxy.Logger.Info("Forwarding SQL: ", message.Command)
rw.Header().Set("Content-Type", "application/json")
headers, data, err := gosqljson.QueryDbToArray(db, "lower", message.Command, message.Params...)
if err != nil {
proxy.Logger.Error(err)
http.Error(rw, fmt.Sprintf("400 - %s", err.Error()), http.StatusBadRequest)
return
} else {
response = Response{headers, data, ""}
}
}
json.NewEncoder(rw).Encode(response)
}
+54
View File
@@ -0,0 +1,54 @@
package connection
import (
"context"
"crypto/tls"
"net"
"time"
"github.com/pkg/errors"
)
// DialEdge makes a TLS connection to a Cloudflare edge node
func DialEdge(
ctx context.Context,
timeout time.Duration,
tlsConfig *tls.Config,
edgeTCPAddr *net.TCPAddr,
) (net.Conn, error) {
// Inherit from parent context so we can cancel (Ctrl-C) while dialing
dialCtx, dialCancel := context.WithTimeout(ctx, timeout)
defer dialCancel()
dialer := net.Dialer{}
edgeConn, err := dialer.DialContext(dialCtx, "tcp", edgeTCPAddr.String())
if err != nil {
return nil, newDialError(err, "DialContext error")
}
tlsEdgeConn := tls.Client(edgeConn, tlsConfig)
tlsEdgeConn.SetDeadline(time.Now().Add(timeout))
if err = tlsEdgeConn.Handshake(); err != nil {
return nil, newDialError(err, "TLS handshake with edge error")
}
// clear the deadline on the conn; h2mux has its own timeouts
tlsEdgeConn.SetDeadline(time.Time{})
return tlsEdgeConn, nil
}
// DialError is an error returned from DialEdge
type DialError struct {
cause error
}
func newDialError(err error, message string) error {
return DialError{cause: errors.Wrap(err, message)}
}
func (e DialError) Error() string {
return e.cause.Error()
}
func (e DialError) Cause() error {
return e.cause
}
+118
View File
@@ -0,0 +1,118 @@
package connection
import (
"fmt"
"math"
"math/rand"
"net"
"reflect"
"testing/quick"
)
type mockAddrs struct {
// a set of synthetic SRV records
addrMap map[net.SRV][]*net.TCPAddr
// the total number of addresses, aggregated across addrMap.
// For the convenience of test code that would otherwise have to compute
// this by hand every time.
numAddrs int
}
func newMockAddrs(port uint16, numRegions uint8, numAddrsPerRegion uint8) mockAddrs {
addrMap := make(map[net.SRV][]*net.TCPAddr)
numAddrs := 0
for r := uint8(0); r < numRegions; r++ {
var (
srv = net.SRV{Target: fmt.Sprintf("test-region-%v.example.com", r), Port: port}
addrs []*net.TCPAddr
)
for a := uint8(0); a < numAddrsPerRegion; a++ {
addrs = append(addrs, &net.TCPAddr{
IP: net.ParseIP(fmt.Sprintf("10.0.%v.%v", r, a)),
Port: int(port),
})
}
addrMap[srv] = addrs
numAddrs += len(addrs)
}
return mockAddrs{addrMap: addrMap, numAddrs: numAddrs}
}
var _ quick.Generator = mockAddrs{}
func (mockAddrs) Generate(rand *rand.Rand, size int) reflect.Value {
port := uint16(rand.Intn(math.MaxUint16))
numRegions := uint8(1 + rand.Intn(10))
numAddrsPerRegion := uint8(1 + rand.Intn(32))
result := newMockAddrs(port, numRegions, numAddrsPerRegion)
return reflect.ValueOf(result)
}
// Returns a function compatible with net.LookupSRV that will return the SRV
// records from mockAddrs.
func mockNetLookupSRV(
m mockAddrs,
) func(service, proto, name string) (cname string, addrs []*net.SRV, err error) {
var addrs []*net.SRV
for k := range m.addrMap {
addr := k
addrs = append(addrs, &addr)
// We can't just do
// addrs = append(addrs, &k)
// `k` will be reused by subsequent loop iterations,
// so all the copies of `&k` would point to the same location.
}
return func(_, _, _ string) (string, []*net.SRV, error) {
return "", addrs, nil
}
}
// Returns a function compatible with net.LookupIP that translates the SRV records
// from mockAddrs into IP addresses, based on the TCP addresses in mockAddrs.
func mockNetLookupIP(
m mockAddrs,
) func(host string) ([]net.IP, error) {
return func(host string) ([]net.IP, error) {
for srv, tcpAddrs := range m.addrMap {
if srv.Target != host {
continue
}
result := make([]net.IP, len(tcpAddrs))
for i, tcpAddr := range tcpAddrs {
result[i] = tcpAddr.IP
}
return result, nil
}
return nil, fmt.Errorf("No IPs for %v", host)
}
}
type mockEdgeServiceDiscoverer struct {
}
func (mr *mockEdgeServiceDiscoverer) Addr() (*net.TCPAddr, error) {
return &net.TCPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: 63102,
}, nil
}
func (mr *mockEdgeServiceDiscoverer) AnyAddr() (*net.TCPAddr, error) {
return &net.TCPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: 63102,
}, nil
}
func (mr *mockEdgeServiceDiscoverer) ReplaceAddr(addr *net.TCPAddr) {}
func (mr *mockEdgeServiceDiscoverer) MarkAddrBad(addr *net.TCPAddr) {}
func (mr *mockEdgeServiceDiscoverer) AvailableAddrs() int {
return 1
}
func (mr *mockEdgeServiceDiscoverer) Refresh() error {
return nil
}
+49
View File
@@ -0,0 +1,49 @@
package connection
import (
"context"
"fmt"
"time"
rpc "zombiezen.com/go/capnproto2/rpc"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tunnelrpc"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
// NewRPCClient creates and returns a new RPC client, which will communicate
// using a stream on the given muxer
func NewRPCClient(
ctx context.Context,
muxer *h2mux.Muxer,
logger logger.Service,
openStreamTimeout time.Duration,
) (client tunnelpogs.TunnelServer_PogsClient, err error) {
openStreamCtx, openStreamCancel := context.WithTimeout(ctx, openStreamTimeout)
defer openStreamCancel()
stream, err := muxer.OpenRPCStream(openStreamCtx)
if err != nil {
return
}
if !isRPCStreamResponse(stream.Headers) {
stream.Close()
err = fmt.Errorf("rpc: bad response headers: %v", stream.Headers)
return
}
conn := rpc.NewConn(
tunnelrpc.NewTransportLogger(logger, rpc.StreamTransport(stream)),
tunnelrpc.ConnLog(logger),
)
client = tunnelpogs.TunnelServer_PogsClient{Client: conn.Bootstrap(ctx), Conn: conn}
return client, nil
}
func isRPCStreamResponse(headers []h2mux.Header) bool {
return len(headers) == 1 &&
headers[0].Name == ":status" &&
headers[0].Value == "200"
}
+145
View File
@@ -0,0 +1,145 @@
package dbconnect
import (
"context"
"encoding/json"
"fmt"
"net/url"
"strings"
"time"
"unicode"
"unicode/utf8"
)
// Client is an interface to talk to any database.
//
// Currently, the only implementation is SQLClient, but its structure
// should be designed to handle a MongoClient or RedisClient in the future.
type Client interface {
Ping(context.Context) error
Submit(context.Context, *Command) (interface{}, error)
}
// NewClient creates a database client based on its URL scheme.
func NewClient(ctx context.Context, originURL *url.URL) (Client, error) {
return NewSQLClient(ctx, originURL)
}
// Command is a standard, non-vendor format for submitting database commands.
//
// When determining the scope of this struct, refer to the following litmus test:
// Could this (roughly) conform to SQL, Document-based, and Key-value command formats?
type Command struct {
Statement string `json:"statement"`
Arguments Arguments `json:"arguments,omitempty"`
Mode string `json:"mode,omitempty"`
Isolation string `json:"isolation,omitempty"`
Timeout time.Duration `json:"timeout,omitempty"`
}
// Validate enforces the contract of Command: non empty statement (both in length and logic),
// lowercase mode and isolation, non-zero timeout, and valid Arguments.
func (cmd *Command) Validate() error {
if cmd.Statement == "" {
return fmt.Errorf("cannot provide an empty statement")
}
if strings.Map(func(char rune) rune {
if char == ';' || unicode.IsSpace(char) {
return -1
}
return char
}, cmd.Statement) == "" {
return fmt.Errorf("cannot provide a statement with no logic: '%s'", cmd.Statement)
}
cmd.Mode = strings.ToLower(cmd.Mode)
cmd.Isolation = strings.ToLower(cmd.Isolation)
if cmd.Timeout.Nanoseconds() <= 0 {
cmd.Timeout = 24 * time.Hour
}
return cmd.Arguments.Validate()
}
// UnmarshalJSON converts a byte representation of JSON into a Command, which is also validated.
func (cmd *Command) UnmarshalJSON(data []byte) error {
// Alias is required to avoid infinite recursion from the default UnmarshalJSON.
type Alias Command
alias := &struct {
*Alias
}{
Alias: (*Alias)(cmd),
}
err := json.Unmarshal(data, &alias)
if err == nil {
err = cmd.Validate()
}
return err
}
// Arguments is a wrapper for either map-based or array-based Command arguments.
//
// Each field is mutually-exclusive and some Client implementations may not
// support both fields (eg. MySQL does not accept named arguments).
type Arguments struct {
Named map[string]interface{}
Positional []interface{}
}
// Validate enforces the contract of Arguments: non nil, mutually exclusive, and no empty or reserved keys.
func (args *Arguments) Validate() error {
if args.Named == nil {
args.Named = map[string]interface{}{}
}
if args.Positional == nil {
args.Positional = []interface{}{}
}
if len(args.Named) > 0 && len(args.Positional) > 0 {
return fmt.Errorf("both named and positional arguments cannot be specified: %+v and %+v", args.Named, args.Positional)
}
for key := range args.Named {
if key == "" {
return fmt.Errorf("named arguments cannot contain an empty key: %+v", args.Named)
}
if !utf8.ValidString(key) {
return fmt.Errorf("named argument does not conform to UTF-8 encoding: %s", key)
}
if strings.HasPrefix(key, "_") {
return fmt.Errorf("named argument cannot start with a reserved keyword '_': %s", key)
}
if unicode.IsNumber([]rune(key)[0]) {
return fmt.Errorf("named argument cannot start with a number: %s", key)
}
}
return nil
}
// UnmarshalJSON converts a byte representation of JSON into Arguments, which is also validated.
func (args *Arguments) UnmarshalJSON(data []byte) error {
var obj interface{}
err := json.Unmarshal(data, &obj)
if err != nil {
return err
}
named, ok := obj.(map[string]interface{})
if ok {
args.Named = named
} else {
positional, ok := obj.([]interface{})
if ok {
args.Positional = positional
} else {
return fmt.Errorf("arguments must either be an object {\"0\":\"val\"} or an array [\"val\"]: %s", string(data))
}
}
return args.Validate()
}
+183
View File
@@ -0,0 +1,183 @@
package dbconnect
import (
"encoding/json"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestCommandValidateEmpty(t *testing.T) {
stmts := []string{
"",
";",
" \n\t",
";\n;\t;",
}
for _, stmt := range stmts {
cmd := Command{Statement: stmt}
assert.Error(t, cmd.Validate(), stmt)
}
}
func TestCommandValidateMode(t *testing.T) {
modes := []string{
"",
"query",
"ExEc",
"PREPARE",
}
for _, mode := range modes {
cmd := Command{Statement: "Ok", Mode: mode}
assert.NoError(t, cmd.Validate(), mode)
assert.Equal(t, strings.ToLower(mode), cmd.Mode)
}
}
func TestCommandValidateIsolation(t *testing.T) {
isos := []string{
"",
"default",
"read_committed",
"SNAPshot",
}
for _, iso := range isos {
cmd := Command{Statement: "Ok", Isolation: iso}
assert.NoError(t, cmd.Validate(), iso)
assert.Equal(t, strings.ToLower(iso), cmd.Isolation)
}
}
func TestCommandValidateTimeout(t *testing.T) {
cmd := Command{Statement: "Ok", Timeout: 0}
assert.NoError(t, cmd.Validate())
assert.NotZero(t, cmd.Timeout)
cmd = Command{Statement: "Ok", Timeout: 1 * time.Second}
assert.NoError(t, cmd.Validate())
assert.Equal(t, 1*time.Second, cmd.Timeout)
}
func TestCommandValidateArguments(t *testing.T) {
cmd := Command{Statement: "Ok", Arguments: Arguments{
Named: map[string]interface{}{"key": "val"},
Positional: []interface{}{"val"},
}}
assert.Error(t, cmd.Validate())
}
func TestCommandUnmarshalJSON(t *testing.T) {
strs := []string{
"{\"statement\":\"Ok\"}",
"{\"statement\":\"Ok\",\"arguments\":[0, 3.14, \"apple\"],\"mode\":\"query\"}",
"{\"statement\":\"Ok\",\"isolation\":\"read_uncommitted\",\"timeout\":1000}",
}
for _, str := range strs {
var cmd Command
assert.NoError(t, json.Unmarshal([]byte(str), &cmd), str)
}
strs = []string{
"",
"\"",
"{}",
"{\"argument\":{\"key\":\"val\"}}",
"{\"statement\":[\"Ok\"]}",
}
for _, str := range strs {
var cmd Command
assert.Error(t, json.Unmarshal([]byte(str), &cmd), str)
}
}
func TestArgumentsValidateNotNil(t *testing.T) {
args := Arguments{}
assert.NoError(t, args.Validate())
assert.NotNil(t, args.Named)
assert.NotNil(t, args.Positional)
}
func TestArgumentsValidateMutuallyExclusive(t *testing.T) {
args := []Arguments{
Arguments{},
Arguments{Named: map[string]interface{}{"key": "val"}},
Arguments{Positional: []interface{}{"val"}},
}
for _, arg := range args {
assert.NoError(t, arg.Validate())
assert.False(t, len(arg.Named) > 0 && len(arg.Positional) > 0)
}
args = []Arguments{
Arguments{
Named: map[string]interface{}{"key": "val"},
Positional: []interface{}{"val"},
},
}
for _, arg := range args {
assert.Error(t, arg.Validate())
assert.True(t, len(arg.Named) > 0 && len(arg.Positional) > 0)
}
}
func TestArgumentsValidateKeys(t *testing.T) {
keys := []string{
"",
"_",
"_key",
"1",
"1key",
"\xf0\x28\x8c\xbc", // non-utf8
}
for _, key := range keys {
args := Arguments{Named: map[string]interface{}{key: "val"}}
assert.Error(t, args.Validate(), key)
}
}
func TestArgumentsUnmarshalJSON(t *testing.T) {
strs := []string{
"{}",
"{\"key\":\"val\"}",
"{\"key\":[1, 3.14, {\"key\":\"val\"}]}",
"[]",
"[\"key\",\"val\"]",
"[{}]",
}
for _, str := range strs {
var args Arguments
assert.NoError(t, json.Unmarshal([]byte(str), &args), str)
}
strs = []string{
"",
"\"",
"1",
"\"key\"",
"{\"key\",\"val\"}",
}
for _, str := range strs {
var args Arguments
assert.Error(t, json.Unmarshal([]byte(str), &args), str)
}
}
+165
View File
@@ -0,0 +1,165 @@
package dbconnect
import (
"context"
"log"
"net"
"strconv"
"gopkg.in/urfave/cli.v2"
"gopkg.in/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
@@ -0,0 +1,27 @@
package dbconnect
import (
"testing"
"github.com/stretchr/testify/assert"
"gopkg.in/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))
}
}
+278
View File
@@ -0,0 +1,278 @@
package dbconnect
import (
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"time"
"github.com/cloudflare/cloudflared/hello"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/validation"
"github.com/gorilla/mux"
"github.com/pkg/errors"
)
// Proxy is an HTTP server that proxies requests to a Client.
type Proxy struct {
client Client
accessValidator *validation.Access
logger logger.Service
}
// NewInsecureProxy creates a Proxy that talks to a Client at an origin.
//
// In insecure mode, the Proxy will allow all Command requests.
func NewInsecureProxy(ctx context.Context, origin string) (*Proxy, error) {
originURL, err := url.Parse(origin)
if err != nil {
return nil, errors.Wrap(err, "must provide a valid database url")
}
client, err := NewClient(ctx, originURL)
if err != nil {
return nil, err
}
err = client.Ping(ctx)
if err != nil {
return nil, errors.Wrap(err, "could not connect to the database")
}
logger, err := logger.New()
if err != nil {
return nil, errors.Wrap(err, "error setting up logger")
}
return &Proxy{client, nil, logger}, nil
}
// NewSecureProxy creates a Proxy that talks to a Client at an origin.
//
// In secure mode, the Proxy will reject any Command requests that are
// not authenticated by Cloudflare Access with a valid JWT.
func NewSecureProxy(ctx context.Context, origin, authDomain, applicationAUD string) (*Proxy, error) {
proxy, err := NewInsecureProxy(ctx, origin)
if err != nil {
return nil, err
}
validator, err := validation.NewAccessValidator(ctx, authDomain, authDomain, applicationAUD)
if err != nil {
return nil, err
}
proxy.accessValidator = validator
return proxy, err
}
// IsInsecure gets whether the Proxy will accept a Command from any source.
func (proxy *Proxy) IsInsecure() bool {
return proxy.accessValidator == nil
}
// IsAllowed checks whether a http.Request is allowed to receive data.
//
// By default, requests must pass through Cloudflare Access for authentication.
// If the proxy is explcitly set to insecure mode, all requests will be allowed.
func (proxy *Proxy) IsAllowed(r *http.Request, verbose ...bool) bool {
if proxy.IsInsecure() {
return true
}
// Access and Tunnel should prevent bad JWTs from even reaching the origin,
// but validate tokens anyway as an abundance of caution.
err := proxy.accessValidator.ValidateRequest(r.Context(), r)
if err == nil {
return true
}
// Warn administrators that invalid JWTs are being rejected. This is indicative
// of either a misconfiguration of the CLI or a massive failure of upstream systems.
if len(verbose) > 0 {
cfRay := proxy.getRayHeader(r)
proxy.logger.Infof("dbproxy: Failed JWT authentication: cf-ray: %s %s", cfRay, err)
}
return false
}
// Start the Proxy at a given address and notify the listener channel when the server is online.
func (proxy *Proxy) Start(ctx context.Context, addr string, listenerC chan<- net.Listener) error {
// STOR-611: use a seperate listener and consider web socket support.
httpListener, err := hello.CreateTLSListener(addr)
if err != nil {
return errors.Wrapf(err, "could not create listener at %s", addr)
}
errC := make(chan error)
defer close(errC)
// Starts the HTTP server and begins to serve requests.
go func() {
errC <- proxy.httpListen(ctx, httpListener)
}()
// Continually ping the server until it comes online or 10 attempts fail.
go func() {
var err error
for i := 0; i < 10; i++ {
_, err = http.Get("http://" + httpListener.Addr().String())
// Once no error was detected, notify the listener channel and return.
if err == nil {
listenerC <- httpListener
return
}
// Backoff between requests to ping the server.
<-time.After(1 * time.Second)
}
errC <- errors.Wrap(err, "took too long for the http server to start")
}()
return <-errC
}
// httpListen starts the httpServer and blocks until the context closes.
func (proxy *Proxy) httpListen(ctx context.Context, listener net.Listener) error {
httpServer := &http.Server{
Addr: listener.Addr().String(),
Handler: proxy.httpRouter(),
ReadTimeout: 10 * time.Second,
WriteTimeout: 60 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
<-ctx.Done()
httpServer.Close()
listener.Close()
}()
return httpServer.Serve(listener)
}
// httpRouter creates a mux.Router for the Proxy.
func (proxy *Proxy) httpRouter() *mux.Router {
router := mux.NewRouter()
router.HandleFunc("/ping", proxy.httpPing()).Methods("GET", "HEAD")
router.HandleFunc("/submit", proxy.httpSubmit()).Methods("POST")
return router
}
// httpPing tests the connection to the database.
//
// By default, this endpoint is unauthenticated to allow for health checks.
// To enable authentication, Cloudflare Access must be enabled on this route.
func (proxy *Proxy) httpPing() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
err := proxy.client.Ping(ctx)
if err == nil {
proxy.httpRespond(w, r, http.StatusOK, "")
} else {
proxy.httpRespondErr(w, r, http.StatusInternalServerError, err)
}
}
}
// httpSubmit sends a command to the database and returns its response.
//
// By default, this endpoint will reject requests that do not pass through Cloudflare Access.
// To disable authentication, the --insecure flag must be specified in the command line.
func (proxy *Proxy) httpSubmit() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !proxy.IsAllowed(r, true) {
proxy.httpRespondErr(w, r, http.StatusForbidden, fmt.Errorf(""))
return
}
var cmd Command
err := json.NewDecoder(r.Body).Decode(&cmd)
if err != nil {
proxy.httpRespondErr(w, r, http.StatusBadRequest, err)
return
}
ctx := r.Context()
data, err := proxy.client.Submit(ctx, &cmd)
if err != nil {
proxy.httpRespondErr(w, r, http.StatusUnprocessableEntity, err)
return
}
w.Header().Set("Content-type", "application/json")
err = json.NewEncoder(w).Encode(data)
if err != nil {
proxy.httpRespondErr(w, r, http.StatusInternalServerError, err)
}
}
}
// httpRespond writes a status code and string response to the response writer.
func (proxy *Proxy) httpRespond(w http.ResponseWriter, r *http.Request, status int, message string) {
w.WriteHeader(status)
// Only expose the message detail of the reponse if the request is not HEAD
// and the user is authenticated. For example, this prevents an unauthenticated
// failed health check from accidentally leaking sensitive information about the Client.
if r.Method != http.MethodHead && proxy.IsAllowed(r) {
if message == "" {
message = http.StatusText(status)
}
fmt.Fprint(w, message)
}
}
// httpRespondErr is similar to httpRespond, except it formats errors to be more friendly.
func (proxy *Proxy) httpRespondErr(w http.ResponseWriter, r *http.Request, defaultStatus int, err error) {
status, err := httpError(defaultStatus, err)
proxy.httpRespond(w, r, status, err.Error())
if len(err.Error()) > 0 {
cfRay := proxy.getRayHeader(r)
proxy.logger.Infof("dbproxy: Database proxy error: cf-ray: %s %s", cfRay, err)
}
}
// getRayHeader returns the request's Cf-ray header.
func (proxy *Proxy) getRayHeader(r *http.Request) string {
return r.Header.Get("Cf-ray")
}
// httpError extracts common errors and returns an status code and friendly error.
func httpError(defaultStatus int, err error) (int, error) {
if err == nil {
return http.StatusNotImplemented, fmt.Errorf("error expected but found none")
}
if err == io.EOF {
return http.StatusBadRequest, fmt.Errorf("request body cannot be empty")
}
if err == context.DeadlineExceeded {
return http.StatusRequestTimeout, err
}
_, ok := err.(net.Error)
if ok {
return http.StatusRequestTimeout, err
}
if err == context.Canceled {
// Does not exist in Golang, but would be: http.StatusClientClosedWithoutResponse
return 444, err
}
return defaultStatus, err
}
+238
View File
@@ -0,0 +1,238 @@
package dbconnect
import (
"context"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
)
func TestNewInsecureProxy(t *testing.T) {
origins := []string{
"",
":/",
"http://localhost",
"tcp://localhost:9000?debug=true",
"mongodb://127.0.0.1",
}
for _, origin := range origins {
proxy, err := NewInsecureProxy(context.Background(), origin)
assert.Error(t, err)
assert.Empty(t, proxy)
}
}
func TestProxyIsAllowed(t *testing.T) {
proxy := helperNewProxy(t)
req := httptest.NewRequest("GET", "https://1.1.1.1/ping", nil)
assert.True(t, proxy.IsAllowed(req))
proxy = helperNewProxy(t, true)
req.Header.Set("Cf-access-jwt-assertion", "xxx")
assert.False(t, proxy.IsAllowed(req))
}
func TestProxyStart(t *testing.T) {
proxy := helperNewProxy(t)
ctx := context.Background()
listenerC := make(chan net.Listener)
err := proxy.Start(ctx, "1.1.1.1:", listenerC)
assert.Error(t, err)
err = proxy.Start(ctx, "127.0.0.1:-1", listenerC)
assert.Error(t, err)
ctx, cancel := context.WithTimeout(ctx, 0)
defer cancel()
err = proxy.Start(ctx, "127.0.0.1:", listenerC)
assert.IsType(t, http.ErrServerClosed, err)
}
func TestProxyHTTPRouter(t *testing.T) {
proxy := helperNewProxy(t)
router := proxy.httpRouter()
tests := []struct {
path string
method string
valid bool
}{
{"", "GET", false},
{"/", "GET", false},
{"/ping", "GET", true},
{"/ping", "HEAD", true},
{"/ping", "POST", false},
{"/submit", "POST", true},
{"/submit", "GET", false},
{"/submit/extra", "POST", false},
}
for _, test := range tests {
match := &mux.RouteMatch{}
ok := router.Match(httptest.NewRequest(test.method, "https://1.1.1.1"+test.path, nil), match)
assert.True(t, ok == test.valid, test.path)
}
}
func TestProxyHTTPPing(t *testing.T) {
proxy := helperNewProxy(t)
server := httptest.NewServer(proxy.httpPing())
defer server.Close()
client := server.Client()
res, err := client.Get(server.URL)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
assert.Equal(t, int64(2), res.ContentLength)
res, err = client.Head(server.URL)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
assert.Equal(t, int64(-1), res.ContentLength)
}
func TestProxyHTTPSubmit(t *testing.T) {
proxy := helperNewProxy(t)
server := httptest.NewServer(proxy.httpSubmit())
defer server.Close()
client := server.Client()
tests := []struct {
input string
status int
output string
}{
{"", http.StatusBadRequest, "request body cannot be empty"},
{"{}", http.StatusBadRequest, "cannot provide an empty statement"},
{"{\"statement\":\"Ok\"}", http.StatusUnprocessableEntity, "cannot provide invalid sql mode: ''"},
{"{\"statement\":\"Ok\",\"mode\":\"query\"}", http.StatusUnprocessableEntity, "near \"Ok\": syntax error"},
{"{\"statement\":\"CREATE TABLE t (a INT);\",\"mode\":\"exec\"}", http.StatusOK, "{\"last_insert_id\":0,\"rows_affected\":0}\n"},
}
for _, test := range tests {
res, err := client.Post(server.URL, "application/json", strings.NewReader(test.input))
assert.NoError(t, err)
assert.Equal(t, test.status, res.StatusCode)
if res.StatusCode > http.StatusOK {
assert.Equal(t, "text/plain; charset=utf-8", res.Header.Get("Content-type"))
} else {
assert.Equal(t, "application/json", res.Header.Get("Content-type"))
}
data, err := ioutil.ReadAll(res.Body)
defer res.Body.Close()
str := string(data)
assert.NoError(t, err)
assert.Equal(t, test.output, str)
}
}
func TestProxyHTTPSubmitForbidden(t *testing.T) {
proxy := helperNewProxy(t, true)
server := httptest.NewServer(proxy.httpSubmit())
defer server.Close()
client := server.Client()
res, err := client.Get(server.URL)
assert.NoError(t, err)
assert.Equal(t, http.StatusForbidden, res.StatusCode)
assert.Zero(t, res.ContentLength)
}
func TestProxyHTTPRespond(t *testing.T) {
proxy := helperNewProxy(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
proxy.httpRespond(w, r, http.StatusAccepted, "Hello")
}))
defer server.Close()
client := server.Client()
res, err := client.Get(server.URL)
assert.NoError(t, err)
assert.Equal(t, http.StatusAccepted, res.StatusCode)
assert.Equal(t, int64(5), res.ContentLength)
data, err := ioutil.ReadAll(res.Body)
defer res.Body.Close()
assert.Equal(t, []byte("Hello"), data)
}
func TestProxyHTTPRespondForbidden(t *testing.T) {
proxy := helperNewProxy(t, true)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
proxy.httpRespond(w, r, http.StatusAccepted, "Hello")
}))
defer server.Close()
client := server.Client()
res, err := client.Get(server.URL)
assert.NoError(t, err)
assert.Equal(t, http.StatusAccepted, res.StatusCode)
assert.Equal(t, int64(0), res.ContentLength)
}
func TestHTTPError(t *testing.T) {
_, errTimeout := net.DialTimeout("tcp", "127.0.0.1", 0)
assert.Error(t, errTimeout)
tests := []struct {
input error
status int
output error
}{
{nil, http.StatusNotImplemented, fmt.Errorf("error expected but found none")},
{io.EOF, http.StatusBadRequest, fmt.Errorf("request body cannot be empty")},
{context.DeadlineExceeded, http.StatusRequestTimeout, nil},
{context.Canceled, 444, nil},
{errTimeout, http.StatusRequestTimeout, nil},
{fmt.Errorf(""), http.StatusInternalServerError, nil},
}
for _, test := range tests {
status, err := httpError(http.StatusInternalServerError, test.input)
assert.Error(t, err)
assert.Equal(t, test.status, status)
if test.output == nil {
test.output = test.input
}
assert.Equal(t, test.output, err)
}
}
func helperNewProxy(t *testing.T, secure ...bool) *Proxy {
t.Helper()
proxy, err := NewSecureProxy(context.Background(), "file::memory:?cache=shared", "test.cloudflareaccess.com", "")
assert.NoError(t, err)
assert.NotNil(t, proxy)
if len(secure) == 0 {
proxy.accessValidator = nil // Mark as insecure
}
return proxy
}
+318
View File
@@ -0,0 +1,318 @@
package dbconnect
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"net/url"
"reflect"
"strings"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"github.com/xo/dburl"
// SQL drivers self-register with the database/sql package.
// https://github.com/golang/go/wiki/SQLDrivers
_ "github.com/denisenkom/go-mssqldb"
_ "github.com/go-sql-driver/mysql"
_ "github.com/mattn/go-sqlite3"
"github.com/kshvakov/clickhouse"
"github.com/lib/pq"
)
// SQLClient is a Client that talks to a SQL database.
type SQLClient struct {
Dialect string
driver *sqlx.DB
}
// NewSQLClient creates a SQL client based on its URL scheme.
func NewSQLClient(ctx context.Context, originURL *url.URL) (Client, error) {
res, err := dburl.Parse(originURL.String())
if err != nil {
helpText := fmt.Sprintf("supported drivers: %+q, see documentation for more details: %s", sql.Drivers(), "https://godoc.org/github.com/xo/dburl")
return nil, fmt.Errorf("could not parse sql database url '%s': %s\n%s", originURL, err.Error(), helpText)
}
// Establishes the driver, but does not test the connection.
driver, err := sqlx.Open(res.Driver, res.DSN)
if err != nil {
return nil, fmt.Errorf("could not open sql driver %s: %s\n%s", res.Driver, err.Error(), res.DSN)
}
// Closes the driver, will occur when the context finishes.
go func() {
<-ctx.Done()
driver.Close()
}()
return &SQLClient{driver.DriverName(), driver}, nil
}
// Ping verifies a connection to the database is still alive.
func (client *SQLClient) Ping(ctx context.Context) error {
return client.driver.PingContext(ctx)
}
// Submit queries or executes a command to the SQL database.
func (client *SQLClient) Submit(ctx context.Context, cmd *Command) (interface{}, error) {
txx, err := cmd.ValidateSQL(client.Dialect)
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(ctx, cmd.Timeout)
defer cancel()
var res interface{}
// Get the next available sql.Conn and submit the Command.
err = sqlConn(ctx, client.driver, txx, func(conn *sql.Conn) error {
stmt := cmd.Statement
args := cmd.Arguments.Positional
if cmd.Mode == "query" {
res, err = sqlQuery(ctx, conn, stmt, args)
} else {
res, err = sqlExec(ctx, conn, stmt, args)
}
return err
})
return res, err
}
// ValidateSQL extends the contract of Command for SQL dialects:
// mode is conformed, arguments are []sql.NamedArg, and isolation is a sql.IsolationLevel.
//
// When the command should not be wrapped in a transaction, *sql.TxOptions and error will both be nil.
func (cmd *Command) ValidateSQL(dialect string) (*sql.TxOptions, error) {
err := cmd.Validate()
if err != nil {
return nil, err
}
mode, err := sqlMode(cmd.Mode)
if err != nil {
return nil, err
}
// Mutates Arguments to only use positional arguments with the type sql.NamedArg.
// This is a required by the sql.Driver before submitting arguments.
cmd.Arguments.sql(dialect)
iso, err := sqlIsolation(cmd.Isolation)
if err != nil {
return nil, err
}
// When isolation is out-of-range, this is indicative that no
// transaction should be executed and sql.TxOptions should be nil.
if iso < sql.LevelDefault {
return nil, nil
}
// In query mode, execute the transaction in read-only, unless it's Microsoft SQL
// which does not support that type of transaction.
readOnly := mode == "query" && dialect != "mssql"
return &sql.TxOptions{Isolation: iso, ReadOnly: readOnly}, nil
}
// sqlConn gets the next available sql.Conn in the connection pool and runs a function to use it.
//
// If the transaction options are nil, run the useIt function outside a transaction.
// This is potentially an unsafe operation if the command does not clean up its state.
func sqlConn(ctx context.Context, driver *sqlx.DB, txx *sql.TxOptions, useIt func(*sql.Conn) error) error {
conn, err := driver.Conn(ctx)
if err != nil {
return err
}
defer conn.Close()
// If transaction options are specified, begin and defer a rollback to catch errors.
var tx *sql.Tx
if txx != nil {
tx, err = conn.BeginTx(ctx, txx)
if err != nil {
return err
}
defer tx.Rollback()
}
err = useIt(conn)
// Check if useIt was successful and a transaction exists before committing.
if err == nil && tx != nil {
err = tx.Commit()
}
return err
}
// sqlQuery queries rows on a sql.Conn and returns an array of result objects.
func sqlQuery(ctx context.Context, conn *sql.Conn, stmt string, args []interface{}) ([]map[string]interface{}, error) {
rows, err := conn.QueryContext(ctx, stmt, args...)
if err == nil {
return sqlRows(rows)
}
return nil, err
}
// sqlExec executes a command on a sql.Conn and returns the result of the operation.
func sqlExec(ctx context.Context, conn *sql.Conn, stmt string, args []interface{}) (sqlResult, error) {
exec, err := conn.ExecContext(ctx, stmt, args...)
if err == nil {
return sqlResultFrom(exec), nil
}
return sqlResult{}, err
}
// sql mutates Arguments to contain a positional []sql.NamedArg.
//
// The actual return type is []interface{} due to the native Golang
// function signatures for sql.Exec and sql.Query being generic.
func (args *Arguments) sql(dialect string) {
result := args.Positional
for i, val := range result {
result[i] = sqlArg("", val, dialect)
}
for key, val := range args.Named {
result = append(result, sqlArg(key, val, dialect))
}
args.Positional = result
args.Named = map[string]interface{}{}
}
// sqlArg creates a sql.NamedArg from a key-value pair and an optional dialect.
//
// Certain dialects will need to wrap objects, such as arrays, to conform its driver requirements.
func sqlArg(key, val interface{}, dialect string) sql.NamedArg {
switch reflect.ValueOf(val).Kind() {
// PostgreSQL and Clickhouse require arrays to be wrapped before
// being inserted into the driver interface.
case reflect.Slice, reflect.Array:
switch dialect {
case "postgres":
val = pq.Array(val)
case "clickhouse":
val = clickhouse.Array(val)
}
}
return sql.Named(fmt.Sprint(key), val)
}
// sqlIsolation tries to match a string to a sql.IsolationLevel.
func sqlIsolation(str string) (sql.IsolationLevel, error) {
if str == "none" {
return sql.IsolationLevel(-1), nil
}
for iso := sql.LevelDefault; ; iso++ {
if iso > sql.LevelLinearizable {
return -1, fmt.Errorf("cannot provide an invalid sql isolation level: '%s'", str)
}
if str == "" || strings.EqualFold(iso.String(), strings.ReplaceAll(str, "_", " ")) {
return iso, nil
}
}
}
// sqlMode tries to match a string to a command mode: 'query' or 'exec' for now.
func sqlMode(str string) (string, error) {
switch str {
case "query", "exec":
return str, nil
default:
return "", fmt.Errorf("cannot provide invalid sql mode: '%s'", str)
}
}
// sqlRows scans through a SQL result set and returns an array of objects.
func sqlRows(rows *sql.Rows) ([]map[string]interface{}, error) {
columns, err := rows.Columns()
if err != nil {
return nil, errors.Wrap(err, "could not extract columns from result")
}
defer rows.Close()
types, err := rows.ColumnTypes()
if err != nil {
// Some drivers do not support type extraction, so fail silently and continue.
types = make([]*sql.ColumnType, len(columns))
}
values := make([]interface{}, len(columns))
pointers := make([]interface{}, len(columns))
var results []map[string]interface{}
for rows.Next() {
for i := range columns {
pointers[i] = &values[i]
}
rows.Scan(pointers...)
// Convert a row, an array of values, into an object where
// each key is the name of its respective column.
entry := make(map[string]interface{})
for i, col := range columns {
entry[col] = sqlValue(values[i], types[i])
}
results = append(results, entry)
}
return results, nil
}
// sqlValue handles special cases where sql.Rows does not return a "human-readable" object.
func sqlValue(val interface{}, col *sql.ColumnType) interface{} {
bytes, ok := val.([]byte)
if ok {
// Opportunistically check for embeded JSON and convert it to a first-class object.
var embeded interface{}
if json.Unmarshal(bytes, &embeded) == nil {
return embeded
}
// STOR-604: investigate a way to coerce PostgreSQL arrays '{a, b, ...}' into JSON.
// Although easy with strings, it becomes more difficult with special types like INET[].
return string(bytes)
}
return val
}
// sqlResult is a thin wrapper around sql.Result.
type sqlResult struct {
LastInsertId int64 `json:"last_insert_id"`
RowsAffected int64 `json:"rows_affected"`
}
// sqlResultFrom converts sql.Result into a JSON-marshable sqlResult.
func sqlResultFrom(res sql.Result) sqlResult {
insertID, errID := res.LastInsertId()
rowsAffected, errRows := res.RowsAffected()
// If an error occurs when extracting the result, it is because the
// driver does not support that specific field. Instead of passing this
// to the user, omit the field in the response.
if errID != nil {
insertID = -1
}
if errRows != nil {
rowsAffected = -1
}
return sqlResult{insertID, rowsAffected}
}
+336
View File
@@ -0,0 +1,336 @@
package dbconnect
import (
"context"
"database/sql"
"fmt"
"net/url"
"strings"
"testing"
"time"
"github.com/kshvakov/clickhouse"
"github.com/lib/pq"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
)
func TestNewSQLClient(t *testing.T) {
originURLs := []string{
"postgres://localhost",
"cockroachdb://localhost:1337",
"postgresql://user:pass@127.0.0.1",
"mysql://localhost",
"clickhouse://127.0.0.1:9000/?debug",
"sqlite3::memory:",
"file:test.db?cache=shared",
}
for _, originURL := range originURLs {
origin, _ := url.Parse(originURL)
_, err := NewSQLClient(context.Background(), origin)
assert.NoError(t, err, originURL)
}
originURLs = []string{
"",
"/",
"http://localhost",
"coolthing://user:pass@127.0.0.1",
}
for _, originURL := range originURLs {
origin, _ := url.Parse(originURL)
_, err := NewSQLClient(context.Background(), origin)
assert.Error(t, err, originURL)
}
}
func TestArgumentsSQL(t *testing.T) {
args := []Arguments{
Arguments{
Positional: []interface{}{
"val", 10, 3.14,
},
},
Arguments{
Named: map[string]interface{}{
"key": time.Unix(0, 0),
},
},
}
var nameType sql.NamedArg
for _, arg := range args {
arg.sql("")
for _, named := range arg.Positional {
assert.IsType(t, nameType, named)
}
}
}
func TestSQLArg(t *testing.T) {
tests := []struct {
key interface{}
val interface{}
dialect string
arg sql.NamedArg
}{
{"key", "val", "mssql", sql.Named("key", "val")},
{0, 1, "sqlite3", sql.Named("0", 1)},
{1, []string{"a", "b", "c"}, "postgres", sql.Named("1", pq.Array([]string{"a", "b", "c"}))},
{"in", []uint{0, 1}, "clickhouse", sql.Named("in", clickhouse.Array([]uint{0, 1}))},
{"", time.Unix(0, 0), "", sql.Named("", time.Unix(0, 0))},
}
for _, test := range tests {
arg := sqlArg(test.key, test.val, test.dialect)
assert.Equal(t, test.arg, arg, test.key)
}
}
func TestSQLIsolation(t *testing.T) {
tests := []struct {
str string
iso sql.IsolationLevel
}{
{"", sql.LevelDefault},
{"DEFAULT", sql.LevelDefault},
{"read_UNcommitted", sql.LevelReadUncommitted},
{"serializable", sql.LevelSerializable},
{"none", sql.IsolationLevel(-1)},
{"SNAP shot", -2},
{"blah", -2},
}
for _, test := range tests {
iso, err := sqlIsolation(test.str)
if test.iso < -1 {
assert.Error(t, err, test.str)
} else {
assert.NoError(t, err)
assert.Equal(t, test.iso, iso, test.str)
}
}
}
func TestSQLMode(t *testing.T) {
modes := []string{
"query",
"exec",
}
for _, mode := range modes {
actual, err := sqlMode(mode)
assert.NoError(t, err)
assert.Equal(t, strings.ToLower(mode), actual, mode)
}
modes = []string{
"",
"blah",
}
for _, mode := range modes {
_, err := sqlMode(mode)
assert.Error(t, err)
}
}
func helperRows(mockRows *sqlmock.Rows) *sql.Rows {
db, mock, _ := sqlmock.New()
mock.ExpectQuery("SELECT").WillReturnRows(mockRows)
rows, _ := db.Query("SELECT")
return rows
}
func TestSQLRows(t *testing.T) {
actual, err := sqlRows(helperRows(sqlmock.NewRows(
[]string{"name", "age", "dept"}).
AddRow("alice", 19, "prod")))
expected := []map[string]interface{}{map[string]interface{}{
"name": "alice",
"age": int64(19),
"dept": "prod"}}
assert.NoError(t, err)
assert.ElementsMatch(t, expected, actual)
}
func TestSQLValue(t *testing.T) {
tests := []struct {
input interface{}
output interface{}
}{
{"hello", "hello"},
{1, 1},
{false, false},
{[]byte("random"), "random"},
{[]byte("{\"json\":true}"), map[string]interface{}{"json": true}},
{[]byte("[]"), []interface{}{}},
}
for _, test := range tests {
assert.Equal(t, test.output, sqlValue(test.input, nil), test.input)
}
}
func TestSQLResultFrom(t *testing.T) {
res := sqlResultFrom(sqlmock.NewResult(1, 2))
assert.Equal(t, sqlResult{1, 2}, res)
res = sqlResultFrom(sqlmock.NewErrorResult(fmt.Errorf("")))
assert.Equal(t, sqlResult{-1, -1}, res)
}
func helperSQLite3(t *testing.T) (context.Context, Client) {
t.Helper()
ctx := context.Background()
url, _ := url.Parse("file::memory:?cache=shared")
sqlite3, err := NewSQLClient(ctx, url)
assert.NoError(t, err)
return ctx, sqlite3
}
func TestPing(t *testing.T) {
ctx, sqlite3 := helperSQLite3(t)
err := sqlite3.Ping(ctx)
assert.NoError(t, err)
}
func TestSubmit(t *testing.T) {
ctx, sqlite3 := helperSQLite3(t)
res, err := sqlite3.Submit(ctx, &Command{
Statement: "CREATE TABLE t (a INTEGER, b FLOAT, c TEXT, d BLOB);",
Mode: "exec",
})
assert.NoError(t, err)
assert.Equal(t, sqlResult{0, 0}, res)
res, err = sqlite3.Submit(ctx, &Command{
Statement: "SELECT * FROM t;",
Mode: "query",
})
assert.NoError(t, err)
assert.Empty(t, res)
res, err = sqlite3.Submit(ctx, &Command{
Statement: "INSERT INTO t VALUES (?, ?, ?, ?);",
Mode: "exec",
Arguments: Arguments{
Positional: []interface{}{
1,
3.14,
"text",
"blob",
},
},
})
assert.NoError(t, err)
assert.Equal(t, sqlResult{1, 1}, res)
res, err = sqlite3.Submit(ctx, &Command{
Statement: "UPDATE t SET c = NULL;",
Mode: "exec",
})
assert.NoError(t, err)
assert.Equal(t, sqlResult{1, 1}, res)
res, err = sqlite3.Submit(ctx, &Command{
Statement: "SELECT * FROM t WHERE a = ?;",
Mode: "query",
Arguments: Arguments{
Positional: []interface{}{1},
},
})
assert.NoError(t, err)
assert.Len(t, res, 1)
resf, ok := res.([]map[string]interface{})
assert.True(t, ok)
assert.EqualValues(t, map[string]interface{}{
"a": int64(1),
"b": 3.14,
"c": nil,
"d": "blob",
}, resf[0])
res, err = sqlite3.Submit(ctx, &Command{
Statement: "DROP TABLE t;",
Mode: "exec",
})
assert.NoError(t, err)
assert.Equal(t, sqlResult{1, 1}, res)
}
func TestSubmitTransaction(t *testing.T) {
ctx, sqlite3 := helperSQLite3(t)
res, err := sqlite3.Submit(ctx, &Command{
Statement: "BEGIN;",
Mode: "exec",
})
assert.Error(t, err)
assert.Empty(t, res)
res, err = sqlite3.Submit(ctx, &Command{
Statement: "BEGIN; CREATE TABLE tt (a INT); COMMIT;",
Mode: "exec",
Isolation: "none",
})
assert.NoError(t, err)
assert.Equal(t, sqlResult{0, 0}, res)
rows, err := sqlite3.Submit(ctx, &Command{
Statement: "SELECT * FROM tt;",
Mode: "query",
Isolation: "repeatable_read",
})
assert.NoError(t, err)
assert.Empty(t, rows)
}
func TestSubmitTimeout(t *testing.T) {
ctx, sqlite3 := helperSQLite3(t)
res, err := sqlite3.Submit(ctx, &Command{
Statement: "SELECT * FROM t;",
Mode: "query",
Timeout: 1 * time.Nanosecond,
})
assert.Error(t, err)
assert.Empty(t, res)
}
func TestSubmitMode(t *testing.T) {
ctx, sqlite3 := helperSQLite3(t)
res, err := sqlite3.Submit(ctx, &Command{
Statement: "SELECT * FROM t;",
Mode: "notanoption",
})
assert.Error(t, err)
assert.Empty(t, res)
}
func TestSubmitEmpty(t *testing.T) {
ctx, sqlite3 := helperSQLite3(t)
res, err := sqlite3.Submit(ctx, &Command{
Statement: "; ; ; ;",
Mode: "query",
})
assert.Error(t, err)
assert.Empty(t, res)
}
+78
View File
@@ -0,0 +1,78 @@
# docker-compose -f ./dbconnect_tests/dbconnect.yaml up --build --force-recreate --renew-anon-volumes --exit-code-from cloudflared
version: "2.3"
networks:
test-dbconnect-network:
driver: bridge
services:
cloudflared:
build:
context: ../
dockerfile: dev.Dockerfile
command: go test github.com/cloudflare/cloudflared/dbconnect_tests -v
depends_on:
postgres:
condition: service_healthy
mysql:
condition: service_healthy
mssql:
condition: service_healthy
clickhouse:
condition: service_healthy
environment:
DBCONNECT_INTEGRATION_TEST: "true"
POSTGRESQL_URL: postgres://postgres:secret@postgres/db?sslmode=disable
MYSQL_URL: mysql://root:secret@mysql/db?tls=false
MSSQL_URL: mssql://sa:secret12345!@mssql
CLICKHOUSE_URL: clickhouse://clickhouse:9000/db
networks:
- test-dbconnect-network
postgres:
image: postgres:11.4-alpine
environment:
POSTGRES_DB: db
POSTGRES_PASSWORD: secret
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
start_period: 3s
interval: 1s
timeout: 3s
retries: 10
networks:
- test-dbconnect-network
mysql:
image: mysql:8.0
environment:
MYSQL_DATABASE: db
MYSQL_ROOT_PASSWORD: secret
healthcheck:
test: ["CMD", "mysqladmin", "ping"]
start_period: 3s
interval: 1s
timeout: 3s
retries: 10
networks:
- test-dbconnect-network
mssql:
image: mcr.microsoft.com/mssql/server:2017-CU8-ubuntu
environment:
ACCEPT_EULA: "Y"
SA_PASSWORD: secret12345!
healthcheck:
test: ["CMD", "/opt/mssql-tools/bin/sqlcmd", "-S", "localhost", "-U", "sa", "-P", "secret12345!", "-Q", "SELECT 1"]
start_period: 3s
interval: 1s
timeout: 3s
retries: 10
networks:
- test-dbconnect-network
clickhouse:
image: yandex/clickhouse-server:19.11
healthcheck:
test: ["CMD", "clickhouse-client", "--query", "SELECT 1"]
start_period: 3s
interval: 1s
timeout: 3s
retries: 10
networks:
- test-dbconnect-network
+265
View File
@@ -0,0 +1,265 @@
package dbconnect_tests
import (
"context"
"log"
"net/url"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/cloudflare/cloudflared/dbconnect"
)
func TestIntegrationPostgreSQL(t *testing.T) {
ctx, pq := helperNewSQLClient(t, "POSTGRESQL_URL")
err := pq.Ping(ctx)
assert.NoError(t, err)
_, err = pq.Submit(ctx, &dbconnect.Command{
Statement: "CREATE TABLE t (a TEXT, b UUID, c JSON, d INET[], e SERIAL);",
Mode: "exec",
})
assert.NoError(t, err)
_, err = pq.Submit(ctx, &dbconnect.Command{
Statement: "INSERT INTO t VALUES ($1, $2, $3, $4);",
Mode: "exec",
Arguments: dbconnect.Arguments{
Positional: []interface{}{
"text",
"6b8d686d-bd8e-43bc-b09a-cfcbbe702c10",
"{\"bool\":true,\"array\":[\"a\", 1, 3.14],\"embed\":{\"num\":21}}",
[]string{"1.1.1.1", "1.0.0.1"},
},
},
})
assert.NoError(t, err)
_, err = pq.Submit(ctx, &dbconnect.Command{
Statement: "UPDATE t SET b = NULL;",
Mode: "exec",
})
assert.NoError(t, err)
res, err := pq.Submit(ctx, &dbconnect.Command{
Statement: "SELECT * FROM t;",
Mode: "query",
})
assert.NoError(t, err)
assert.IsType(t, make([]map[string]interface{}, 0), res)
actual := res.([]map[string]interface{})[0]
expected := map[string]interface{}{
"a": "text",
"b": nil,
"c": map[string]interface{}{
"bool": true,
"array": []interface{}{"a", float64(1), 3.14},
"embed": map[string]interface{}{"num": float64(21)},
},
"d": "{1.1.1.1,1.0.0.1}",
"e": int64(1),
}
assert.EqualValues(t, expected, actual)
_, err = pq.Submit(ctx, &dbconnect.Command{
Statement: "DROP TABLE t;",
Mode: "exec",
})
assert.NoError(t, err)
}
func TestIntegrationMySQL(t *testing.T) {
ctx, my := helperNewSQLClient(t, "MYSQL_URL")
err := my.Ping(ctx)
assert.NoError(t, err)
_, err = my.Submit(ctx, &dbconnect.Command{
Statement: "CREATE TABLE t (a CHAR, b TINYINT, c FLOAT, d JSON, e YEAR);",
Mode: "exec",
})
assert.NoError(t, err)
_, err = my.Submit(ctx, &dbconnect.Command{
Statement: "INSERT INTO t VALUES (?, ?, ?, ?, ?);",
Mode: "exec",
Arguments: dbconnect.Arguments{
Positional: []interface{}{
"a",
10,
3.14,
"{\"bool\":true}",
2000,
},
},
})
assert.NoError(t, err)
_, err = my.Submit(ctx, &dbconnect.Command{
Statement: "ALTER TABLE t ADD COLUMN f GEOMETRY;",
Mode: "exec",
})
assert.NoError(t, err)
res, err := my.Submit(ctx, &dbconnect.Command{
Statement: "SELECT * FROM t;",
Mode: "query",
})
assert.NoError(t, err)
assert.IsType(t, make([]map[string]interface{}, 0), res)
actual := res.([]map[string]interface{})[0]
expected := map[string]interface{}{
"a": "a",
"b": float64(10),
"c": 3.14,
"d": map[string]interface{}{"bool": true},
"e": float64(2000),
"f": nil,
}
assert.EqualValues(t, expected, actual)
_, err = my.Submit(ctx, &dbconnect.Command{
Statement: "DROP TABLE t;",
Mode: "exec",
})
assert.NoError(t, err)
}
func TestIntegrationMSSQL(t *testing.T) {
ctx, ms := helperNewSQLClient(t, "MSSQL_URL")
err := ms.Ping(ctx)
assert.NoError(t, err)
_, err = ms.Submit(ctx, &dbconnect.Command{
Statement: "CREATE TABLE t (a BIT, b DECIMAL, c MONEY, d TEXT);",
Mode: "exec"})
assert.NoError(t, err)
_, err = ms.Submit(ctx, &dbconnect.Command{
Statement: "INSERT INTO t VALUES (?, ?, ?, ?);",
Mode: "exec",
Arguments: dbconnect.Arguments{
Positional: []interface{}{
0,
3,
"$0.99",
"text",
},
},
})
assert.NoError(t, err)
_, err = ms.Submit(ctx, &dbconnect.Command{
Statement: "UPDATE t SET d = NULL;",
Mode: "exec",
})
assert.NoError(t, err)
res, err := ms.Submit(ctx, &dbconnect.Command{
Statement: "SELECT * FROM t;",
Mode: "query",
})
assert.NoError(t, err)
assert.IsType(t, make([]map[string]interface{}, 0), res)
actual := res.([]map[string]interface{})[0]
expected := map[string]interface{}{
"a": false,
"b": float64(3),
"c": float64(0.99),
"d": nil,
}
assert.EqualValues(t, expected, actual)
_, err = ms.Submit(ctx, &dbconnect.Command{
Statement: "DROP TABLE t;",
Mode: "exec",
})
assert.NoError(t, err)
}
func TestIntegrationClickhouse(t *testing.T) {
ctx, ch := helperNewSQLClient(t, "CLICKHOUSE_URL")
err := ch.Ping(ctx)
assert.NoError(t, err)
_, err = ch.Submit(ctx, &dbconnect.Command{
Statement: "CREATE TABLE t (a UUID, b String, c Float64, d UInt32, e Int16, f Array(Enum8('a'=1, 'b'=2, 'c'=3))) engine=Memory;",
Mode: "exec",
})
assert.NoError(t, err)
_, err = ch.Submit(ctx, &dbconnect.Command{
Statement: "INSERT INTO t VALUES (?, ?, ?, ?, ?, ?);",
Mode: "exec",
Arguments: dbconnect.Arguments{
Positional: []interface{}{
"ec65f626-6f50-4c86-9628-6314ef1edacd",
"",
3.14,
314,
-144,
[]string{"a", "b", "c"},
},
},
})
assert.NoError(t, err)
res, err := ch.Submit(ctx, &dbconnect.Command{
Statement: "SELECT * FROM t;",
Mode: "query",
})
assert.NoError(t, err)
assert.IsType(t, make([]map[string]interface{}, 0), res)
actual := res.([]map[string]interface{})[0]
expected := map[string]interface{}{
"a": "ec65f626-6f50-4c86-9628-6314ef1edacd",
"b": "",
"c": float64(3.14),
"d": uint32(314),
"e": int16(-144),
"f": []string{"a", "b", "c"},
}
assert.EqualValues(t, expected, actual)
_, err = ch.Submit(ctx, &dbconnect.Command{
Statement: "DROP TABLE t;",
Mode: "exec",
})
assert.NoError(t, err)
}
func helperNewSQLClient(t *testing.T, env string) (context.Context, dbconnect.Client) {
_, ok := os.LookupEnv("DBCONNECT_INTEGRATION_TEST")
if ok {
t.Helper()
} else {
t.SkipNow()
}
val, ok := os.LookupEnv(env)
if !ok {
log.Fatalf("must provide database url as environment variable: %s", env)
}
parsed, err := url.Parse(val)
if err != nil {
log.Fatalf("cannot provide invalid database url: %s=%s", env, val)
}
ctx := context.Background()
client, err := dbconnect.NewSQLClient(ctx, parsed)
if err != nil {
log.Fatalf("could not start test client: %s", err)
}
return ctx, client
}
+4
View File
@@ -0,0 +1,4 @@
FROM golang:1.12 as builder
WORKDIR /go/src/github.com/cloudflare/cloudflared/
RUN apt-get update
COPY . .
+135
View File
@@ -0,0 +1,135 @@
package allregions
import (
"context"
"crypto/tls"
"fmt"
"net"
"time"
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
)
const (
// Used to discover HA origintunneld servers
srvService = "origintunneld"
srvProto = "tcp"
srvName = "argotunnel.com"
// Used to fallback to DoT when we can't use the default resolver to
// discover HA origintunneld servers (GitHub issue #75).
dotServerName = "cloudflare-dns.com"
dotServerAddr = "1.1.1.1:853"
dotTimeout = time.Duration(15 * time.Second)
// SRV record resolution TTL
resolveEdgeAddrTTL = 1 * time.Hour
subsystemEdgeAddrResolver = "edgeAddrResolver"
)
// Redeclare network functions so they can be overridden in tests.
var (
netLookupSRV = net.LookupSRV
netLookupIP = net.LookupIP
)
// If the call to net.LookupSRV fails, try to fall back to DoT from Cloudflare directly.
//
// Note: Instead of DoT, we could also have used DoH. Either of these:
// - directly via the JSON API (https://1.1.1.1/dns-query?ct=application/dns-json&name=_origintunneld._tcp.argotunnel.com&type=srv)
// - indirectly via `tunneldns.NewUpstreamHTTPS()`
// But both of these cases miss out on a key feature from the stdlib:
// "The returned records are sorted by priority and randomized by weight within a priority."
// (https://golang.org/pkg/net/#Resolver.LookupSRV)
// Does this matter? I don't know. It may someday. Let's use DoT so we don't need to worry about it.
// See also: Go feature request for stdlib-supported DoH: https://github.com/golang/go/issues/27552
var fallbackLookupSRV = lookupSRVWithDOT
var friendlyDNSErrorLines = []string{
`Please try the following things to diagnose this issue:`,
` 1. ensure that argotunnel.com is returning "origintunneld" service records.`,
` Run your system's equivalent of: dig srv _origintunneld._tcp.argotunnel.com`,
` 2. ensure that your DNS resolver is not returning compressed SRV records.`,
` See GitHub issue https://github.com/golang/go/issues/27546`,
` For example, you could use Cloudflare's 1.1.1.1 as your resolver:`,
` https://developers.cloudflare.com/1.1.1.1/setting-up-1.1.1.1/`,
}
// EdgeDiscovery implements HA service discovery lookup.
func edgeDiscovery(logger logger.Service) ([][]*net.TCPAddr, error) {
_, addrs, err := netLookupSRV(srvService, srvProto, srvName)
if err != nil {
_, fallbackAddrs, fallbackErr := fallbackLookupSRV(srvService, srvProto, srvName)
if fallbackErr != nil || len(fallbackAddrs) == 0 {
// use the original DNS error `err` in messages, not `fallbackErr`
logger.Errorf("Error looking up Cloudflare edge IPs: the DNS query failed: %s", err)
for _, s := range friendlyDNSErrorLines {
logger.Error(s)
}
return nil, errors.Wrapf(err, "Could not lookup srv records on _%v._%v.%v", srvService, srvProto, srvName)
}
// Accept the fallback results and keep going
addrs = fallbackAddrs
}
var resolvedIPsPerCNAME [][]*net.TCPAddr
for _, addr := range addrs {
ips, err := resolveSRVToTCP(addr)
if err != nil {
return nil, err
}
resolvedIPsPerCNAME = append(resolvedIPsPerCNAME, ips)
}
return resolvedIPsPerCNAME, nil
}
func lookupSRVWithDOT(service, proto, name string) (cname string, addrs []*net.SRV, err error) {
// Inspiration: https://github.com/artyom/dot/blob/master/dot.go
r := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, _ string, _ string) (net.Conn, error) {
var dialer net.Dialer
conn, err := dialer.DialContext(ctx, "tcp", dotServerAddr)
if err != nil {
return nil, err
}
tlsConfig := &tls.Config{ServerName: dotServerName}
return tls.Client(conn, tlsConfig), nil
},
}
ctx, cancel := context.WithTimeout(context.Background(), dotTimeout)
defer cancel()
return r.LookupSRV(ctx, srvService, srvProto, srvName)
}
func resolveSRVToTCP(srv *net.SRV) ([]*net.TCPAddr, error) {
ips, err := netLookupIP(srv.Target)
if err != nil {
return nil, errors.Wrapf(err, "Couldn't resolve SRV record %v", srv)
}
if len(ips) == 0 {
return nil, fmt.Errorf("SRV record %v had no IPs", srv)
}
addrs := make([]*net.TCPAddr, len(ips))
for i, ip := range ips {
addrs[i] = &net.TCPAddr{IP: ip, Port: int(srv.Port)}
}
return addrs, nil
}
// ResolveAddrs resolves TCP address given a list of addresses. Address can be a hostname, however, it will return at most one
// of the hostname's IP addresses
func ResolveAddrs(addrs []string) ([]*net.TCPAddr, error) {
var tcpAddrs []*net.TCPAddr
for _, addr := range addrs {
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
return nil, err
}
tcpAddrs = append(tcpAddrs, tcpAddr)
}
return tcpAddrs, nil
}
@@ -0,0 +1,33 @@
package allregions
import (
"testing"
"github.com/cloudflare/cloudflared/logger"
"github.com/stretchr/testify/assert"
)
func TestEdgeDiscovery(t *testing.T) {
mockAddrs := newMockAddrs(19, 2, 5)
netLookupSRV = mockNetLookupSRV(mockAddrs)
netLookupIP = mockNetLookupIP(mockAddrs)
expectedAddrSet := map[string]bool{}
for _, addrs := range mockAddrs.addrMap {
for _, addr := range addrs {
expectedAddrSet[addr.String()] = true
}
}
l := logger.NewOutputWriter(logger.NewMockWriteManager())
addrLists, err := edgeDiscovery(l)
assert.NoError(t, err)
actualAddrSet := map[string]bool{}
for _, addrs := range addrLists {
for _, addr := range addrs {
actualAddrSet[addr.String()] = true
}
}
assert.Equal(t, expectedAddrSet, actualAddrSet)
}
@@ -0,0 +1,89 @@
package allregions
import (
"fmt"
"math"
"math/rand"
"net"
"reflect"
"testing/quick"
)
type mockAddrs struct {
// a set of synthetic SRV records
addrMap map[net.SRV][]*net.TCPAddr
// the total number of addresses, aggregated across addrMap.
// For the convenience of test code that would otherwise have to compute
// this by hand every time.
numAddrs int
}
func newMockAddrs(port uint16, numRegions uint8, numAddrsPerRegion uint8) mockAddrs {
addrMap := make(map[net.SRV][]*net.TCPAddr)
numAddrs := 0
for r := uint8(0); r < numRegions; r++ {
var (
srv = net.SRV{Target: fmt.Sprintf("test-region-%v.example.com", r), Port: port}
addrs []*net.TCPAddr
)
for a := uint8(0); a < numAddrsPerRegion; a++ {
addrs = append(addrs, &net.TCPAddr{
IP: net.ParseIP(fmt.Sprintf("10.0.%v.%v", r, a)),
Port: int(port),
})
}
addrMap[srv] = addrs
numAddrs += len(addrs)
}
return mockAddrs{addrMap: addrMap, numAddrs: numAddrs}
}
var _ quick.Generator = mockAddrs{}
func (mockAddrs) Generate(rand *rand.Rand, size int) reflect.Value {
port := uint16(rand.Intn(math.MaxUint16))
numRegions := uint8(1 + rand.Intn(10))
numAddrsPerRegion := uint8(1 + rand.Intn(32))
result := newMockAddrs(port, numRegions, numAddrsPerRegion)
return reflect.ValueOf(result)
}
// Returns a function compatible with net.LookupSRV that will return the SRV
// records from mockAddrs.
func mockNetLookupSRV(
m mockAddrs,
) func(service, proto, name string) (cname string, addrs []*net.SRV, err error) {
var addrs []*net.SRV
for k := range m.addrMap {
addr := k
addrs = append(addrs, &addr)
// We can't just do
// addrs = append(addrs, &k)
// `k` will be reused by subsequent loop iterations,
// so all the copies of `&k` would point to the same location.
}
return func(_, _, _ string) (string, []*net.SRV, error) {
return "", addrs, nil
}
}
// Returns a function compatible with net.LookupIP that translates the SRV records
// from mockAddrs into IP addresses, based on the TCP addresses in mockAddrs.
func mockNetLookupIP(
m mockAddrs,
) func(host string) ([]net.IP, error) {
return func(host string) ([]net.IP, error) {
for srv, tcpAddrs := range m.addrMap {
if srv.Target != host {
continue
}
result := make([]net.IP, len(tcpAddrs))
for i, tcpAddr := range tcpAddrs {
result[i] = tcpAddr.IP
}
return result, nil
}
return nil, fmt.Errorf("No IPs for %v", host)
}
}
+82
View File
@@ -0,0 +1,82 @@
package allregions
import (
"net"
)
// Region contains cloudflared edge addresses. The edge is partitioned into several regions for
// redundancy purposes.
type Region struct {
connFor map[*net.TCPAddr]UsedBy
}
// NewRegion creates a region with the given addresses, which are all unused.
func NewRegion(addrs []*net.TCPAddr) Region {
// The zero value of UsedBy is Unused(), so we can just initialize the map's values with their
// zero values.
m := make(map[*net.TCPAddr]UsedBy)
for _, addr := range addrs {
m[addr] = Unused()
}
return Region{connFor: m}
}
// AddrUsedBy finds the address used by the given connection in this region.
// Returns nil if the connection isn't using any IP.
func (r *Region) AddrUsedBy(connID int) *net.TCPAddr {
for addr, used := range r.connFor {
if used.Used && used.ConnID == connID {
return addr
}
}
return nil
}
// AvailableAddrs counts how many unused addresses this region contains.
func (r Region) AvailableAddrs() int {
n := 0
for _, usedby := range r.connFor {
if !usedby.Used {
n++
}
}
return n
}
// GetUnusedIP returns a random unused address in this region.
// Returns nil if all addresses are in use.
func (r Region) GetUnusedIP(excluding *net.TCPAddr) *net.TCPAddr {
for addr, usedby := range r.connFor {
if !usedby.Used && addr != excluding {
return addr
}
}
return nil
}
// Use the address, assigning it to a proxy connection.
func (r Region) Use(addr *net.TCPAddr, connID int) {
if addr == nil {
//logrus.Errorf("Attempted to use nil address for connection %d", connID)
return
}
r.connFor[addr] = InUse(connID)
}
// GetAnyAddress returns an arbitrary address from the region.
func (r Region) GetAnyAddress() *net.TCPAddr {
for addr := range r.connFor {
return addr
}
return nil
}
// GiveBack the address, ensuring it is no longer assigned to an IP.
// Returns true if the address is in this region.
func (r Region) GiveBack(addr *net.TCPAddr) (ok bool) {
if _, ok := r.connFor[addr]; !ok {
return false
}
r.connFor[addr] = Unused()
return true
}
+287
View File
@@ -0,0 +1,287 @@
package allregions
import (
"fmt"
"net"
"reflect"
"testing"
)
func TestRegion_New(t *testing.T) {
r := NewRegion([]*net.TCPAddr{&addr0, &addr1, &addr2})
fmt.Println(r.connFor)
if r.AvailableAddrs() != 3 {
t.Errorf("r.AvailableAddrs() == %v but want 3", r.AvailableAddrs())
}
}
func TestRegion_AddrUsedBy(t *testing.T) {
type fields struct {
connFor map[*net.TCPAddr]UsedBy
}
type args struct {
connID int
}
tests := []struct {
name string
fields fields
args args
want *net.TCPAddr
}{
{
name: "happy trivial test",
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
&addr0: InUse(0),
}},
args: args{connID: 0},
want: &addr0,
},
{
name: "sad trivial test",
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
&addr0: InUse(0),
}},
args: args{connID: 1},
want: nil,
},
{
name: "sad test",
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
&addr0: InUse(0),
&addr1: InUse(1),
&addr2: InUse(2),
}},
args: args{connID: 3},
want: nil,
},
{
name: "happy test",
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
&addr0: InUse(0),
&addr1: InUse(1),
&addr2: InUse(2),
}},
args: args{connID: 1},
want: &addr1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Region{
connFor: tt.fields.connFor,
}
if got := r.AddrUsedBy(tt.args.connID); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Region.AddrUsedBy() = %v, want %v", got, tt.want)
}
})
}
}
func TestRegion_AvailableAddrs(t *testing.T) {
type fields struct {
connFor map[*net.TCPAddr]UsedBy
}
tests := []struct {
name string
fields fields
want int
}{
{
name: "contains addresses",
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
&addr0: InUse(0),
&addr1: Unused(),
&addr2: InUse(2),
}},
want: 1,
},
{
name: "all free",
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
&addr0: Unused(),
&addr1: Unused(),
&addr2: Unused(),
}},
want: 3,
},
{
name: "all used",
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
&addr0: InUse(0),
&addr1: InUse(1),
&addr2: InUse(2),
}},
want: 0,
},
{
name: "empty",
fields: fields{connFor: map[*net.TCPAddr]UsedBy{}},
want: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := Region{
connFor: tt.fields.connFor,
}
if got := r.AvailableAddrs(); got != tt.want {
t.Errorf("Region.AvailableAddrs() = %v, want %v", got, tt.want)
}
})
}
}
func TestRegion_GetUnusedIP(t *testing.T) {
type fields struct {
connFor map[*net.TCPAddr]UsedBy
}
type args struct {
excluding *net.TCPAddr
}
tests := []struct {
name string
fields fields
args args
want *net.TCPAddr
}{
{
name: "happy test with excluding set",
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
&addr0: Unused(),
&addr1: Unused(),
&addr2: InUse(2),
}},
args: args{excluding: &addr0},
want: &addr1,
},
{
name: "happy test with no excluding",
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
&addr0: InUse(0),
&addr1: Unused(),
&addr2: InUse(2),
}},
args: args{excluding: nil},
want: &addr1,
},
{
name: "sad test with no excluding",
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
&addr0: InUse(0),
&addr1: InUse(1),
&addr2: InUse(2),
}},
args: args{excluding: nil},
want: nil,
},
{
name: "sad test with excluding",
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
&addr0: Unused(),
&addr1: InUse(1),
&addr2: InUse(2),
}},
args: args{excluding: &addr0},
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := Region{
connFor: tt.fields.connFor,
}
if got := r.GetUnusedIP(tt.args.excluding); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Region.GetUnusedIP() = %v, want %v", got, tt.want)
}
})
}
}
func TestRegion_GiveBack(t *testing.T) {
type fields struct {
connFor map[*net.TCPAddr]UsedBy
}
type args struct {
addr *net.TCPAddr
}
tests := []struct {
name string
fields fields
args args
wantOk bool
availableAfter int
}{
{
name: "sad test with excluding",
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
&addr1: InUse(1),
}},
args: args{addr: &addr1},
wantOk: true,
availableAfter: 1,
},
{
name: "sad test with excluding",
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
&addr1: InUse(1),
}},
args: args{addr: &addr2},
wantOk: false,
availableAfter: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := Region{
connFor: tt.fields.connFor,
}
if gotOk := r.GiveBack(tt.args.addr); gotOk != tt.wantOk {
t.Errorf("Region.GiveBack() = %v, want %v", gotOk, tt.wantOk)
}
if tt.availableAfter != r.AvailableAddrs() {
t.Errorf("Region.AvailableAddrs() = %v, want %v", r.AvailableAddrs(), tt.availableAfter)
}
})
}
}
func TestRegion_GetAnyAddress(t *testing.T) {
type fields struct {
connFor map[*net.TCPAddr]UsedBy
}
tests := []struct {
name string
fields fields
wantNil bool
}{
{
name: "Sad test -- GetAnyAddress should only fail if the region is empty",
fields: fields{connFor: map[*net.TCPAddr]UsedBy{}},
wantNil: true,
},
{
name: "Happy test (all addresses unused)",
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
&addr0: Unused(),
}},
wantNil: false,
},
{
name: "Happy test (GetAnyAddress can still return addresses used by proxy conns)",
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
&addr0: InUse(2),
}},
wantNil: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := Region{
connFor: tt.fields.connFor,
}
if got := r.GetAnyAddress(); tt.wantNil != (got == nil) {
t.Errorf("Region.GetAnyAddress() = %v, but should it return nil? %v", got, tt.wantNil)
}
})
}
}
+125
View File
@@ -0,0 +1,125 @@
package allregions
import (
"fmt"
"net"
"github.com/cloudflare/cloudflared/logger"
)
// Regions stores Cloudflare edge network IPs, partitioned into two regions.
// This is NOT thread-safe. Users of this package should use it with a lock.
type Regions struct {
region1 Region
region2 Region
}
// ------------------------------------
// Constructors
// ------------------------------------
// ResolveEdge resolves the Cloudflare edge, returning all regions discovered.
func ResolveEdge(logger logger.Service) (*Regions, error) {
addrLists, err := edgeDiscovery(logger)
if err != nil {
return nil, err
}
if len(addrLists) < 2 {
return nil, fmt.Errorf("expected at least 2 Cloudflare Regions regions, but SRV only returned %v", len(addrLists))
}
return &Regions{
region1: NewRegion(addrLists[0]),
region2: NewRegion(addrLists[1]),
}, nil
}
// StaticEdge creates a list of edge addresses from the list of hostnames.
// Mainly used for testing connectivity.
func StaticEdge(hostnames []string) (*Regions, error) {
addrs, err := ResolveAddrs(hostnames)
if err != nil {
return nil, err
}
return NewNoResolve(addrs), nil
}
// NewNoResolve doesn't resolve the edge. Instead it just uses the given addresses.
// You probably only need this for testing.
func NewNoResolve(addrs []*net.TCPAddr) *Regions {
region1 := make([]*net.TCPAddr, 0)
region2 := make([]*net.TCPAddr, 0)
for i, v := range addrs {
if i%2 == 0 {
region1 = append(region1, v)
} else {
region2 = append(region2, v)
}
}
return &Regions{
region1: NewRegion(region1),
region2: NewRegion(region2),
}
}
// ------------------------------------
// Methods
// ------------------------------------
// GetAnyAddress returns an arbitrary address from the larger region.
func (rs *Regions) GetAnyAddress() *net.TCPAddr {
if addr := rs.region1.GetAnyAddress(); addr != nil {
return addr
}
return rs.region2.GetAnyAddress()
}
// AddrUsedBy finds the address used by the given connection.
// Returns nil if the connection isn't using an address.
func (rs *Regions) AddrUsedBy(connID int) *net.TCPAddr {
if addr := rs.region1.AddrUsedBy(connID); addr != nil {
return addr
}
return rs.region2.AddrUsedBy(connID)
}
// GetUnusedAddr gets an unused addr from the edge, excluding the given addr. Prefer to use addresses
// evenly across both regions.
func (rs *Regions) GetUnusedAddr(excluding *net.TCPAddr, connID int) *net.TCPAddr {
if rs.region1.AvailableAddrs() > rs.region2.AvailableAddrs() {
return getAddrs(excluding, connID, &rs.region1, &rs.region2)
}
return getAddrs(excluding, connID, &rs.region2, &rs.region1)
}
// getAddrs tries to grab address form `first` region, then `second` region
// this is an unrolled loop over 2 element array
func getAddrs(excluding *net.TCPAddr, connID int, first *Region, second *Region) *net.TCPAddr {
addr := first.GetUnusedIP(excluding)
if addr != nil {
first.Use(addr, connID)
return addr
}
addr = second.GetUnusedIP(excluding)
if addr != nil {
second.Use(addr, connID)
return addr
}
return nil
}
// AvailableAddrs returns how many edge addresses aren't used.
func (rs *Regions) AvailableAddrs() int {
return rs.region1.AvailableAddrs() + rs.region2.AvailableAddrs()
}
// GiveBack the address so that other connections can use it.
// Returns true if the address is in this edge.
func (rs *Regions) GiveBack(addr *net.TCPAddr) bool {
if found := rs.region1.GiveBack(addr); found {
return found
}
return rs.region2.GiveBack(addr)
}
+141
View File
@@ -0,0 +1,141 @@
package allregions
import (
"net"
"testing"
"github.com/stretchr/testify/assert"
)
var (
addr0 = net.TCPAddr{
IP: net.ParseIP("123.4.5.0"),
Port: 8000,
Zone: "",
}
addr1 = net.TCPAddr{
IP: net.ParseIP("123.4.5.1"),
Port: 8000,
Zone: "",
}
addr2 = net.TCPAddr{
IP: net.ParseIP("123.4.5.2"),
Port: 8000,
Zone: "",
}
addr3 = net.TCPAddr{
IP: net.ParseIP("123.4.5.3"),
Port: 8000,
Zone: "",
}
)
func makeRegions() Regions {
r1 := NewRegion([]*net.TCPAddr{&addr0, &addr1})
r2 := NewRegion([]*net.TCPAddr{&addr2, &addr3})
return Regions{region1: r1, region2: r2}
}
func TestRegions_AddrUsedBy(t *testing.T) {
rs := makeRegions()
addr1 := rs.GetUnusedAddr(nil, 1)
assert.Equal(t, addr1, rs.AddrUsedBy(1))
addr2 := rs.GetUnusedAddr(nil, 2)
assert.Equal(t, addr2, rs.AddrUsedBy(2))
addr3 := rs.GetUnusedAddr(nil, 3)
assert.Equal(t, addr3, rs.AddrUsedBy(3))
}
func TestRegions_Giveback_Region1(t *testing.T) {
rs := makeRegions()
rs.region1.Use(&addr0, 0)
rs.region1.Use(&addr1, 1)
rs.region2.Use(&addr2, 2)
rs.region2.Use(&addr3, 3)
assert.Equal(t, 0, rs.AvailableAddrs())
rs.GiveBack(&addr0)
assert.Equal(t, &addr0, rs.GetUnusedAddr(nil, 3))
}
func TestRegions_Giveback_Region2(t *testing.T) {
rs := makeRegions()
rs.region1.Use(&addr0, 0)
rs.region1.Use(&addr1, 1)
rs.region2.Use(&addr2, 2)
rs.region2.Use(&addr3, 3)
assert.Equal(t, 0, rs.AvailableAddrs())
rs.GiveBack(&addr2)
assert.Equal(t, &addr2, rs.GetUnusedAddr(nil, 2))
}
func TestRegions_GetUnusedAddr_OneAddrLeft(t *testing.T) {
rs := makeRegions()
rs.region1.Use(&addr0, 0)
rs.region1.Use(&addr1, 1)
rs.region2.Use(&addr2, 2)
assert.Equal(t, 1, rs.AvailableAddrs())
assert.Equal(t, &addr3, rs.GetUnusedAddr(nil, 3))
}
func TestRegions_GetUnusedAddr_Excluding_Region1(t *testing.T) {
rs := makeRegions()
rs.region1.Use(&addr0, 0)
rs.region1.Use(&addr1, 1)
assert.Equal(t, 2, rs.AvailableAddrs())
assert.Equal(t, &addr3, rs.GetUnusedAddr(&addr2, 3))
}
func TestRegions_GetUnusedAddr_Excluding_Region2(t *testing.T) {
rs := makeRegions()
rs.region2.Use(&addr2, 0)
rs.region2.Use(&addr3, 1)
assert.Equal(t, 2, rs.AvailableAddrs())
assert.Equal(t, &addr1, rs.GetUnusedAddr(&addr0, 1))
}
func TestNewNoResolveBalancesRegions(t *testing.T) {
type args struct {
addrs []*net.TCPAddr
}
tests := []struct {
name string
args args
}{
{
name: "one address",
args: args{addrs: []*net.TCPAddr{&addr0}},
},
{
name: "two addresses",
args: args{addrs: []*net.TCPAddr{&addr0, &addr1}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
regions := NewNoResolve(tt.args.addrs)
RegionsIsBalanced(t, regions)
})
}
}
func RegionsIsBalanced(t *testing.T, rs *Regions) {
delta := rs.region1.AvailableAddrs() - rs.region2.AvailableAddrs()
assert.True(t, abs(delta) <= 1)
}
func abs(x int) int {
if x >= 0 {
return x
}
return -x
}
+14
View File
@@ -0,0 +1,14 @@
package allregions
type UsedBy struct {
ConnID int
Used bool
}
func InUse(connID int) UsedBy {
return UsedBy{ConnID: connID, Used: true}
}
func Unused() UsedBy {
return UsedBy{}
}
+132
View File
@@ -0,0 +1,132 @@
package edgediscovery
import (
"fmt"
"net"
"sync"
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
"github.com/cloudflare/cloudflared/logger"
)
const (
subsystem = "edgediscovery"
)
var errNoAddressesLeft = fmt.Errorf("There are no free edge addresses left")
// Edge finds addresses on the Cloudflare edge and hands them out to connections.
type Edge struct {
regions *allregions.Regions
sync.Mutex
logger logger.Service
}
// ------------------------------------
// Constructors
// ------------------------------------
// ResolveEdge runs the initial discovery of the Cloudflare edge, finding Addrs that can be allocated
// to connections.
func ResolveEdge(l logger.Service) (*Edge, error) {
regions, err := allregions.ResolveEdge(l)
if err != nil {
return new(Edge), err
}
return &Edge{
logger: l,
regions: regions,
}, nil
}
// StaticEdge creates a list of edge addresses from the list of hostnames. Mainly used for testing connectivity.
func StaticEdge(l logger.Service, hostnames []string) (*Edge, error) {
regions, err := allregions.StaticEdge(hostnames)
if err != nil {
return new(Edge), err
}
return &Edge{
logger: l,
regions: regions,
}, nil
}
// MockEdge creates a Cloudflare Edge from arbitrary TCP addresses. Used for testing.
func MockEdge(l logger.Service, addrs []*net.TCPAddr) *Edge {
regions := allregions.NewNoResolve(addrs)
return &Edge{
logger: l,
regions: regions,
}
}
// ------------------------------------
// Methods
// ------------------------------------
// GetAddrForRPC gives this connection an edge Addr.
func (ed *Edge) GetAddrForRPC() (*net.TCPAddr, error) {
ed.Lock()
defer ed.Unlock()
addr := ed.regions.GetAnyAddress()
if addr == nil {
return nil, errNoAddressesLeft
}
return addr, nil
}
// GetAddr gives this proxy connection an edge Addr. Prefer Addrs this connection has already used.
func (ed *Edge) GetAddr(connID int) (*net.TCPAddr, error) {
ed.Lock()
defer ed.Unlock()
// If this connection has already used an edge addr, return it.
if addr := ed.regions.AddrUsedBy(connID); addr != nil {
ed.logger.Debugf("edgediscovery - GetAddr: Returning same address back to proxy connection: connID: %d", connID)
return addr, nil
}
// Otherwise, give it an unused one
addr := ed.regions.GetUnusedAddr(nil, connID)
if addr == nil {
ed.logger.Debugf("edgediscovery - GetAddr: No addresses left to give proxy connection: connID: %d", connID)
return nil, errNoAddressesLeft
}
ed.logger.Debugf("edgediscovery - GetAddr: Giving connection its new address %s: connID: %d", addr, connID)
return addr, nil
}
// GetDifferentAddr gives back the proxy connection's edge Addr and uses a new one.
func (ed *Edge) GetDifferentAddr(connID int) (*net.TCPAddr, error) {
ed.Lock()
defer ed.Unlock()
oldAddr := ed.regions.AddrUsedBy(connID)
if oldAddr != nil {
ed.regions.GiveBack(oldAddr)
}
addr := ed.regions.GetUnusedAddr(oldAddr, connID)
if addr == nil {
ed.logger.Debugf("edgediscovery - GetDifferentAddr: No addresses left to give proxy connection: connID: %d", connID)
// note: if oldAddr were not nil, it will become available on the next iteration
return nil, errNoAddressesLeft
}
ed.logger.Debugf("edgediscovery - GetDifferentAddr: Giving connection its new address %s: connID: %d", addr, connID)
return addr, nil
}
// AvailableAddrs returns how many unused addresses there are left.
func (ed *Edge) AvailableAddrs() int {
ed.Lock()
defer ed.Unlock()
return ed.regions.AvailableAddrs()
}
// GiveBack the address so that other connections can use it.
// Returns true if the address is in this edge.
func (ed *Edge) GiveBack(addr *net.TCPAddr) bool {
ed.Lock()
defer ed.Unlock()
ed.logger.Debug("edgediscovery - GiveBack: Address now unused")
return ed.regions.GiveBack(addr)
}
+177
View File
@@ -0,0 +1,177 @@
package edgediscovery
import (
"net"
"testing"
"github.com/cloudflare/cloudflared/logger"
"github.com/stretchr/testify/assert"
)
var (
addr0 = net.TCPAddr{
IP: net.ParseIP("123.0.0.0"),
Port: 8000,
Zone: "",
}
addr1 = net.TCPAddr{
IP: net.ParseIP("123.0.0.1"),
Port: 8000,
Zone: "",
}
addr2 = net.TCPAddr{
IP: net.ParseIP("123.0.0.2"),
Port: 8000,
Zone: "",
}
addr3 = net.TCPAddr{
IP: net.ParseIP("123.0.0.3"),
Port: 8000,
Zone: "",
}
)
func TestGiveBack(t *testing.T) {
l := logger.NewOutputWriter(logger.NewMockWriteManager())
edge := MockEdge(l, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
// Give this connection an address
assert.Equal(t, 4, edge.AvailableAddrs())
const connID = 0
addr, err := edge.GetAddr(connID)
assert.NoError(t, err)
assert.NotNil(t, addr)
assert.Equal(t, 3, edge.AvailableAddrs())
// Get it back
edge.GiveBack(addr)
assert.Equal(t, 4, edge.AvailableAddrs())
}
func TestRPCAndProxyShareSingleEdgeIP(t *testing.T) {
l := logger.NewOutputWriter(logger.NewMockWriteManager())
// Make an edge with a single IP
edge := MockEdge(l, []*net.TCPAddr{&addr0})
tunnelConnID := 0
// Use the IP for a tunnel
addrTunnel, err := edge.GetAddr(tunnelConnID)
assert.NoError(t, err)
// Ensure the IP can be used for RPC too
addrRPC, err := edge.GetAddrForRPC()
assert.NoError(t, err)
assert.Equal(t, addrTunnel, addrRPC)
}
func TestGetAddrForRPC(t *testing.T) {
l := logger.NewOutputWriter(logger.NewMockWriteManager())
edge := MockEdge(l, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
// Get a connection
assert.Equal(t, 4, edge.AvailableAddrs())
addr, err := edge.GetAddrForRPC()
assert.NoError(t, err)
assert.NotNil(t, addr)
// Using an address for RPC shouldn't consume it
assert.Equal(t, 4, edge.AvailableAddrs())
// Get it back
edge.GiveBack(addr)
assert.Equal(t, 4, edge.AvailableAddrs())
}
func TestOnePerRegion(t *testing.T) {
l := logger.NewOutputWriter(logger.NewMockWriteManager())
// Make an edge with only one address
edge := MockEdge(l, []*net.TCPAddr{&addr0, &addr1})
// Use the only address
const connID = 0
a1, err := edge.GetAddr(connID)
assert.NoError(t, err)
assert.NotNil(t, a1)
// if the first address is bad, get the second one
a2, err := edge.GetDifferentAddr(connID)
assert.NoError(t, err)
assert.NotNil(t, a2)
assert.NotEqual(t, a1, a2)
// now that second one is bad, get the first one again
a3, err := edge.GetDifferentAddr(connID)
assert.NoError(t, err)
assert.Equal(t, a1, a3)
}
func TestOnlyOneAddrLeft(t *testing.T) {
l := logger.NewOutputWriter(logger.NewMockWriteManager())
// Make an edge with only one address
edge := MockEdge(l, []*net.TCPAddr{&addr0})
// Use the only address
const connID = 0
addr, err := edge.GetAddr(connID)
assert.NoError(t, err)
assert.NotNil(t, addr)
// If that edge address is "bad", there's no alternative address.
_, err = edge.GetDifferentAddr(connID)
assert.Error(t, err)
// previously bad address should become available again on next iteration.
addr, err = edge.GetDifferentAddr(connID)
assert.NoError(t, err)
assert.NotNil(t, addr)
}
func TestNoAddrsLeft(t *testing.T) {
l := logger.NewOutputWriter(logger.NewMockWriteManager())
// Make an edge with no addresses
edge := MockEdge(l, []*net.TCPAddr{})
_, err := edge.GetAddr(2)
assert.Error(t, err)
_, err = edge.GetAddrForRPC()
assert.Error(t, err)
}
func TestGetAddr(t *testing.T) {
l := logger.NewOutputWriter(logger.NewMockWriteManager())
edge := MockEdge(l, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
// Give this connection an address
const connID = 0
addr, err := edge.GetAddr(connID)
assert.NoError(t, err)
assert.NotNil(t, addr)
// If the same connection requests another address, it should get the same one.
addr2, err := edge.GetAddr(connID)
assert.NoError(t, err)
assert.Equal(t, addr, addr2)
}
func TestGetDifferentAddr(t *testing.T) {
l := logger.NewOutputWriter(logger.NewMockWriteManager())
edge := MockEdge(l, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
// Give this connection an address
assert.Equal(t, 4, edge.AvailableAddrs())
const connID = 0
addr, err := edge.GetAddr(connID)
assert.NoError(t, err)
assert.NotNil(t, addr)
assert.Equal(t, 3, edge.AvailableAddrs())
// If the same connection requests another address, it should get the same one.
addr2, err := edge.GetDifferentAddr(connID)
assert.NoError(t, err)
assert.NotEqual(t, addr, addr2)
assert.Equal(t, 3, edge.AvailableAddrs())
}
+78
View File
@@ -0,0 +1,78 @@
module github.com/cloudflare/cloudflared
go 1.12
require (
github.com/BurntSushi/go-sumtype v0.0.0-20190304192233-fcb4a6205bdc // indirect
github.com/DATA-DOG/go-sqlmock v1.3.3
github.com/acmacalister/skittles v0.0.0-20160609003031-7423546701e1
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect
github.com/aws/aws-sdk-go v1.25.8
github.com/beorn7/perks v1.0.1 // indirect
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect
github.com/cloudflare/brotli-go v0.0.0-20191101163834-d34379f7ff93
github.com/cloudflare/cfssl v0.0.0-20141119014638-2f7f44e802e2 // indirect
github.com/cloudflare/golibs v0.0.0-20170913112048-333127dbecfc
github.com/coredns/coredns v1.2.0
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73
github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a
github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0
github.com/equinox-io/equinox v1.2.0
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect
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/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/fsnotify/fsnotify v1.4.9
github.com/getsentry/raven-go v0.0.0-20180517221441-ed7bcb39ff10
github.com/gliderlabs/ssh v0.0.0-20191009160644-63518b5243e0
github.com/go-sql-driver/mysql v1.4.1
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
github.com/google/uuid v1.1.1
github.com/gorilla/mux v1.7.3
github.com/gorilla/websocket v1.4.0
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
github.com/jmoiron/sqlx v1.2.0
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kshvakov/clickhouse v1.3.11
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lib/pq v1.2.0
github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mattn/go-isatty v0.0.10 // indirect
github.com/mattn/go-sqlite3 v1.11.0
github.com/mholt/caddy v0.0.0-20180807230124-d3b731e9255b // indirect
github.com/miekg/dns v1.1.27
github.com/mitchellh/go-homedir v1.1.0
github.com/opentracing/opentracing-go v1.1.0 // indirect
github.com/philhofer/fwd v1.0.0 // indirect
github.com/pkg/errors v0.8.1
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/prometheus/client_golang v1.0.0
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 // indirect
github.com/prometheus/common v0.7.0 // indirect
github.com/prometheus/procfs v0.0.5 // indirect
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 // indirect
github.com/stretchr/testify v1.3.0
github.com/tinylib/msgp v1.1.0 // indirect
github.com/xo/dburl v0.0.0-20191005012637-293c3298d6c0
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9
golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 // indirect
golang.org/x/sync v0.0.0-20190423024810-112230192c58
golang.org/x/sys v0.0.0-20191020212454-3e7259c5e7c2
google.golang.org/appengine v1.5.0 // indirect
google.golang.org/genproto v0.0.0-20191007204434-a023cd5227bd // indirect
google.golang.org/grpc v1.24.0 // indirect
gopkg.in/coreos/go-oidc.v2 v2.1.0
gopkg.in/square/go-jose.v2 v2.4.0 // indirect
gopkg.in/urfave/cli.v2 v2.0.0-20180128181224-d604b6ffeee8
gopkg.in/yaml.v2 v2.2.4
zombiezen.com/go/capnproto2 v2.18.0+incompatible
)
// ../../go/pkg/mod/github.com/coredns/coredns@v1.2.0/plugin/metrics/metrics.go:40:49: too many arguments in call to prometheus.NewProcessCollector
// have (int, string)
// want (prometheus.ProcessCollectorOpts)
replace github.com/prometheus/client_golang => github.com/prometheus/client_golang v0.9.0-pre1
+291
View File
@@ -0,0 +1,291 @@
bitbucket.org/liamstask/goose v0.0.0-20150115234039-8488cc47d90c/go.mod h1:hSVuE3qU7grINVSwrmzHfpg9k87ALBk+XaualNyUzI4=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/go-sumtype v0.0.0-20190304192233-fcb4a6205bdc h1:nvTP+jmloR0+J4YQur/rLRdLcGVEU4SquDgH+Bo7gBY=
github.com/BurntSushi/go-sumtype v0.0.0-20190304192233-fcb4a6205bdc/go.mod h1:7yTWMMG2vOm4ABVciEt4EgNVP7fxwtcKIb/EuiLiKqY=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
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/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
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/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
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/aws/aws-sdk-go v1.25.8 h1:n7I+HUUXjun2CsX7JK+1hpRIkZrlKhd3nayeb+Xmavs=
github.com/aws/aws-sdk-go v1.25.8/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
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/bkaradzic/go-lz4 v1.0.0 h1:RXc4wYsyz985CkXXeX04y4VnZFGG8Rd43pRaHsOXAKk=
github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4=
github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261 h1:6/yVvBsKeAw05IUj4AzvrxaCnDjN4nUqKjW9+w5wixg=
github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 h1:JLaf/iINcLyjwbtTsCJjc6rtlASgHeIJPrB6QmwURnA=
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/backoff v0.0.0-20161212185259-647f3cdfc87a/go.mod h1:rzgs2ZOiguV6/NpiDgADjRLPNyZlApIWxKpkT+X8SdY=
github.com/cloudflare/brotli-go v0.0.0-20191101163834-d34379f7ff93 h1:QrGfkZDnMxcWHaYDdB7CmqS9i26OAnUj/xcus/abYkY=
github.com/cloudflare/brotli-go v0.0.0-20191101163834-d34379f7ff93/go.mod h1:QiTe66jFdP7cUKMCCf/WrvDyYdtdmdZfVcdoLbzaKVY=
github.com/cloudflare/cfssl v0.0.0-20141119014638-2f7f44e802e2 h1:GOj9uxwfkVdMaHYGQqhab3hr1rjMDqfaQI+JjpKXgMM=
github.com/cloudflare/cfssl v0.0.0-20141119014638-2f7f44e802e2/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
github.com/cloudflare/go-metrics v0.0.0-20151117154305-6a9aea36fb41/go.mod h1:eaZPlJWD+G9wseg1BuRXlHnjntPMrywMsyxf+LTOdP4=
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/cloudflare/redoctober v0.0.0-20171127175943-746a508df14c/go.mod h1:6Se34jNoqrd8bTxrmJB2Bg2aoZ2CdSXonils9NsiNgo=
github.com/coredns/coredns v1.2.0 h1:YEI38K2BJYzL/SxO2tZFD727T/C68DqVWkBQjT0sWPU=
github.com/coredns/coredns v1.2.0/go.mod h1:zASH/MVDgR6XZTbxvOnsZfffS+31vg6Ackf/wo1+AM0=
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73 h1:7CNPV0LWRCa1FNmqg700pbXhzvmoaXKyfxWRkjRym7Q=
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a h1:W8b4lQ4tFF21aspRGoBuCNV6V2fFJBF+pm1J6OY8Lys=
github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
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/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/equinox-io/equinox v1.2.0 h1:bBS7Ou+Y7Jwgmy8TWSYxEh85WctuFn7FPlgbUzX4DBA=
github.com/equinox-io/equinox v1.2.0/go.mod h1:6s3HJB0PYUNgs0mxmI8fHdfVl3TQ25ieA/PVfr+eyVo=
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9 h1:wWke/RUCl7VRjQhwPlR/v0glZXNYzBHdNUzf/Am2Nmg=
github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9/go.mod h1:uPmAp6Sws4L7+Q/OokbWDAK1ibXYhB3PXFP1kol5hPg=
github.com/facebookgo/grace v0.0.0-20180706040059-75cf19382434 h1:mOp33BLbcbJ8fvTAmZacbBiOASfxN+MLcLxymZCIrGE=
github.com/facebookgo/grace v0.0.0-20180706040059-75cf19382434/go.mod h1:KigFdumBXUPSwzLDbeuzyt0elrL7+CP7TKuhrhT4bcU=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/getsentry/raven-go v0.0.0-20180121060056-563b81fc02b7/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/getsentry/raven-go v0.0.0-20180517221441-ed7bcb39ff10 h1:YO10pIIBftO/kkTFdWhctH96grJ7qiy7bMdiZcIvPKs=
github.com/getsentry/raven-go v0.0.0-20180517221441-ed7bcb39ff10/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
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-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
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 h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/certificate-transparency-go v1.0.21 h1:Yf1aXowfZ2nuboBsg7iYGLmwsOARdV86pfH3g95wXmE=
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU=
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVzF6no3QaDSMLGLEHtHSBSefs+MgcDWnmhmo=
github.com/jmoiron/sqlx v0.0.0-20180124204410-05cef0741ade/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU=
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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/sqlstruct v0.0.0-20150923205031-648daed35d49/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/kisom/goutils v1.1.0/go.mod h1:+UBTfd78habUYWFbNWTJNG+jNG/i/lGURakr4A/yNRw=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/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 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
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/go-gypsy v0.0.0-20160905020020-08cad365cd28/go.mod h1:T/T7jsxVqf9k/zYOqbgNAsANsjxTd1Yq3htjDhQ1H0c=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
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/lib/pq v0.0.0-20180201184707-88edab080323/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
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/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.10.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/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mholt/caddy v0.0.0-20180807230124-d3b731e9255b h1:/BbY4n99iMazlr2igipph+hj0MwlZIWpcsP8Iy+na+s=
github.com/mholt/caddy v0.0.0-20180807230124-d3b731e9255b/go.mod h1:Wb1PlT4DAYSqOEd03MsqkdkXnTxA8v9pKjdpxbqM1kY=
github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM=
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
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 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
github.com/pierrec/lz4 v2.0.5+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 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/prometheus/client_golang v0.9.0-pre1 h1:AWTOhsOI9qxeirTuA0A4By/1Es1+y9EcCGY6bBZ2fhM=
github.com/prometheus/client_golang v0.9.0-pre1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY=
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8=
github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo=
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU=
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
github.com/weppos/publicsuffix-go v0.5.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
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/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is=
github.com/zmap/zcrypto v0.0.0-20190729165852-9051775e6a2e/go.mod h1:w7kd3qXHh8FNaczNjslXqvFQiv5mMWRXlL9klTUAHc8=
github.com/zmap/zlint v0.0.0-20190806154020-fd021b4cfbeb/go.mod h1:29UiAJNsiVdvTBFCJW8e3q6dcDbOoPkhMgttOSCIMMY=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/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-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582 h1:p9xBe/w/OzkeYVKm234g55gMdD1nSIooTir5kV11kfA=
golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191020212454-3e7259c5e7c2 h1:nq114VpM8lsSlP+lyUbANecYHYiFcSNFtqcBlxRV+gA=
golang.org/x/sys v0.0.0-20191020212454-3e7259c5e7c2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190221204921-83362c3779f5/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20191007204434-a023cd5227bd h1:84VQPzup3IpKLxuIAZjHMhVjJ8fZ4/i3yUnj3k6fUdw=
google.golang.org/genproto v0.0.0-20191007204434-a023cd5227bd/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.24.0 h1:vb/1TCsVn3DcJlQ0Gs1yB1pKI6Do2/QNwxdKqmc/b0s=
google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/coreos/go-oidc.v2 v2.1.0 h1:E8PjVFdj/SLDKB0hvb70KTbMbYVHjqztiQdSkIg8E+I=
gopkg.in/coreos/go-oidc.v2 v2.1.0/go.mod h1:fYaTe2FS96wZZwR17YTDHwG+Mw6fmyqJNxN2eNCGPCI=
gopkg.in/square/go-jose.v2 v2.4.0 h1:0kXPskUMGAXXWJlP05ktEMOV0vmzFQUWw6d+aZJQU8A=
gopkg.in/square/go-jose.v2 v2.4.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/urfave/cli.v2 v2.0.0-20180128181224-d604b6ffeee8 h1:/pLAskKF+d5SawboKd8GB8ew4ClHDbt2c3K9EBFeRGU=
gopkg.in/urfave/cli.v2 v2.0.0-20180128181224-d604b6ffeee8/go.mod h1:cKXr3E0k4aosgycml1b5z33BVV6hai1Kh7uDgFOkbcs=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
zombiezen.com/go/capnproto2 v0.0.0-20180616160808-7cfd211c19c7 h1:CZoOFlTPbKfAShKYrMuUfYbnXexFT1rYRUX1SPnrdE4=
zombiezen.com/go/capnproto2 v0.0.0-20180616160808-7cfd211c19c7/go.mod h1:TMGa8HWGJkXiq4nHe9Zu/JgRF5oUtg4XizFC+Vexbec=
zombiezen.com/go/capnproto2 v2.18.0+incompatible h1:mwfXZniffG5mXokQGHUJWGnqIBggoPfT/CEwon9Yess=
zombiezen.com/go/capnproto2 v2.18.0+incompatible/go.mod h1:XO5Pr2SbXgqZwn0m0Ru54QBqpOf4K5AYBO+8LAOBQEQ=
+37 -20
View File
@@ -3,6 +3,7 @@ package h2mux
import (
"sync"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/net/http2"
)
@@ -12,23 +13,28 @@ type activeStreamMap struct {
sync.RWMutex
// streams tracks open streams.
streams map[uint32]*MuxedStream
// streamsEmpty is a chan that should be closed when no more streams are open.
streamsEmpty chan struct{}
// nextStreamID is the next ID to use on our side of the connection.
// This is odd for clients, even for servers.
nextStreamID uint32
// maxPeerStreamID is the ID of the most recent stream opened by the peer.
maxPeerStreamID uint32
// activeStreams is a gauge shared by all muxers of this process to expose the total number of active streams
activeStreams prometheus.Gauge
// ignoreNewStreams is true when the connection is being shut down. New streams
// cannot be registered.
ignoreNewStreams bool
// streamsEmpty is a chan that will be closed when no more streams are open.
streamsEmptyChan chan struct{}
closeOnce sync.Once
}
func newActiveStreamMap(useClientStreamNumbers bool) *activeStreamMap {
func newActiveStreamMap(useClientStreamNumbers bool, activeStreams prometheus.Gauge) *activeStreamMap {
m := &activeStreamMap{
streams: make(map[uint32]*MuxedStream),
streamsEmpty: make(chan struct{}),
nextStreamID: 1,
streams: make(map[uint32]*MuxedStream),
streamsEmptyChan: make(chan struct{}),
nextStreamID: 1,
activeStreams: activeStreams,
}
// Client initiated stream uses odd stream ID, server initiated stream uses even stream ID
if !useClientStreamNumbers {
@@ -37,6 +43,13 @@ func newActiveStreamMap(useClientStreamNumbers bool) *activeStreamMap {
return m
}
// This function should be called while `m` is locked.
func (m *activeStreamMap) notifyStreamsEmpty() {
m.closeOnce.Do(func() {
close(m.streamsEmptyChan)
})
}
// Len returns the number of active streams.
func (m *activeStreamMap) Len() int {
m.RLock()
@@ -63,6 +76,7 @@ func (m *activeStreamMap) Set(newStream *MuxedStream) bool {
return false
}
m.streams[newStream.streamID] = newStream
m.activeStreams.Inc()
return true
}
@@ -70,31 +84,33 @@ func (m *activeStreamMap) Set(newStream *MuxedStream) bool {
func (m *activeStreamMap) Delete(streamID uint32) {
m.Lock()
defer m.Unlock()
delete(m.streams, streamID)
if len(m.streams) == 0 && m.streamsEmpty != nil {
close(m.streamsEmpty)
m.streamsEmpty = nil
if _, ok := m.streams[streamID]; ok {
delete(m.streams, streamID)
m.activeStreams.Dec()
}
// shutting down, and now the map is empty
if m.ignoreNewStreams && len(m.streams) == 0 {
m.notifyStreamsEmpty()
}
}
// Shutdown blocks new streams from being created. It returns a channel that receives an event
// once the last stream has closed, or nil if a shutdown is in progress.
func (m *activeStreamMap) Shutdown() <-chan struct{} {
// Shutdown blocks new streams from being created.
// It returns `done`, a channel that is closed once the last stream has closed
// and `progress`, whether a shutdown was already in progress
func (m *activeStreamMap) Shutdown() (done <-chan struct{}, alreadyInProgress bool) {
m.Lock()
defer m.Unlock()
if m.ignoreNewStreams {
// already shutting down
return nil
return m.streamsEmptyChan, true
}
m.ignoreNewStreams = true
done := make(chan struct{})
if len(m.streams) == 0 {
// nothing to shut down
close(done)
return done
// there are no streams to wait for
m.notifyStreamsEmpty()
}
m.streamsEmpty = done
return done
return m.streamsEmptyChan, false
}
// AcquireLocalID acquires a new stream ID for a stream you're opening.
@@ -162,4 +178,5 @@ func (m *activeStreamMap) Abort() {
stream.Close()
}
m.ignoreNewStreams = true
m.notifyStreamsEmpty()
}
+195
View File
@@ -0,0 +1,195 @@
package h2mux
import (
"sync"
"testing"
"github.com/stretchr/testify/assert"
)
func TestShutdown(t *testing.T) {
const numStreams = 1000
m := newActiveStreamMap(true, NewActiveStreamsMetrics("test", t.Name()))
// Add all the streams
{
var wg sync.WaitGroup
wg.Add(numStreams)
for i := 0; i < numStreams; i++ {
go func(streamID int) {
defer wg.Done()
stream := &MuxedStream{streamID: uint32(streamID)}
ok := m.Set(stream)
assert.True(t, ok)
}(i)
}
wg.Wait()
}
assert.Equal(t, numStreams, m.Len(), "All the streams should have been added")
shutdownChan, alreadyInProgress := m.Shutdown()
select {
case <-shutdownChan:
assert.Fail(t, "before Shutdown(), shutdownChan shouldn't be closed")
default:
}
assert.False(t, alreadyInProgress)
shutdownChan2, alreadyInProgress2 := m.Shutdown()
assert.Equal(t, shutdownChan, shutdownChan2, "repeated calls to Shutdown() should return the same channel")
assert.True(t, alreadyInProgress2, "repeated calls to Shutdown() should return true for 'in progress'")
// Delete all the streams
{
var wg sync.WaitGroup
wg.Add(numStreams)
for i := 0; i < numStreams; i++ {
go func(streamID int) {
defer wg.Done()
m.Delete(uint32(streamID))
}(i)
}
wg.Wait()
}
assert.Equal(t, 0, m.Len(), "All the streams should have been deleted")
select {
case <-shutdownChan:
default:
assert.Fail(t, "After all the streams are deleted, shutdownChan should have been closed")
}
}
func TestEmptyBeforeShutdown(t *testing.T) {
const numStreams = 1000
m := newActiveStreamMap(true, NewActiveStreamsMetrics("test", t.Name()))
// Add all the streams
{
var wg sync.WaitGroup
wg.Add(numStreams)
for i := 0; i < numStreams; i++ {
go func(streamID int) {
defer wg.Done()
stream := &MuxedStream{streamID: uint32(streamID)}
ok := m.Set(stream)
assert.True(t, ok)
}(i)
}
wg.Wait()
}
assert.Equal(t, numStreams, m.Len(), "All the streams should have been added")
// Delete all the streams, bringing m to size 0
{
var wg sync.WaitGroup
wg.Add(numStreams)
for i := 0; i < numStreams; i++ {
go func(streamID int) {
defer wg.Done()
m.Delete(uint32(streamID))
}(i)
}
wg.Wait()
}
assert.Equal(t, 0, m.Len(), "All the streams should have been deleted")
// Add one stream back
const soloStreamID = uint32(0)
ok := m.Set(&MuxedStream{streamID: soloStreamID})
assert.True(t, ok)
shutdownChan, alreadyInProgress := m.Shutdown()
select {
case <-shutdownChan:
assert.Fail(t, "before Shutdown(), shutdownChan shouldn't be closed")
default:
}
assert.False(t, alreadyInProgress)
shutdownChan2, alreadyInProgress2 := m.Shutdown()
assert.Equal(t, shutdownChan, shutdownChan2, "repeated calls to Shutdown() should return the same channel")
assert.True(t, alreadyInProgress2, "repeated calls to Shutdown() should return true for 'in progress'")
// Remove the remaining stream
m.Delete(soloStreamID)
select {
case <-shutdownChan:
default:
assert.Fail(t, "After all the streams are deleted, shutdownChan should have been closed")
}
}
type noopBuffer struct {
isClosed bool
}
func (t *noopBuffer) Read(p []byte) (n int, err error) { return len(p), nil }
func (t *noopBuffer) Write(p []byte) (n int, err error) { return len(p), nil }
func (t *noopBuffer) Reset() {}
func (t *noopBuffer) Len() int { return 0 }
func (t *noopBuffer) Close() error { t.isClosed = true; return nil }
func (t *noopBuffer) Closed() bool { return t.isClosed }
type noopReadyList struct{}
func (_ *noopReadyList) Signal(streamID uint32) {}
func TestAbort(t *testing.T) {
const numStreams = 1000
m := newActiveStreamMap(true, NewActiveStreamsMetrics("test", t.Name()))
var openedStreams sync.Map
// Add all the streams
{
var wg sync.WaitGroup
wg.Add(numStreams)
for i := 0; i < numStreams; i++ {
go func(streamID int) {
defer wg.Done()
stream := &MuxedStream{
streamID: uint32(streamID),
readBuffer: &noopBuffer{},
writeBuffer: &noopBuffer{},
readyList: &noopReadyList{},
}
ok := m.Set(stream)
assert.True(t, ok)
openedStreams.Store(stream.streamID, stream)
}(i)
}
wg.Wait()
}
assert.Equal(t, numStreams, m.Len(), "All the streams should have been added")
shutdownChan, alreadyInProgress := m.Shutdown()
select {
case <-shutdownChan:
assert.Fail(t, "before Abort(), shutdownChan shouldn't be closed")
default:
}
assert.False(t, alreadyInProgress)
m.Abort()
assert.Equal(t, numStreams, m.Len(), "Abort() shouldn't delete any streams")
openedStreams.Range(func(key interface{}, value interface{}) bool {
stream := value.(*MuxedStream)
readBuffer := stream.readBuffer.(*noopBuffer)
writeBuffer := stream.writeBuffer.(*noopBuffer)
return assert.True(t, readBuffer.isClosed && writeBuffer.isClosed, "Abort() should have closed all the streams")
})
select {
case <-shutdownChan:
default:
assert.Fail(t, "after Abort(), shutdownChan should have been closed")
}
// multiple aborts shouldn't cause any issues
m.Abort()
m.Abort()
m.Abort()
}
+7 -3
View File
@@ -7,6 +7,7 @@ import (
)
var (
// HTTP2 error codes: https://http2.github.io/http2-spec/#ErrorCodes
ErrHandshakeTimeout = MuxerHandshakeError{"1000 handshake timeout"}
ErrBadHandshakeNotSettings = MuxerHandshakeError{"1001 unexpected response"}
ErrBadHandshakeUnexpectedAck = MuxerHandshakeError{"1002 unexpected response"}
@@ -19,9 +20,12 @@ var (
ErrUnknownStream = MuxerProtocolError{"2002 unknown stream", http2.ErrCodeProtocol}
ErrInvalidStream = MuxerProtocolError{"2003 invalid stream", http2.ErrCodeProtocol}
ErrStreamHeadersSent = MuxerApplicationError{"3000 headers already sent"}
ErrConnectionClosed = MuxerApplicationError{"3001 connection closed"}
ErrConnectionDropped = MuxerApplicationError{"3002 connection dropped"}
ErrStreamHeadersSent = MuxerApplicationError{"3000 headers already sent"}
ErrStreamRequestConnectionClosed = MuxerApplicationError{"3001 connection closed while opening stream"}
ErrConnectionDropped = MuxerApplicationError{"3002 connection dropped"}
ErrStreamRequestTimeout = MuxerApplicationError{"3003 open stream timeout"}
ErrResponseHeadersTimeout = MuxerApplicationError{"3004 timeout waiting for initial response headers"}
ErrResponseHeadersConnectionClosed = MuxerApplicationError{"3005 connection closed while waiting for initial response headers"}
ErrClosedStream = MuxerStreamError{"4000 stream closed", http2.ErrCodeStreamClosed}
)
+4 -1
View File
@@ -542,7 +542,10 @@ func (w *h2DictWriter) Write(p []byte) (n int, err error) {
}
func (w *h2DictWriter) Close() error {
return w.comp.Close()
if w.comp != nil {
return w.comp.Close()
}
return nil
}
// From http2/hpack
+119 -47
View File
@@ -1,14 +1,14 @@
package h2mux
import (
"bytes"
"context"
"io"
"strings"
"sync"
"time"
log "github.com/sirupsen/logrus"
"github.com/cloudflare/cloudflared/logger"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/net/http2"
"golang.org/x/net/http2/hpack"
"golang.org/x/sync/errgroup"
@@ -20,7 +20,7 @@ const (
maxWindowSize uint32 = (1 << 31) - 1 // 2^31-1 = 2147483647, max window size in http2 spec
defaultTimeout time.Duration = 5 * time.Second
defaultRetries uint64 = 5
defaultWriteBufferMaxLen int = 1024 * 1024 * 512 // 500mb
defaultWriteBufferMaxLen int = 1024 * 1024 // 1mb
SettingMuxerMagic http2.SettingID = 0x42db
MuxerMagicOrigin uint32 = 0xa2e43c8b
@@ -48,7 +48,7 @@ type MuxerConfig struct {
// The minimum number of heartbeats to send before terminating the connection.
MaxHeartbeats uint64
// Logger to use
Logger *log.Entry
Logger logger.Service
CompressionQuality CompressionSetting
// Initial size for HTTP2 flow control windows
DefaultWindowSize uint32
@@ -90,8 +90,12 @@ type Muxer struct {
compressionQuality CompressionPreset
}
type Header struct {
Name, Value string
func RPCHeaders() []Header {
return []Header{
{Name: ":method", Value: "RPC"},
{Name: ":scheme", Value: "capnp"},
{Name: ":path", Value: "*"},
}
}
// Handshake establishes a muxed connection with the peer.
@@ -100,6 +104,7 @@ func Handshake(
w io.WriteCloser,
r io.ReadCloser,
config MuxerConfig,
activeStreamsMetrics prometheus.Gauge,
) (*Muxer, error) {
// Set default config values
if config.Timeout == 0 {
@@ -123,7 +128,7 @@ func Handshake(
newStreamChan: make(chan MuxedStreamRequest),
abortChan: make(chan struct{}),
readyList: NewReadyList(),
streams: newActiveStreamMap(config.IsClient),
streams: newActiveStreamMap(config.IsClient, activeStreamsMetrics),
}
m.f.ReadMetaHeaders = hpack.NewDecoder(4096, func(hpack.HeaderField) {})
@@ -131,10 +136,10 @@ func Handshake(
handshakeSetting := http2.Setting{ID: SettingMuxerMagic, Val: MuxerMagicEdge}
compressionSetting := http2.Setting{ID: SettingCompression, Val: config.CompressionQuality.toH2Setting()}
if CompressionIsSupported() {
log.Debug("Compression is supported")
config.Logger.Debug("muxer: Compression is supported")
m.compressionQuality = config.CompressionQuality.getPreset()
} else {
log.Debug("Compression is not supported")
config.Logger.Debug("muxer: Compression is not supported")
compressionSetting = http2.Setting{ID: SettingCompression, Val: 0}
}
@@ -171,12 +176,12 @@ func Handshake(
// Sanity check to enusre idelDuration is sane
if idleDuration == 0 || idleDuration < defaultTimeout {
idleDuration = defaultTimeout
config.Logger.Warn("Minimum idle time has been adjusted to ", defaultTimeout)
config.Logger.Infof("muxer: Minimum idle time has been adjusted to %d", defaultTimeout)
}
maxRetries := config.MaxHeartbeats
if maxRetries == 0 {
maxRetries = defaultRetries
config.Logger.Warn("Minimum number of unacked heartbeats to send before closing the connection has been adjusted to ", maxRetries)
config.Logger.Infof("muxer: Minimum number of unacked heartbeats to send before closing the connection has been adjusted to %d", maxRetries)
}
compBytesBefore, compBytesAfter := NewAtomicCounter(0), NewAtomicCounter(0)
@@ -316,24 +321,63 @@ func joinErrorsWithTimeout(errChan <-chan error, receiveCount int, timeout time.
func (m *Muxer) Serve(ctx context.Context) error {
errGroup, _ := errgroup.WithContext(ctx)
errGroup.Go(func() error {
err := m.muxReader.run(m.config.Logger)
m.explicitShutdown.Fuse(false)
m.r.Close()
m.abort()
return err
ch := make(chan error)
go func() {
err := m.muxReader.run(m.config.Logger)
m.explicitShutdown.Fuse(false)
m.r.Close()
m.abort()
// don't block if parent goroutine quit early
select {
case ch <- err:
default:
}
}()
select {
case err := <-ch:
return err
case <-ctx.Done():
return ctx.Err()
}
})
errGroup.Go(func() error {
err := m.muxWriter.run(m.config.Logger)
m.explicitShutdown.Fuse(false)
m.w.Close()
m.abort()
return err
ch := make(chan error)
go func() {
err := m.muxWriter.run(m.config.Logger)
m.explicitShutdown.Fuse(false)
m.w.Close()
m.abort()
// don't block if parent goroutine quit early
select {
case ch <- err:
default:
}
}()
select {
case err := <-ch:
return err
case <-ctx.Done():
return ctx.Err()
}
})
errGroup.Go(func() error {
err := m.muxMetricsUpdater.run(m.config.Logger)
return err
ch := make(chan error)
go func() {
err := m.muxMetricsUpdater.run(m.config.Logger)
// don't block if parent goroutine quit early
select {
case ch <- err:
default:
}
}()
select {
case err := <-ch:
return err
case <-ctx.Done():
return ctx.Err()
}
})
err := errGroup.Wait()
@@ -344,9 +388,11 @@ func (m *Muxer) Serve(ctx context.Context) error {
}
// Shutdown is called to initiate the "happy path" of muxer termination.
func (m *Muxer) Shutdown() {
// It blocks new streams from being created.
// It returns a channel that is closed when the last stream has been closed.
func (m *Muxer) Shutdown() <-chan struct{} {
m.explicitShutdown.Fuse(true)
m.muxReader.Shutdown()
return m.muxReader.Shutdown()
}
// IsUnexpectedTunnelError identifies errors that are expected when shutting down the h2mux tunnel.
@@ -379,33 +425,59 @@ func isConnectionClosedError(err error) bool {
// OpenStream opens a new data stream with the given headers.
// Called by proxy server and tunnel
func (m *Muxer) OpenStream(headers []Header, body io.Reader) (*MuxedStream, error) {
stream := &MuxedStream{
responseHeadersReceived: make(chan struct{}),
readBuffer: NewSharedBuffer(),
writeBuffer: &bytes.Buffer{},
writeBufferMaxLen: m.config.StreamWriteBufferMaxLen,
writeBufferHasSpace: make(chan struct{}, 1),
receiveWindow: m.config.DefaultWindowSize,
receiveWindowCurrentMax: m.config.DefaultWindowSize,
receiveWindowMax: m.config.MaxWindowSize,
sendWindow: m.config.DefaultWindowSize,
readyList: m.readyList,
writeHeaders: headers,
dictionaries: m.muxReader.dictionaries,
func (m *Muxer) OpenStream(ctx context.Context, headers []Header, body io.Reader) (*MuxedStream, error) {
stream := m.NewStream(headers)
if err := m.MakeMuxedStreamRequest(ctx, NewMuxedStreamRequest(stream, body)); err != nil {
return nil, err
}
if err := m.AwaitResponseHeaders(ctx, stream); err != nil {
return nil, err
}
return stream, nil
}
select {
// Will be received by mux writer
case m.newStreamChan <- MuxedStreamRequest{stream: stream, body: body}:
case <-m.abortChan:
return nil, ErrConnectionClosed
func (m *Muxer) OpenRPCStream(ctx context.Context) (*MuxedStream, error) {
stream := m.NewStream(RPCHeaders())
if err := m.MakeMuxedStreamRequest(ctx, NewMuxedStreamRequest(stream, nil)); err != nil {
return nil, err
}
if err := m.AwaitResponseHeaders(ctx, stream); err != nil {
return nil, err
}
return stream, nil
}
func (m *Muxer) NewStream(headers []Header) *MuxedStream {
return NewStream(m.config, headers, m.readyList, m.muxReader.dictionaries)
}
func (m *Muxer) MakeMuxedStreamRequest(ctx context.Context, request MuxedStreamRequest) error {
select {
case <-stream.responseHeadersReceived:
return stream, nil
case <-ctx.Done():
return ErrStreamRequestTimeout
case <-m.abortChan:
return nil, ErrConnectionClosed
return ErrStreamRequestConnectionClosed
// Will be received by mux writer
case m.newStreamChan <- request:
return nil
}
}
func (m *Muxer) CloseStreamRead(stream *MuxedStream) {
stream.CloseRead()
if stream.WriteClosed() {
m.streams.Delete(stream.streamID)
}
}
func (m *Muxer) AwaitResponseHeaders(ctx context.Context, stream *MuxedStream) error {
select {
case <-ctx.Done():
return ErrResponseHeadersTimeout
case <-m.abortChan:
return ErrResponseHeadersConnectionClosed
case <-stream.responseHeadersReceived:
return nil
}
}
+278 -214
View File
@@ -15,12 +15,20 @@ import (
"testing"
"time"
log "github.com/sirupsen/logrus"
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/sync/errgroup"
)
const (
testOpenStreamTimeout = time.Millisecond * 5000
testHandshakeTimeout = time.Millisecond * 1000
)
func TestMain(m *testing.M) {
if os.Getenv("VERBOSE") == "1" {
log.SetLevel(log.DebugLevel)
//TODO: set log level
}
os.Exit(m.Run())
}
@@ -35,42 +43,52 @@ type DefaultMuxerPair struct {
doneC chan struct{}
}
func NewDefaultMuxerPair() *DefaultMuxerPair {
func NewDefaultMuxerPair(t assert.TestingT, testName string, f MuxedStreamFunc) *DefaultMuxerPair {
origin, edge := net.Pipe()
return &DefaultMuxerPair{
p := &DefaultMuxerPair{
OriginMuxConfig: MuxerConfig{
Timeout: time.Second,
Timeout: testHandshakeTimeout,
Handler: f,
IsClient: true,
Name: "origin",
Logger: log.NewEntry(log.New()),
Logger: logger.NewOutputWriter(logger.NewMockWriteManager()),
DefaultWindowSize: (1 << 8) - 1,
MaxWindowSize: (1 << 15) - 1,
StreamWriteBufferMaxLen: 1024,
HeartbeatInterval: defaultTimeout,
MaxHeartbeats: defaultRetries,
},
OriginConn: origin,
EdgeMuxConfig: MuxerConfig{
Timeout: time.Second,
Timeout: testHandshakeTimeout,
IsClient: false,
Name: "edge",
Logger: log.NewEntry(log.New()),
Logger: logger.NewOutputWriter(logger.NewMockWriteManager()),
DefaultWindowSize: (1 << 8) - 1,
MaxWindowSize: (1 << 15) - 1,
StreamWriteBufferMaxLen: 1024,
HeartbeatInterval: defaultTimeout,
MaxHeartbeats: defaultRetries,
},
EdgeConn: edge,
doneC: make(chan struct{}),
}
assert.NoError(t, p.Handshake(testName))
return p
}
func NewCompressedMuxerPair(quality CompressionSetting) *DefaultMuxerPair {
func NewCompressedMuxerPair(t assert.TestingT, testName string, quality CompressionSetting, f MuxedStreamFunc) *DefaultMuxerPair {
origin, edge := net.Pipe()
return &DefaultMuxerPair{
p := &DefaultMuxerPair{
OriginMuxConfig: MuxerConfig{
Timeout: time.Second,
Handler: f,
IsClient: true,
Name: "origin",
CompressionQuality: quality,
Logger: log.NewEntry(log.New()),
Logger: logger.NewOutputWriter(logger.NewMockWriteManager()),
HeartbeatInterval: defaultTimeout,
MaxHeartbeats: defaultRetries,
},
OriginConn: origin,
EdgeMuxConfig: MuxerConfig{
@@ -78,49 +96,35 @@ func NewCompressedMuxerPair(quality CompressionSetting) *DefaultMuxerPair {
IsClient: false,
Name: "edge",
CompressionQuality: quality,
Logger: log.NewEntry(log.New()),
Logger: logger.NewOutputWriter(logger.NewMockWriteManager()),
HeartbeatInterval: defaultTimeout,
MaxHeartbeats: defaultRetries,
},
EdgeConn: edge,
doneC: make(chan struct{}),
}
assert.NoError(t, p.Handshake(testName))
return p
}
func (p *DefaultMuxerPair) Handshake(t *testing.T) {
edgeErrC := make(chan error)
originErrC := make(chan error)
go func() {
var err error
p.EdgeMux, err = Handshake(p.EdgeConn, p.EdgeConn, p.EdgeMuxConfig)
edgeErrC <- err
}()
go func() {
var err error
p.OriginMux, err = Handshake(p.OriginConn, p.OriginConn, p.OriginMuxConfig)
originErrC <- err
}()
func (p *DefaultMuxerPair) Handshake(testName string) error {
ctx, cancel := context.WithTimeout(context.Background(), testHandshakeTimeout)
defer cancel()
errGroup, _ := errgroup.WithContext(ctx)
errGroup.Go(func() (err error) {
p.EdgeMux, err = Handshake(p.EdgeConn, p.EdgeConn, p.EdgeMuxConfig, NewActiveStreamsMetrics(testName, "edge"))
return errors.Wrap(err, "edge handshake failure")
})
errGroup.Go(func() (err error) {
p.OriginMux, err = Handshake(p.OriginConn, p.OriginConn, p.OriginMuxConfig, NewActiveStreamsMetrics(testName, "origin"))
return errors.Wrap(err, "origin handshake failure")
})
select {
case err := <-edgeErrC:
if err != nil {
t.Fatalf("edge handshake failure: %s", err)
}
case <-time.After(time.Second * 5):
t.Fatalf("edge handshake timeout")
}
select {
case err := <-originErrC:
if err != nil {
t.Fatalf("origin handshake failure: %s", err)
}
case <-time.After(time.Second * 5):
t.Fatalf("origin handshake timeout")
}
return errGroup.Wait()
}
func (p *DefaultMuxerPair) HandshakeAndServe(t *testing.T) {
func (p *DefaultMuxerPair) Serve(t assert.TestingT) {
ctx := context.Background()
p.Handshake(t)
var wg sync.WaitGroup
wg.Add(2)
go func() {
@@ -155,18 +159,23 @@ func (p *DefaultMuxerPair) Wait(t *testing.T) {
}
}
func (p *DefaultMuxerPair) OpenEdgeMuxStream(headers []Header, body io.Reader) (*MuxedStream, error) {
ctx, cancel := context.WithTimeout(context.Background(), testOpenStreamTimeout)
defer cancel()
return p.EdgeMux.OpenStream(ctx, headers, body)
}
func TestHandshake(t *testing.T) {
muxPair := NewDefaultMuxerPair()
muxPair.Handshake(t)
f := func(stream *MuxedStream) error {
return nil
}
muxPair := NewDefaultMuxerPair(t, t.Name(), f)
AssertIfPipeReadable(t, muxPair.OriginConn)
AssertIfPipeReadable(t, muxPair.EdgeConn)
}
func TestSingleStream(t *testing.T) {
closeC := make(chan struct{})
muxPair := NewDefaultMuxerPair()
muxPair.OriginMuxConfig.Handler = MuxedStreamFunc(func(stream *MuxedStream) error {
defer close(closeC)
f := MuxedStreamFunc(func(stream *MuxedStream) error {
if len(stream.Headers) != 1 {
t.Fatalf("expected %d headers, got %d", 1, len(stream.Headers))
}
@@ -181,8 +190,6 @@ func TestSingleStream(t *testing.T) {
})
buf := []byte("Hello world")
stream.Write(buf)
// after this receive, the edge closed the stream
<-closeC
n, err := io.ReadFull(stream, buf)
if n > 0 {
t.Fatalf("read %d bytes after EOF", n)
@@ -192,9 +199,10 @@ func TestSingleStream(t *testing.T) {
}
return nil
})
muxPair.HandshakeAndServe(t)
muxPair := NewDefaultMuxerPair(t, t.Name(), f)
muxPair.Serve(t)
stream, err := muxPair.EdgeMux.OpenStream(
stream, err := muxPair.OpenEdgeMuxStream(
[]Header{{Name: "test-header", Value: "headerValue"}},
nil,
)
@@ -222,7 +230,6 @@ func TestSingleStream(t *testing.T) {
t.Fatalf("expected response body %s, got %s", "Hello world", responseBody)
}
stream.Close()
closeC <- struct{}{}
n, err = stream.Write([]byte("aaaaa"))
if n > 0 {
t.Fatalf("wrote %d bytes after EOF", n)
@@ -230,13 +237,11 @@ func TestSingleStream(t *testing.T) {
if err != io.EOF {
t.Fatalf("expected EOF, got %s", err)
}
<-closeC
}
func TestSingleStreamLargeResponseBody(t *testing.T) {
muxPair := NewDefaultMuxerPair()
bodySize := 1 << 24
muxPair.OriginMuxConfig.Handler = MuxedStreamFunc(func(stream *MuxedStream) error {
f := MuxedStreamFunc(func(stream *MuxedStream) error {
if len(stream.Headers) != 1 {
t.Fatalf("expected %d headers, got %d", 1, len(stream.Headers))
}
@@ -265,9 +270,10 @@ func TestSingleStreamLargeResponseBody(t *testing.T) {
return nil
})
muxPair.HandshakeAndServe(t)
muxPair := NewDefaultMuxerPair(t, t.Name(), f)
muxPair.Serve(t)
stream, err := muxPair.EdgeMux.OpenStream(
stream, err := muxPair.OpenEdgeMuxStream(
[]Header{{Name: "test-header", Value: "headerValue"}},
nil,
)
@@ -295,38 +301,39 @@ func TestSingleStreamLargeResponseBody(t *testing.T) {
}
func TestMultipleStreams(t *testing.T) {
muxPair := NewDefaultMuxerPair()
maxStreams := 64
errorsC := make(chan error, maxStreams)
muxPair.OriginMuxConfig.Handler = MuxedStreamFunc(func(stream *MuxedStream) error {
l := logger.NewOutputWriter(logger.NewMockWriteManager())
f := MuxedStreamFunc(func(stream *MuxedStream) error {
if len(stream.Headers) != 1 {
t.Fatalf("expected %d headers, got %d", 1, len(stream.Headers))
}
if stream.Headers[0].Name != "client-token" {
t.Fatalf("expected header name %s, got %s", "client-token", stream.Headers[0].Name)
}
log.Debugf("Got request for stream %s", stream.Headers[0].Value)
l.Debugf("Got request for stream %s", stream.Headers[0].Value)
stream.WriteHeaders([]Header{
{Name: "response-token", Value: stream.Headers[0].Value},
})
log.Debugf("Wrote headers for stream %s", stream.Headers[0].Value)
l.Debugf("Wrote headers for stream %s", stream.Headers[0].Value)
stream.Write([]byte("OK"))
log.Debugf("Wrote body for stream %s", stream.Headers[0].Value)
l.Debugf("Wrote body for stream %s", stream.Headers[0].Value)
return nil
})
muxPair.HandshakeAndServe(t)
muxPair := NewDefaultMuxerPair(t, t.Name(), f)
muxPair.Serve(t)
maxStreams := 64
errorsC := make(chan error, maxStreams)
var wg sync.WaitGroup
wg.Add(maxStreams)
for i := 0; i < maxStreams; i++ {
go func(tokenId int) {
defer wg.Done()
tokenString := fmt.Sprintf("%d", tokenId)
stream, err := muxPair.EdgeMux.OpenStream(
stream, err := muxPair.OpenEdgeMuxStream(
[]Header{{Name: "client-token", Value: tokenString}},
nil,
)
log.Debugf("Got headers for stream %d", tokenId)
l.Debugf("Got headers for stream %d", tokenId)
if err != nil {
errorsC <- err
return
@@ -364,7 +371,7 @@ func TestMultipleStreams(t *testing.T) {
testFail := false
for err := range errorsC {
testFail = true
log.Error(err)
l.Errorf("%s", err)
}
if testFail {
t.Fatalf("TestMultipleStreams failed")
@@ -373,13 +380,12 @@ func TestMultipleStreams(t *testing.T) {
func TestMultipleStreamsFlowControl(t *testing.T) {
maxStreams := 32
errorsC := make(chan error, maxStreams)
responseSizes := make([]int32, maxStreams)
for i := 0; i < maxStreams; i++ {
responseSizes[i] = rand.Int31n(int32(defaultWindowSize << 4))
}
muxPair := NewDefaultMuxerPair()
muxPair.OriginMuxConfig.Handler = MuxedStreamFunc(func(stream *MuxedStream) error {
f := MuxedStreamFunc(func(stream *MuxedStream) error {
if len(stream.Headers) != 1 {
t.Fatalf("expected %d headers, got %d", 1, len(stream.Headers))
}
@@ -405,94 +411,82 @@ func TestMultipleStreamsFlowControl(t *testing.T) {
}
return nil
})
muxPair.HandshakeAndServe(t)
muxPair := NewDefaultMuxerPair(t, t.Name(), f)
muxPair.Serve(t)
var wg sync.WaitGroup
wg.Add(maxStreams)
errGroup, _ := errgroup.WithContext(context.Background())
for i := 0; i < maxStreams; i++ {
go func(tokenId int) {
defer wg.Done()
stream, err := muxPair.EdgeMux.OpenStream(
errGroup.Go(func() error {
stream, err := muxPair.OpenEdgeMuxStream(
[]Header{{Name: "test-header", Value: "headerValue"}},
nil,
)
if err != nil {
errorsC <- fmt.Errorf("stream %d error in OpenStream: %s", stream.streamID, err)
return
return fmt.Errorf("error in OpenStream: %d %s", stream.streamID, err)
}
if len(stream.Headers) != 1 {
errorsC <- fmt.Errorf("stream %d expected %d headers, got %d", stream.streamID, 1, len(stream.Headers))
return
return fmt.Errorf("stream %d expected %d headers, got %d", stream.streamID, 1, len(stream.Headers))
}
if stream.Headers[0].Name != "response-header" {
errorsC <- fmt.Errorf("stream %d expected header name %s, got %s", stream.streamID, "response-header", stream.Headers[0].Name)
return
return fmt.Errorf("stream %d expected header name %s, got %s", stream.streamID, "response-header", stream.Headers[0].Name)
}
if stream.Headers[0].Value != "responseValue" {
errorsC <- fmt.Errorf("stream %d expected header value %s, got %s", stream.streamID, "responseValue", stream.Headers[0].Value)
return
return fmt.Errorf("stream %d expected header value %s, got %s", stream.streamID, "responseValue", stream.Headers[0].Value)
}
responseBody := make([]byte, responseSizes[(stream.streamID-2)/2])
n, err := io.ReadFull(stream, responseBody)
if err != nil {
errorsC <- fmt.Errorf("stream %d error from (*MuxedStream).Read: %s", stream.streamID, err)
return
return fmt.Errorf("stream %d error from (*MuxedStream).Read: %s", stream.streamID, err)
}
if n != len(responseBody) {
errorsC <- fmt.Errorf("stream %d expected response body to have %d bytes, got %d", stream.streamID, len(responseBody), n)
return
return fmt.Errorf("stream %d expected response body to have %d bytes, got %d", stream.streamID, len(responseBody), n)
}
}(i)
}
wg.Wait()
close(errorsC)
testFail := false
for err := range errorsC {
testFail = true
log.Error(err)
}
if testFail {
t.Fatalf("TestMultipleStreamsFlowControl failed")
return nil
})
}
assert.NoError(t, errGroup.Wait())
}
func TestGracefulShutdown(t *testing.T) {
l := logger.NewOutputWriter(logger.NewMockWriteManager())
sendC := make(chan struct{})
responseBuf := bytes.Repeat([]byte("Hello world"), 65536)
muxPair := NewDefaultMuxerPair()
muxPair.OriginMuxConfig.Handler = MuxedStreamFunc(func(stream *MuxedStream) error {
f := MuxedStreamFunc(func(stream *MuxedStream) error {
stream.WriteHeaders([]Header{
{Name: "response-header", Value: "responseValue"},
})
<-sendC
log.Debugf("Writing %d bytes", len(responseBuf))
l.Debugf("Writing %d bytes", len(responseBuf))
stream.Write(responseBuf)
stream.CloseWrite()
log.Debugf("Wrote %d bytes", len(responseBuf))
l.Debugf("Wrote %d bytes", len(responseBuf))
// Reading from the stream will block until the edge closes its end of the stream.
// Otherwise, we'll close the whole connection before receiving the 'stream closed'
// message from the edge.
// Graceful shutdown works if you omit this, it just gives spurious errors for now -
// TODO ignore errors when writing 'stream closed' and we're shutting down.
stream.Read([]byte{0})
log.Debugf("Handler ends")
l.Debugf("Handler ends")
return nil
})
muxPair.HandshakeAndServe(t)
muxPair := NewDefaultMuxerPair(t, t.Name(), f)
muxPair.Serve(t)
stream, err := muxPair.EdgeMux.OpenStream(
stream, err := muxPair.OpenEdgeMuxStream(
[]Header{{Name: "test-header", Value: "headerValue"}},
nil,
)
// Start graceful shutdown of the edge mux - this should also close the origin mux when done
muxPair.EdgeMux.Shutdown()
close(sendC)
if err != nil {
t.Fatalf("error in OpenStream: %s", err)
}
// Start graceful shutdown of the edge mux - this should also close the origin mux when done
muxPair.EdgeMux.Shutdown()
close(sendC)
responseBody := make([]byte, len(responseBuf))
log.Debugf("Waiting for %d bytes", len(responseBuf))
l.Debugf("Waiting for %d bytes", len(responseBuf))
n, err := io.ReadFull(stream, responseBody)
if err != nil {
t.Fatalf("error from (*MuxedStream).Read with %d bytes read: %s", n, err)
@@ -511,8 +505,8 @@ func TestUnexpectedShutdown(t *testing.T) {
sendC := make(chan struct{})
handlerFinishC := make(chan struct{})
responseBuf := bytes.Repeat([]byte("Hello world"), 65536)
muxPair := NewDefaultMuxerPair()
muxPair.OriginMuxConfig.Handler = MuxedStreamFunc(func(stream *MuxedStream) error {
f := MuxedStreamFunc(func(stream *MuxedStream) error {
defer close(handlerFinishC)
stream.WriteHeaders([]Header{
{Name: "response-header", Value: "responseValue"},
@@ -533,9 +527,10 @@ func TestUnexpectedShutdown(t *testing.T) {
}
return nil
})
muxPair.HandshakeAndServe(t)
muxPair := NewDefaultMuxerPair(t, t.Name(), f)
muxPair.Serve(t)
stream, err := muxPair.EdgeMux.OpenStream(
stream, err := muxPair.OpenEdgeMuxStream(
[]Header{{Name: "test-header", Value: "headerValue"}},
nil,
)
@@ -580,9 +575,8 @@ func EchoHandler(stream *MuxedStream) error {
func TestOpenAfterDisconnect(t *testing.T) {
for i := 0; i < 3; i++ {
muxPair := NewDefaultMuxerPair()
muxPair.OriginMuxConfig.Handler = MuxedStreamFunc(EchoHandler)
muxPair.HandshakeAndServe(t)
muxPair := NewDefaultMuxerPair(t, fmt.Sprintf("%s_%d", t.Name(), i), EchoHandler)
muxPair.Serve(t)
switch i {
case 0:
@@ -597,22 +591,21 @@ func TestOpenAfterDisconnect(t *testing.T) {
muxPair.EdgeConn.Close()
}
_, err := muxPair.EdgeMux.OpenStream(
_, err := muxPair.OpenEdgeMuxStream(
[]Header{{Name: "test-header", Value: "headerValue"}},
nil,
)
if err != ErrConnectionClosed {
t.Fatalf("unexpected error in OpenStream: %s", err)
if err != ErrStreamRequestConnectionClosed && err != ErrResponseHeadersConnectionClosed {
t.Fatalf("case %v: unexpected error in OpenStream: %v", i, err)
}
}
}
func TestHPACK(t *testing.T) {
muxPair := NewDefaultMuxerPair()
muxPair.OriginMuxConfig.Handler = MuxedStreamFunc(EchoHandler)
muxPair.HandshakeAndServe(t)
muxPair := NewDefaultMuxerPair(t, t.Name(), EchoHandler)
muxPair.Serve(t)
stream, err := muxPair.EdgeMux.OpenStream(
stream, err := muxPair.OpenEdgeMuxStream(
[]Header{
{Name: ":method", Value: "RPC"},
{Name: ":scheme", Value: "capnp"},
@@ -626,7 +619,7 @@ func TestHPACK(t *testing.T) {
stream.Close()
for i := 0; i < 3; i++ {
stream, err := muxPair.EdgeMux.OpenStream(
stream, err := muxPair.OpenEdgeMuxStream(
[]Header{
{Name: ":method", Value: "GET"},
{Name: ":scheme", Value: "https"},
@@ -686,10 +679,9 @@ func AssertIfPipeReadable(t *testing.T, pipe io.ReadCloser) {
}
func TestMultipleStreamsWithDictionaries(t *testing.T) {
l := logger.NewOutputWriter(logger.NewMockWriteManager())
for q := CompressionNone; q <= CompressionMax; q++ {
muxPair := NewCompressedMuxerPair(q)
htmlBody := `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"` +
`"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">` +
`<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">` +
@@ -712,7 +704,7 @@ func TestMultipleStreamsWithDictionaries(t *testing.T) {
`</body>` +
`</html>`
muxPair.OriginMuxConfig.Handler = MuxedStreamFunc(func(stream *MuxedStream) error {
f := MuxedStreamFunc(func(stream *MuxedStream) error {
var contentType string
var pathHeader Header
@@ -744,8 +736,8 @@ func TestMultipleStreamsWithDictionaries(t *testing.T) {
return nil
})
muxPair.HandshakeAndServe(t)
muxPair := NewCompressedMuxerPair(t, fmt.Sprintf("%s_%d", t.Name(), q), q, f)
muxPair.Serve(t)
var wg sync.WaitGroup
@@ -782,25 +774,26 @@ func TestMultipleStreamsWithDictionaries(t *testing.T) {
errorsC := make(chan error, len(paths))
for i, s := range paths {
go func(i int, path string) {
go func(index int, path string) {
defer wg.Done()
stream, err := muxPair.EdgeMux.OpenStream(
stream, err := muxPair.OpenEdgeMuxStream(
[]Header{
{Name: ":method", Value: "GET"},
{Name: ":scheme", Value: "https"},
{Name: ":authority", Value: "tunnel.otterlyadorable.co.uk"},
{Name: ":path", Value: path},
{Name: "cf-ray", Value: "378948953f044408-SFO-DOG"},
{Name: "idx", Value: strconv.Itoa(i)},
{Name: "idx", Value: strconv.Itoa(index)},
{Name: "accept-encoding", Value: "gzip, br"},
},
nil,
)
if err != nil {
t.Fatalf("error in OpenStream: %s", err)
errorsC <- fmt.Errorf("error in OpenStream: %v", err)
return
}
expectBody := strings.Replace(htmlBody, "paragraph", path, 1) + strconv.Itoa(i)
expectBody := strings.Replace(htmlBody, "paragraph", path, 1) + strconv.Itoa(index)
responseBody := make([]byte, len(expectBody)*2)
n, err := stream.Read(responseBody)
if err != nil {
@@ -823,7 +816,7 @@ func TestMultipleStreamsWithDictionaries(t *testing.T) {
testFail := false
for err := range errorsC {
testFail = true
log.Error(err)
l.Errorf("%s", err)
}
if testFail {
t.Fatalf("TestMultipleStreams failed")
@@ -836,42 +829,49 @@ func TestMultipleStreamsWithDictionaries(t *testing.T) {
}
}
func sampleSiteHandler(stream *MuxedStream) error {
var contentType string
var pathHeader Header
func sampleSiteHandler(files map[string][]byte) MuxedStreamFunc {
l := logger.NewOutputWriter(logger.NewMockWriteManager())
for _, h := range stream.Headers {
if h.Name == ":path" {
pathHeader = h
break
return func(stream *MuxedStream) error {
var contentType string
var pathHeader Header
for _, h := range stream.Headers {
if h.Name == ":path" {
pathHeader = h
break
}
}
}
if pathHeader.Name != ":path" {
panic("Couldn't find :path header in test")
}
if pathHeader.Name != ":path" {
return fmt.Errorf("Couldn't find :path header in test")
}
if strings.Contains(pathHeader.Value, "html") {
contentType = "text/html; charset=utf-8"
} else if strings.Contains(pathHeader.Value, "js") {
contentType = "application/javascript"
} else if strings.Contains(pathHeader.Value, "css") {
contentType = "text/css"
} else {
contentType = "img/gif"
if strings.Contains(pathHeader.Value, "html") {
contentType = "text/html; charset=utf-8"
} else if strings.Contains(pathHeader.Value, "js") {
contentType = "application/javascript"
} else if strings.Contains(pathHeader.Value, "css") {
contentType = "text/css"
} else {
contentType = "img/gif"
}
stream.WriteHeaders([]Header{
Header{Name: "content-type", Value: contentType},
})
l.Debugf("Wrote headers for stream %s", pathHeader.Value)
file, ok := files[pathHeader.Value]
if !ok {
return fmt.Errorf("%s content is not preloaded", pathHeader.Value)
}
stream.Write(file)
l.Debugf("Wrote body for stream %s", pathHeader.Value)
return nil
}
stream.WriteHeaders([]Header{
Header{Name: "content-type", Value: contentType},
})
log.Debugf("Wrote headers for stream %s", pathHeader.Value)
b, _ := ioutil.ReadFile("./sample" + pathHeader.Value)
stream.Write(b)
log.Debugf("Wrote body for stream %s", pathHeader.Value)
return nil
}
func sampleSiteTest(t *testing.T, muxPair *DefaultMuxerPair, path string) {
stream, err := muxPair.EdgeMux.OpenStream(
func sampleSiteTest(muxPair *DefaultMuxerPair, path string, files map[string][]byte) error {
stream, err := muxPair.OpenEdgeMuxStream(
[]Header{
{Name: ":method", Value: "GET"},
{Name: ":scheme", Value: "https"},
@@ -883,50 +883,75 @@ func sampleSiteTest(t *testing.T, muxPair *DefaultMuxerPair, path string) {
nil,
)
if err != nil {
t.Fatalf("error in OpenStream: %s", err)
return fmt.Errorf("error in OpenStream: %v", err)
}
expectBody, _ := ioutil.ReadFile("./sample" + path)
responseBody := make([]byte, len(expectBody))
file, ok := files[path]
if !ok {
return fmt.Errorf("%s content is not preloaded", path)
}
responseBody := make([]byte, len(file))
n, err := io.ReadFull(stream, responseBody)
log.Debugf("Got body for stream %s", path)
if err != nil {
t.Fatalf("error from (*MuxedStream).Read: %s", err)
return fmt.Errorf("error from (*MuxedStream).Read: %v", err)
}
if n != len(expectBody) {
t.Fatalf("expected response body to have %d bytes, got %d", len(expectBody), n)
if n != len(file) {
return fmt.Errorf("expected response body to have %d bytes, got %d", len(file), n)
}
if string(responseBody[:n]) != string(expectBody) {
t.Fatalf("expected response body %s, got %s", expectBody, responseBody[:n])
if string(responseBody[:n]) != string(file) {
return fmt.Errorf("expected response body %s, got %s", file, responseBody[:n])
}
return nil
}
func loadSampleFiles(paths []string) (map[string][]byte, error) {
files := make(map[string][]byte)
for _, path := range paths {
if _, ok := files[path]; !ok {
expectBody, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
files[path] = expectBody
}
}
return files, nil
}
func TestSampleSiteWithDictionaries(t *testing.T) {
paths := []string{
"./sample/index.html",
"./sample/index2.html",
"./sample/index1.html",
"./sample/ghost-url.min.js",
"./sample/jquery.fitvids.js",
"./sample/index1.html",
"./sample/index2.html",
"./sample/index.html",
}
files, err := loadSampleFiles(paths)
assert.NoError(t, err)
for q := CompressionNone; q <= CompressionMax; q++ {
muxPair := NewCompressedMuxerPair(q)
muxPair.OriginMuxConfig.Handler = MuxedStreamFunc(sampleSiteHandler)
muxPair.HandshakeAndServe(t)
muxPair := NewCompressedMuxerPair(t, fmt.Sprintf("%s_%d", t.Name(), q), q, sampleSiteHandler(files))
muxPair.Serve(t)
var wg sync.WaitGroup
paths := []string{
"/index.html",
"/index2.html",
"/index1.html",
"/ghost-url.min.js",
"/jquery.fitvids.js",
"/index1.html",
"/index2.html",
"/index.html",
}
errC := make(chan error, len(paths))
wg.Add(len(paths))
for _, s := range paths {
go func(path string) {
sampleSiteTest(t, muxPair, path)
wg.Done()
defer wg.Done()
errC <- sampleSiteTest(muxPair, path, files)
}(s)
}
wg.Wait()
close(errC)
for err := range errC {
assert.NoError(t, err)
}
originMuxMetrics := muxPair.OriginMux.Metrics()
if q > CompressionNone && originMuxMetrics.CompBytesBefore.Value() <= 10*originMuxMetrics.CompBytesAfter.Value() {
@@ -936,35 +961,74 @@ func TestSampleSiteWithDictionaries(t *testing.T) {
}
func TestLongSiteWithDictionaries(t *testing.T) {
paths := []string{
"./sample/index.html",
"./sample/index1.html",
"./sample/index2.html",
"./sample/ghost-url.min.js",
"./sample/jquery.fitvids.js",
}
files, err := loadSampleFiles(paths)
assert.NoError(t, err)
for q := CompressionNone; q <= CompressionMedium; q++ {
muxPair := NewCompressedMuxerPair(q)
muxPair.OriginMuxConfig.Handler = MuxedStreamFunc(sampleSiteHandler)
muxPair.HandshakeAndServe(t)
muxPair := NewCompressedMuxerPair(t, fmt.Sprintf("%s_%d", t.Name(), q), q, sampleSiteHandler(files))
muxPair.Serve(t)
var wg sync.WaitGroup
rand.Seed(time.Now().Unix())
paths := []string{
"/index.html",
"/index1.html",
"/index2.html",
"/ghost-url.min.js",
"/jquery.fitvids.js"}
tstLen := 1000
wg.Add(tstLen)
tstLen := 500
errGroup, _ := errgroup.WithContext(context.Background())
for i := 0; i < tstLen; i++ {
path := paths[rand.Int()%len(paths)]
go func(path string) {
sampleSiteTest(t, muxPair, path)
wg.Done()
}(path)
errGroup.Go(func() error {
path := paths[rand.Int()%len(paths)]
return sampleSiteTest(muxPair, path, files)
})
}
wg.Wait()
assert.NoError(t, errGroup.Wait())
originMuxMetrics := muxPair.OriginMux.Metrics()
if q > CompressionNone && originMuxMetrics.CompBytesBefore.Value() <= 100*originMuxMetrics.CompBytesAfter.Value() {
if q > CompressionNone && originMuxMetrics.CompBytesBefore.Value() <= 10*originMuxMetrics.CompBytesAfter.Value() {
t.Fatalf("Cross-stream compression is expected to give a better compression ratio")
}
}
}
func BenchmarkOpenStream(b *testing.B) {
const streams = 5000
for i := 0; i < b.N; i++ {
b.StopTimer()
f := MuxedStreamFunc(func(stream *MuxedStream) error {
if len(stream.Headers) != 1 {
b.Fatalf("expected %d headers, got %d", 1, len(stream.Headers))
}
if stream.Headers[0].Name != "test-header" {
b.Fatalf("expected header name %s, got %s", "test-header", stream.Headers[0].Name)
}
if stream.Headers[0].Value != "headerValue" {
b.Fatalf("expected header value %s, got %s", "headerValue", stream.Headers[0].Value)
}
stream.WriteHeaders([]Header{
{Name: "response-header", Value: "responseValue"},
})
return nil
})
muxPair := NewDefaultMuxerPair(b, fmt.Sprintf("%s_%d", b.Name(), i), f)
muxPair.Serve(b)
b.StartTimer()
openStreams(b, muxPair, streams)
}
}
func openStreams(b *testing.B, muxPair *DefaultMuxerPair, n int) {
errGroup, _ := errgroup.WithContext(context.Background())
for i := 0; i < n; i++ {
errGroup.Go(func() error {
_, err := muxPair.OpenEdgeMuxStream(
[]Header{{Name: "test-header", Value: "headerValue"}},
nil,
)
return err
})
}
assert.NoError(b, errGroup.Wait())
}
+254
View File
@@ -0,0 +1,254 @@
package h2mux
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/pkg/errors"
)
type Header struct {
Name, Value string
}
var headerEncoding = base64.RawStdEncoding
const (
RequestUserHeadersField = "cf-cloudflared-request-headers"
ResponseUserHeadersField = "cf-cloudflared-response-headers"
ResponseMetaHeaderField = "cf-cloudflared-response-meta"
ResponseSourceCloudflared = "cloudflared"
ResponseSourceOrigin = "origin"
CFAccessTokenHeader = "cf-access-token"
CFJumpDestinationHeader = "CF-Access-Jump-Destination"
CFAccessClientIDHeader = "CF-Access-Client-Id"
CFAccessClientSecretHeader = "CF-Access-Client-Secret"
)
// H2RequestHeadersToH1Request converts the HTTP/2 headers coming from origintunneld
// to an HTTP/1 Request object destined for the local origin web service.
// This operation includes conversion of the pseudo-headers into their closest
// HTTP/1 equivalents. See https://tools.ietf.org/html/rfc7540#section-8.1.2.3
func H2RequestHeadersToH1Request(h2 []Header, h1 *http.Request) error {
for _, header := range h2 {
if !IsControlHeader(header.Name) {
continue
}
switch strings.ToLower(header.Name) {
case ":method":
h1.Method = header.Value
case ":scheme":
// noop - use the preexisting scheme from h1.URL
case ":authority":
// Otherwise the host header will be based on the origin URL
h1.Host = header.Value
case ":path":
// We don't want to be an "opinionated" proxy, so ideally we would use :path as-is.
// However, this HTTP/1 Request object belongs to the Go standard library,
// whose URL package makes some opinionated decisions about the encoding of
// URL characters: see the docs of https://godoc.org/net/url#URL,
// in particular the EscapedPath method https://godoc.org/net/url#URL.EscapedPath,
// which is always used when computing url.URL.String(), whether we'd like it or not.
//
// Well, not *always*. We could circumvent this by using url.URL.Opaque. But
// that would present unusual difficulties when using an HTTP proxy: url.URL.Opaque
// is treated differently when HTTP_PROXY is set!
// See https://github.com/golang/go/issues/5684#issuecomment-66080888
//
// This means we are subject to the behavior of net/url's function `shouldEscape`
// (as invoked with mode=encodePath): https://github.com/golang/go/blob/go1.12.7/src/net/url/url.go#L101
if header.Value == "*" {
h1.URL.Path = "*"
continue
}
// Due to the behavior of validation.ValidateUrl, h1.URL may
// already have a partial value, with or without a trailing slash.
base := h1.URL.String()
base = strings.TrimRight(base, "/")
// But we know :path begins with '/', because we handled '*' above - see RFC7540
requestURL, err := url.Parse(base + header.Value)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("invalid path '%v'", header.Value))
}
h1.URL = requestURL
case "content-length":
contentLength, err := strconv.ParseInt(header.Value, 10, 64)
if err != nil {
return fmt.Errorf("unparseable content length")
}
h1.ContentLength = contentLength
case RequestUserHeadersField:
// Do not forward the serialized headers to the origin -- deserialize them, and ditch the serialized version
// Find and parse user headers serialized into a single one
userHeaders, err := ParseUserHeaders(RequestUserHeadersField, h2)
if err != nil {
return errors.Wrap(err, "Unable to parse user headers")
}
for _, userHeader := range userHeaders {
h1.Header.Add(http.CanonicalHeaderKey(userHeader.Name), userHeader.Value)
}
default:
// All other control headers shall just be proxied transparently
h1.Header.Add(http.CanonicalHeaderKey(header.Name), header.Value)
}
}
return nil
}
func ParseUserHeaders(headerNameToParseFrom string, headers []Header) ([]Header, error) {
for _, header := range headers {
if header.Name == headerNameToParseFrom {
return DeserializeHeaders(header.Value)
}
}
return nil, fmt.Errorf("%v header not found", RequestUserHeadersField)
}
func IsControlHeader(headerName string) bool {
headerName = strings.ToLower(headerName)
return headerName == "content-length" ||
headerName == "connection" || headerName == "upgrade" || // Websocket headers
strings.HasPrefix(headerName, ":") ||
strings.HasPrefix(headerName, "cf-")
}
// IsWebsocketClientHeader returns true if the header name is required by the client to upgrade properly
func IsWebsocketClientHeader(headerName string) bool {
headerName = strings.ToLower(headerName)
return headerName == "sec-websocket-accept" ||
headerName == "connection" ||
headerName == "upgrade"
}
func H1ResponseToH2ResponseHeaders(h1 *http.Response) (h2 []Header) {
h2 = []Header{
{Name: ":status", Value: strconv.Itoa(h1.StatusCode)},
}
userHeaders := http.Header{}
for header, values := range h1.Header {
for _, value := range values {
if strings.ToLower(header) == "content-length" {
// This header has meaning in HTTP/2 and will be used by the edge,
// so it should be sent as an HTTP/2 response header.
// Since these are http2 headers, they're required to be lowercase
h2 = append(h2, Header{Name: strings.ToLower(header), Value: value})
} else if !IsControlHeader(header) || IsWebsocketClientHeader(header) {
// User headers, on the other hand, must all be serialized so that
// HTTP/2 header validation won't be applied to HTTP/1 header values
if _, ok := userHeaders[header]; ok {
userHeaders[header] = append(userHeaders[header], value)
} else {
userHeaders[header] = []string{value}
}
}
}
}
// Perform user header serialization and set them in the single header
h2 = append(h2, CreateSerializedHeaders(ResponseUserHeadersField, userHeaders)...)
return h2
}
// Serialize HTTP1.x headers by base64-encoding each header name and value,
// and then joining them in the format of [key:value;]
func SerializeHeaders(h1Headers http.Header) string {
var serializedHeaders []string
for headerName, headerValues := range h1Headers {
for _, headerValue := range headerValues {
encodedName := make([]byte, headerEncoding.EncodedLen(len(headerName)))
headerEncoding.Encode(encodedName, []byte(headerName))
encodedValue := make([]byte, headerEncoding.EncodedLen(len(headerValue)))
headerEncoding.Encode(encodedValue, []byte(headerValue))
serializedHeaders = append(
serializedHeaders,
strings.Join(
[]string{string(encodedName), string(encodedValue)},
":",
),
)
}
}
return strings.Join(serializedHeaders, ";")
}
// Deserialize headers serialized by `SerializeHeader`
func DeserializeHeaders(serializedHeaders string) ([]Header, error) {
const unableToDeserializeErr = "Unable to deserialize headers"
var deserialized []Header
for _, serializedPair := range strings.Split(serializedHeaders, ";") {
if len(serializedPair) == 0 {
continue
}
serializedHeaderParts := strings.Split(serializedPair, ":")
if len(serializedHeaderParts) != 2 {
return nil, errors.New(unableToDeserializeErr)
}
serializedName := serializedHeaderParts[0]
serializedValue := serializedHeaderParts[1]
deserializedName := make([]byte, headerEncoding.DecodedLen(len(serializedName)))
deserializedValue := make([]byte, headerEncoding.DecodedLen(len(serializedValue)))
if _, err := headerEncoding.Decode(deserializedName, []byte(serializedName)); err != nil {
return nil, errors.Wrap(err, unableToDeserializeErr)
}
if _, err := headerEncoding.Decode(deserializedValue, []byte(serializedValue)); err != nil {
return nil, errors.Wrap(err, unableToDeserializeErr)
}
deserialized = append(deserialized, Header{
Name: string(deserializedName),
Value: string(deserializedValue),
})
}
return deserialized, nil
}
func CreateSerializedHeaders(headersField string, headers ...http.Header) []Header {
var serializedHeaderChunks []string
for _, headerChunk := range headers {
serializedHeaderChunks = append(serializedHeaderChunks, SerializeHeaders(headerChunk))
}
return []Header{{
headersField,
strings.Join(serializedHeaderChunks, ";"),
}}
}
type ResponseMetaHeader struct {
Source string `json:"src"`
}
func CreateResponseMetaHeader(headerName, source string) Header {
jsonResponseMetaHeader, err := json.Marshal(ResponseMetaHeader{Source: source})
if err != nil {
panic(err)
}
return Header{
Name: headerName,
Value: string(jsonResponseMetaHeader),
}
}
+652
View File
@@ -0,0 +1,652 @@
package h2mux
import (
"fmt"
"math/rand"
"net/http"
"net/url"
"reflect"
"regexp"
"sort"
"strings"
"testing"
"testing/quick"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type ByName []Header
func (a ByName) Len() int { return len(a) }
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByName) Less(i, j int) bool {
if a[i].Name == a[j].Name {
return a[i].Value < a[j].Value
}
return a[i].Name < a[j].Name
}
func TestH2RequestHeadersToH1Request_RegularHeaders(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
assert.NoError(t, err)
mockHeaders := http.Header{
"Mock header 1": {"Mock value 1"},
"Mock header 2": {"Mock value 2"},
}
headersConversionErr := H2RequestHeadersToH1Request(CreateSerializedHeaders(RequestUserHeadersField, mockHeaders), request)
assert.True(t, reflect.DeepEqual(mockHeaders, request.Header))
assert.NoError(t, headersConversionErr)
}
func TestH2RequestHeadersToH1Request_NoHeaders(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
assert.NoError(t, err)
headersConversionErr := H2RequestHeadersToH1Request(
[]Header{{
RequestUserHeadersField,
SerializeHeaders(http.Header{}),
}},
request,
)
assert.True(t, reflect.DeepEqual(http.Header{}, request.Header))
assert.NoError(t, headersConversionErr)
}
func TestH2RequestHeadersToH1Request_InvalidHostPath(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
assert.NoError(t, err)
mockRequestHeaders := []Header{
{Name: ":path", Value: "//bad_path/"},
{Name: RequestUserHeadersField, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
}
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
assert.Equal(t, http.Header{
"Mock header": []string{"Mock value"},
}, request.Header)
assert.Equal(t, "http://example.com//bad_path/", request.URL.String())
assert.NoError(t, headersConversionErr)
}
func TestH2RequestHeadersToH1Request_HostPathWithQuery(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
assert.NoError(t, err)
mockRequestHeaders := []Header{
{Name: ":path", Value: "/?query=mock%20value"},
{Name: RequestUserHeadersField, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
}
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
assert.Equal(t, http.Header{
"Mock header": []string{"Mock value"},
}, request.Header)
assert.Equal(t, "http://example.com/?query=mock%20value", request.URL.String())
assert.NoError(t, headersConversionErr)
}
func TestH2RequestHeadersToH1Request_HostPathWithURLEncoding(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
assert.NoError(t, err)
mockRequestHeaders := []Header{
{Name: ":path", Value: "/mock%20path"},
{Name: RequestUserHeadersField, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
}
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
assert.Equal(t, http.Header{
"Mock header": []string{"Mock value"},
}, request.Header)
assert.Equal(t, "http://example.com/mock%20path", request.URL.String())
assert.NoError(t, headersConversionErr)
}
func TestH2RequestHeadersToH1Request_WeirdURLs(t *testing.T) {
type testCase struct {
path string
want string
}
testCases := []testCase{
{
path: "",
want: "",
},
{
path: "/",
want: "/",
},
{
path: "//",
want: "//",
},
{
path: "/test",
want: "/test",
},
{
path: "//test",
want: "//test",
},
{
// https://github.com/cloudflare/cloudflared/issues/81
path: "//test/",
want: "//test/",
},
{
path: "/%2Ftest",
want: "/%2Ftest",
},
{
path: "//%20test",
want: "//%20test",
},
{
// https://github.com/cloudflare/cloudflared/issues/124
path: "/test?get=somthing%20a",
want: "/test?get=somthing%20a",
},
{
path: "/%20",
want: "/%20",
},
{
// stdlib's EscapedPath() will always percent-encode ' '
path: "/ ",
want: "/%20",
},
{
path: "/ a ",
want: "/%20a%20",
},
{
path: "/a%20b",
want: "/a%20b",
},
{
path: "/foo/bar;param?query#frag",
want: "/foo/bar;param?query#frag",
},
{
// stdlib's EscapedPath() will always percent-encode non-ASCII chars
path: "/a␠b",
want: "/a%E2%90%A0b",
},
{
path: "/a-umlaut-ä",
want: "/a-umlaut-%C3%A4",
},
{
path: "/a-umlaut-%C3%A4",
want: "/a-umlaut-%C3%A4",
},
{
path: "/a-umlaut-%c3%a4",
want: "/a-umlaut-%c3%a4",
},
{
// here the second '#' is treated as part of the fragment
path: "/a#b#c",
want: "/a#b%23c",
},
{
path: "/a#b␠c",
want: "/a#b%E2%90%A0c",
},
{
path: "/a#b%20c",
want: "/a#b%20c",
},
{
path: "/a#b c",
want: "/a#b%20c",
},
{
// stdlib's EscapedPath() will always percent-encode '\'
path: "/\\",
want: "/%5C",
},
{
path: "/a\\",
want: "/a%5C",
},
{
path: "/a,b.c.",
want: "/a,b.c.",
},
{
path: "/.",
want: "/.",
},
{
// stdlib's EscapedPath() will always percent-encode '`'
path: "/a`",
want: "/a%60",
},
{
path: "/a[0]",
want: "/a[0]",
},
{
path: "/?a[0]=5 &b[]=",
want: "/?a[0]=5 &b[]=",
},
{
path: "/?a=%22b%20%22",
want: "/?a=%22b%20%22",
},
}
for index, testCase := range testCases {
requestURL := "https://example.com"
request, err := http.NewRequest(http.MethodGet, requestURL, nil)
assert.NoError(t, err)
mockRequestHeaders := []Header{
{Name: ":path", Value: testCase.path},
{Name: RequestUserHeadersField, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
}
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
assert.NoError(t, headersConversionErr)
assert.Equal(t,
http.Header{
"Mock header": []string{"Mock value"},
},
request.Header)
assert.Equal(t,
"https://example.com"+testCase.want,
request.URL.String(),
"Failed URL index: %v %#v", index, testCase)
}
}
func TestH2RequestHeadersToH1Request_QuickCheck(t *testing.T) {
config := &quick.Config{
Values: func(args []reflect.Value, rand *rand.Rand) {
args[0] = reflect.ValueOf(randomHTTP2Path(t, rand))
},
}
type testOrigin struct {
url string
expectedScheme string
expectedBasePath string
}
testOrigins := []testOrigin{
{
url: "http://origin.hostname.example.com:8080",
expectedScheme: "http",
expectedBasePath: "http://origin.hostname.example.com:8080",
},
{
url: "http://origin.hostname.example.com:8080/",
expectedScheme: "http",
expectedBasePath: "http://origin.hostname.example.com:8080",
},
{
url: "http://origin.hostname.example.com:8080/api",
expectedScheme: "http",
expectedBasePath: "http://origin.hostname.example.com:8080/api",
},
{
url: "http://origin.hostname.example.com:8080/api/",
expectedScheme: "http",
expectedBasePath: "http://origin.hostname.example.com:8080/api",
},
{
url: "https://origin.hostname.example.com:8080/api",
expectedScheme: "https",
expectedBasePath: "https://origin.hostname.example.com:8080/api",
},
}
// use multiple schemes to demonstrate that the URL is based on the
// origin's scheme, not the :scheme header
for _, testScheme := range []string{"http", "https"} {
for _, testOrigin := range testOrigins {
assertion := func(testPath string) bool {
const expectedMethod = "POST"
const expectedHostname = "request.hostname.example.com"
h2 := []Header{
{Name: ":method", Value: expectedMethod},
{Name: ":scheme", Value: testScheme},
{Name: ":authority", Value: expectedHostname},
{Name: ":path", Value: testPath},
{Name: RequestUserHeadersField, Value: ""},
}
h1, err := http.NewRequest("GET", testOrigin.url, nil)
require.NoError(t, err)
err = H2RequestHeadersToH1Request(h2, h1)
return assert.NoError(t, err) &&
assert.Equal(t, expectedMethod, h1.Method) &&
assert.Equal(t, expectedHostname, h1.Host) &&
assert.Equal(t, testOrigin.expectedScheme, h1.URL.Scheme) &&
assert.Equal(t, testOrigin.expectedBasePath+testPath, h1.URL.String())
}
err := quick.Check(assertion, config)
assert.NoError(t, err)
}
}
}
func randomASCIIPrintableChar(rand *rand.Rand) int {
// smallest printable ASCII char is 32, largest is 126
const startPrintable = 32
const endPrintable = 127
return startPrintable + rand.Intn(endPrintable-startPrintable)
}
// randomASCIIText generates an ASCII string, some of whose characters may be
// percent-encoded. Its "logical length" (ignoring percent-encoding) is
// between 1 and `maxLength`.
func randomASCIIText(rand *rand.Rand, minLength int, maxLength int) string {
length := minLength + rand.Intn(maxLength)
result := ""
for i := 0; i < length; i++ {
c := randomASCIIPrintableChar(rand)
// 1/4 chance of using percent encoding when not necessary
if c == '%' || rand.Intn(4) == 0 {
result += fmt.Sprintf("%%%02X", c)
} else {
result += string(c)
}
}
return result
}
// Calls `randomASCIIText` and ensures the result is a valid URL path,
// i.e. one that can pass unchanged through url.URL.String()
func randomHTTP1Path(t *testing.T, rand *rand.Rand, minLength int, maxLength int) string {
text := randomASCIIText(rand, minLength, maxLength)
re, err := regexp.Compile("[^/;,]*")
require.NoError(t, err)
return "/" + re.ReplaceAllStringFunc(text, url.PathEscape)
}
// Calls `randomASCIIText` and ensures the result is a valid URL query,
// i.e. one that can pass unchanged through url.URL.String()
func randomHTTP1Query(rand *rand.Rand, minLength int, maxLength int) string {
text := randomASCIIText(rand, minLength, maxLength)
return "?" + strings.ReplaceAll(text, "#", "%23")
}
// Calls `randomASCIIText` and ensures the result is a valid URL fragment,
// i.e. one that can pass unchanged through url.URL.String()
func randomHTTP1Fragment(t *testing.T, rand *rand.Rand, minLength int, maxLength int) string {
text := randomASCIIText(rand, minLength, maxLength)
u, err := url.Parse("#" + text)
require.NoError(t, err)
return u.String()
}
// Assemble a random :path pseudoheader that is legal by Go stdlib standards
// (i.e. all characters will satisfy "net/url".shouldEscape for their respective locations)
func randomHTTP2Path(t *testing.T, rand *rand.Rand) string {
result := randomHTTP1Path(t, rand, 1, 64)
if rand.Intn(2) == 1 {
result += randomHTTP1Query(rand, 1, 32)
}
if rand.Intn(2) == 1 {
result += randomHTTP1Fragment(t, rand, 1, 16)
}
return result
}
func stdlibHeaderToH2muxHeader(headers http.Header) (h2muxHeaders []Header) {
for name, values := range headers {
for _, value := range values {
h2muxHeaders = append(h2muxHeaders, Header{name, value})
}
}
return h2muxHeaders
}
func TestSerializeHeaders(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
assert.NoError(t, err)
mockHeaders := http.Header{
"Mock-Header-One": {"Mock header one value", "three"},
"Mock-Header-Two-Long": {"Mock header two value\nlong"},
":;": {":;", ";:"},
":": {":"},
";": {";"},
";;": {";;"},
"Empty values": {"", ""},
"": {"Empty key"},
"control\tcharacter\b\n": {"value\n\b\t"},
";\v:": {":\v;"},
}
for header, values := range mockHeaders {
for _, value := range values {
// Note that Golang's http library is opinionated;
// at this point every header name will be title-cased in order to comply with the HTTP RFC
// This means our proxy is not completely transparent when it comes to proxying headers
request.Header.Add(header, value)
}
}
serializedHeaders := SerializeHeaders(request.Header)
// Sanity check: the headers serialized to something that's not an empty string
assert.NotEqual(t, "", serializedHeaders)
// Deserialize back, and ensure we get the same set of headers
deserializedHeaders, err := DeserializeHeaders(serializedHeaders)
assert.NoError(t, err)
assert.Equal(t, 13, len(deserializedHeaders))
h2muxExpectedHeaders := stdlibHeaderToH2muxHeader(mockHeaders)
sort.Sort(ByName(deserializedHeaders))
sort.Sort(ByName(h2muxExpectedHeaders))
assert.True(
t,
reflect.DeepEqual(h2muxExpectedHeaders, deserializedHeaders),
fmt.Sprintf("got = %#v, want = %#v\n", deserializedHeaders, h2muxExpectedHeaders),
)
}
func TestSerializeNoHeaders(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
assert.NoError(t, err)
serializedHeaders := SerializeHeaders(request.Header)
deserializedHeaders, err := DeserializeHeaders(serializedHeaders)
assert.NoError(t, err)
assert.Equal(t, 0, len(deserializedHeaders))
}
func TestDeserializeMalformed(t *testing.T) {
var err error
malformedData := []string{
"malformed data",
"bW9jawo=", // "mock"
"bW9jawo=:ZGF0YQo=:bW9jawo=", // "mock:data:mock"
"::",
}
for _, malformedValue := range malformedData {
_, err = DeserializeHeaders(malformedValue)
assert.Error(t, err)
}
}
func TestParseHeaders(t *testing.T) {
mockUserHeadersToSerialize := http.Header{
"Mock-Header-One": {"1", "1.5"},
"Mock-Header-Two": {"2"},
"Mock-Header-Three": {"3"},
}
mockHeaders := []Header{
{Name: "One", Value: "1"},
{Name: "Cf-Two", Value: "cf-value-1"},
{Name: "Cf-Two", Value: "cf-value-2"},
{Name: RequestUserHeadersField, Value: SerializeHeaders(mockUserHeadersToSerialize)},
}
expectedHeaders := []Header{
{Name: "Mock-Header-One", Value: "1"},
{Name: "Mock-Header-One", Value: "1.5"},
{Name: "Mock-Header-Two", Value: "2"},
{Name: "Mock-Header-Three", Value: "3"},
}
parsedHeaders, err := ParseUserHeaders(RequestUserHeadersField, mockHeaders)
assert.NoError(t, err)
assert.ElementsMatch(t, expectedHeaders, parsedHeaders)
}
func TestParseHeadersNoSerializedHeader(t *testing.T) {
mockHeaders := []Header{
{Name: "One", Value: "1"},
{Name: "Cf-Two", Value: "cf-value-1"},
{Name: "Cf-Two", Value: "cf-value-2"},
}
_, err := ParseUserHeaders(RequestUserHeadersField, mockHeaders)
assert.EqualError(t, err, fmt.Sprintf("%s header not found", RequestUserHeadersField))
}
func TestIsControlHeader(t *testing.T) {
controlHeaders := []string{
// Anything that begins with cf-
"cf-sample-header",
"CF-SAMPLE-HEADER",
"Cf-Sample-Header",
// Any http2 pseudoheader
":sample-pseudo-header",
// content-length is a special case, it has to be there
// for some requests to work (per the HTTP2 spec)
"content-length",
}
for _, header := range controlHeaders {
assert.True(t, IsControlHeader(header))
}
}
func TestIsNotControlHeader(t *testing.T) {
notControlHeaders := []string{
"Mock-header",
"Another-sample-header",
}
for _, header := range notControlHeaders {
assert.False(t, IsControlHeader(header))
}
}
func TestH1ResponseToH2ResponseHeaders(t *testing.T) {
mockHeaders := http.Header{
"User-header-one": {""},
"User-header-two": {"1", "2"},
"cf-header": {"cf-value"},
"Content-Length": {"123"},
}
mockResponse := http.Response{
StatusCode: 200,
Header: mockHeaders,
}
headers := H1ResponseToH2ResponseHeaders(&mockResponse)
serializedHeadersIndex := -1
for i, header := range headers {
if header.Name == ResponseUserHeadersField {
serializedHeadersIndex = i
break
}
}
assert.NotEqual(t, -1, serializedHeadersIndex)
actualControlHeaders := append(
headers[:serializedHeadersIndex],
headers[serializedHeadersIndex+1:]...,
)
expectedControlHeaders := []Header{
{Name: ":status", Value: "200"},
{Name: "content-length", Value: "123"},
}
assert.ElementsMatch(t, expectedControlHeaders, actualControlHeaders)
actualUserHeaders, err := DeserializeHeaders(headers[serializedHeadersIndex].Value)
expectedUserHeaders := []Header{
{Name: "User-header-one", Value: ""},
{Name: "User-header-two", Value: "1"},
{Name: "User-header-two", Value: "2"},
}
assert.NoError(t, err)
assert.ElementsMatch(t, expectedUserHeaders, actualUserHeaders)
}
// The purpose of this test is to check that our code and the http.Header
// implementation don't throw validation errors about header size
func TestHeaderSize(t *testing.T) {
largeValue := randSeq(5 * 1024 * 1024) // 5Mb
largeHeaders := http.Header{
"User-header": {largeValue},
}
mockResponse := http.Response{
StatusCode: 200,
Header: largeHeaders,
}
serializedHeaders := H1ResponseToH2ResponseHeaders(&mockResponse)
request, err := http.NewRequest(http.MethodGet, "https://example.com/", nil)
assert.NoError(t, err)
for _, header := range serializedHeaders {
request.Header.Set(header.Name, header.Value)
}
for _, header := range serializedHeaders {
if header.Name != ResponseUserHeadersField {
continue
}
deserializedHeaders, err := DeserializeHeaders(header.Value)
assert.NoError(t, err)
assert.Equal(t, largeValue, deserializedHeaders[0].Value)
}
}
func randSeq(n int) string {
randomizer := rand.New(rand.NewSource(17))
var letters = []rune(":;,+/=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
b := make([]rune, n)
for i := range b {
b[i] = letters[randomizer.Intn(len(letters))]
}
return string(b)
}
+88 -12
View File
@@ -17,6 +17,12 @@ type ReadWriteClosedCloser interface {
Closed() bool
}
// MuxedStreamDataSignaller is a write-only *ReadyList
type MuxedStreamDataSignaller interface {
// Non-blocking: call this when data is ready to be sent for the given stream ID.
Signal(ID uint32)
}
// MuxedStream is logically an HTTP/2 stream, with an additional buffer for outgoing data.
type MuxedStream struct {
streamID uint32
@@ -55,8 +61,8 @@ type MuxedStream struct {
// This is the amount of bytes that are in the peer's receive window
// (how much data we can send from this stream).
sendWindow uint32
// Reference to the muxer's readyList; signal this for stream data to be sent.
readyList *ReadyList
// The muxer's readyList
readyList MuxedStreamDataSignaller
// The headers that should be sent, and a flag so we only send them once.
headersSent bool
writeHeaders []Header
@@ -68,7 +74,8 @@ type MuxedStream struct {
sentEOF bool
// true if the peer sent us an EOF
receivedEOF bool
// If valid, tunnelHostname is used to identify which origin service is the intended recipient of the request
tunnelHostname TunnelHostname
// Compression-related fields
receivedUseDict bool
method string
@@ -77,6 +84,33 @@ type MuxedStream struct {
dictionaries h2Dictionaries
}
type TunnelHostname string
func (th TunnelHostname) String() string {
return string(th)
}
func (th TunnelHostname) IsSet() bool {
return th != ""
}
func NewStream(config MuxerConfig, writeHeaders []Header, readyList MuxedStreamDataSignaller, dictionaries h2Dictionaries) *MuxedStream {
return &MuxedStream{
responseHeadersReceived: make(chan struct{}),
readBuffer: NewSharedBuffer(),
writeBuffer: &bytes.Buffer{},
writeBufferMaxLen: config.StreamWriteBufferMaxLen,
writeBufferHasSpace: make(chan struct{}, 1),
receiveWindow: config.DefaultWindowSize,
receiveWindowCurrentMax: config.DefaultWindowSize,
receiveWindowMax: config.MaxWindowSize,
sendWindow: config.DefaultWindowSize,
readyList: readyList,
writeHeaders: writeHeaders,
dictionaries: dictionaries,
}
}
func (s *MuxedStream) Read(p []byte) (n int, err error) {
var readBuffer ReadWriteClosedCloser
if s.dictionaries.read != nil {
@@ -109,9 +143,10 @@ func (s *MuxedStream) Write(p []byte) (int, error) {
// If the buffer is full, block till there is more room.
// Use a loop to recheck the buffer size after the lock is reacquired.
for s.writeBufferMaxLen <= s.writeBuffer.Len() {
s.writeLock.Unlock()
<-s.writeBufferHasSpace
s.writeLock.Lock()
s.awaitWriteBufferHasSpace()
if s.writeEOF {
return totalWritten, io.EOF
}
}
amountToWrite := len(p) - totalWritten
spaceAvailable := s.writeBufferMaxLen - s.writeBuffer.Len()
@@ -160,10 +195,19 @@ func (s *MuxedStream) CloseWrite() error {
if c, ok := s.writeBuffer.(io.Closer); ok {
c.Close()
}
// Allow MuxedStream::Write() to terminate its loop with err=io.EOF, if needed
s.notifyWriteBufferHasSpace()
// We need to send something over the wire, even if it's an END_STREAM with no data
s.writeNotify()
return nil
}
func (s *MuxedStream) WriteClosed() bool {
s.writeLock.Lock()
defer s.writeLock.Unlock()
return s.writeEOF
}
func (s *MuxedStream) WriteHeaders(headers []Header) error {
s.writeLock.Lock()
defer s.writeLock.Unlock()
@@ -185,6 +229,42 @@ func (s *MuxedStream) WriteHeaders(headers []Header) error {
return nil
}
// IsRPCStream returns if the stream is used to transport RPC.
func (s *MuxedStream) IsRPCStream() bool {
rpcHeaders := RPCHeaders()
if len(s.Headers) != len(rpcHeaders) {
return false
}
// The headers order matters, so RPC stream should be opened with OpenRPCStream method and let MuxWriter serializes the headers.
for i, rpcHeader := range rpcHeaders {
if s.Headers[i] != rpcHeader {
return false
}
}
return true
}
func (s *MuxedStream) TunnelHostname() TunnelHostname {
return s.tunnelHostname
}
// Block until a value is sent on writeBufferHasSpace.
// Must be called while holding writeLock
func (s *MuxedStream) awaitWriteBufferHasSpace() {
s.writeLock.Unlock()
<-s.writeBufferHasSpace
s.writeLock.Lock()
}
// Send a value on writeBufferHasSpace without blocking.
// Must be called while holding writeLock
func (s *MuxedStream) notifyWriteBufferHasSpace() {
select {
case s.writeBufferHasSpace <- struct{}{}:
default:
}
}
func (s *MuxedStream) getReceiveWindow() uint32 {
s.writeLock.Lock()
defer s.writeLock.Unlock()
@@ -304,17 +384,13 @@ func (s *MuxedStream) getChunk() *streamChunk {
sendData: !s.sentEOF,
eof: s.writeEOF && uint32(s.writeBuffer.Len()) <= s.sendWindow,
}
// Copy at most s.sendWindow bytes, adjust the sendWindow accordingly
writeLen, _ := io.CopyN(&chunk.buffer, s.writeBuffer, int64(s.sendWindow))
s.sendWindow -= uint32(writeLen)
// Non-blocking channel send. This will allow MuxedStream::Write() to continue, if needed
// Allow MuxedStream::Write() to continue, if needed
if s.writeBuffer.Len() < s.writeBufferMaxLen {
select {
case s.writeBufferHasSpace <- struct{}{}:
default:
}
s.notifyWriteBufferHasSpace()
}
// When we write the chunk, we'll write the WINDOW_UPDATE frame if needed

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