Compare commits

..

86 Commits

Author SHA1 Message Date
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
Nick Vollmar 980bee22a5 Release 2019.4.0 2019-04-03 14:31:34 -05:00
Chung-Ting Huang 80d3d989eb TUN-1673: Conflate Hello and Connect RPCs 2019-04-01 13:50:30 -05:00
Adam Chalmers 6804a5ff9d TUN-1648: ConnectionID is now a UUID 2019-03-28 15:03:30 -05:00
1231 changed files with 192436 additions and 27789 deletions
+2
View File
@@ -9,3 +9,5 @@ guide/public
\#*\#
cscope.*
cloudflared
!cmd/cloudflared/
.DS_Store
+12
View File
@@ -0,0 +1,12 @@
FROM golang:1.12 as builder
WORKDIR /go/src/github.com/cloudflare/cloudflared/
RUN apt-get update && apt-get install -y --no-install-recommends upx
# Run after `apt-get update` to improve rebuild scenarios
COPY . .
RUN make cloudflared
RUN upx --no-progress cloudflared
FROM gcr.io/distroless/base
COPY --from=builder /go/src/github.com/cloudflare/cloudflared/cloudflared /usr/local/bin/
ENTRYPOINT ["cloudflared", "--no-autoupdate"]
CMD ["version"]
Generated
+205 -118
View File
@@ -2,20 +2,73 @@
[[projects]]
digest = "1:b16fbfbcc20645cb419f78325bb2e85ec729b338e996a228124d68931a6f2a37"
digest = "1:9f3b30d9f8e0d7040f729b82dcbc8f0dead820a133b3147ce355fc451f32d761"
name = "github.com/BurntSushi/toml"
packages = ["."]
pruneopts = "UT"
revision = "b26d9c308763d68093482582cea63d69be07a0f0"
version = "v0.3.0"
revision = "3012a1dbe2e4bd1391d42b32f0577cb7bbc7f005"
version = "v0.3.1"
[[projects]]
branch = "master"
digest = "1:1a200e7e73293b75eb8e5c93d023b5472663432da0b663e1532624fcfede9ca8"
name = "github.com/anmitsu/go-shlex"
packages = ["."]
pruneopts = "UT"
revision = "648efa622239a2f6ff949fed78ee37b48d499ba4"
[[projects]]
digest = "1:4a296b8f487dab7ea37c4558d6e84be8103eb6bd297202e870266ff99ebd769d"
name = "github.com/aws/aws-sdk-go"
packages = [
"aws",
"aws/awserr",
"aws/awsutil",
"aws/client",
"aws/client/metadata",
"aws/corehandlers",
"aws/credentials",
"aws/credentials/ec2rolecreds",
"aws/credentials/endpointcreds",
"aws/credentials/processcreds",
"aws/credentials/stscreds",
"aws/csm",
"aws/defaults",
"aws/ec2metadata",
"aws/endpoints",
"aws/request",
"aws/session",
"aws/signer/v4",
"internal/ini",
"internal/s3err",
"internal/sdkio",
"internal/sdkrand",
"internal/sdkuri",
"internal/shareddefaults",
"private/protocol",
"private/protocol/eventstream",
"private/protocol/eventstream/eventstreamapi",
"private/protocol/json/jsonutil",
"private/protocol/query",
"private/protocol/query/queryutil",
"private/protocol/rest",
"private/protocol/restxml",
"private/protocol/xml/xmlutil",
"service/s3",
"service/sts",
"service/sts/stsiface",
]
pruneopts = "UT"
revision = "2eb9a651f22769d792f974e8a8efe16afea3370f"
version = "v1.23.9"
[[projects]]
digest = "1:d6afaeed1502aa28e80a4ed0981d570ad91b2579193404256ce672ed0a609e0d"
name = "github.com/beorn7/perks"
packages = ["quantile"]
pruneopts = "UT"
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
revision = "4b2b341e8d7715fae06375aa633dbb6e91b3fb46"
version = "v1.0.0"
[[projects]]
digest = "1:fed1f537c2f1269fe475a8556c393fe466641682d73ef8fd0491cd3aa1e47bad"
@@ -26,7 +79,6 @@
version = "2018.01.18"
[[projects]]
branch = "master"
digest = "1:e5003c19d396d8b3cf1324ea0bf49b00f13e9466d0297d1268b641f1c617c3a2"
name = "github.com/cloudflare/brotli-go"
packages = ["."]
@@ -34,7 +86,6 @@
revision = "18c9f6c67e3dfc12e0ddaca748d2887f97a7ac28"
[[projects]]
branch = "master"
digest = "1:6dbb2bbc7e6333e691c4d82fd86485f0695a35902fbb9b2df5f72e22ab0040f3"
name = "github.com/cloudflare/golibs"
packages = ["lrucache"]
@@ -42,8 +93,7 @@
revision = "333127dbecfcc23a8db7d9a4f52785d23aff44a1"
[[projects]]
branch = "master"
digest = "1:af68c8bdac3ceaf46c67f0c43cc1ee5f10e705d8e04f8a63fa41231d0be6b0fb"
digest = "1:3f9506ee991cdee1f05bf0cd3e34b5cd922dc00d6a950fb4beb4e07ab1c4d3d1"
name = "github.com/coredns/coredns"
packages = [
"core/dnsserver",
@@ -72,19 +122,13 @@
"request",
]
pruneopts = "UT"
revision = "992e7928c7c258628d2b13b769acc86781b9faea"
revision = "2e322f6e8a54f18c6aef9c25a7c432c291a3d9f7"
version = "v1.2.0"
[[projects]]
branch = "master"
digest = "1:6a503e232df389d94ebb97dfb22d4ae463b6e2f351660613e11d9e42f57ab6df"
digest = "1:6f70106e7bc1c803e8a0a4519e09c12d154771acfa2559206e97b033bbd1dd38"
name = "github.com/coreos/go-oidc"
packages = [
"http",
"jose",
"key",
"oauth2",
"oidc",
]
packages = ["jose"]
pruneopts = "UT"
revision = "a93f71fdfe73d2c0f5413c0565eea0af6523a6df"
@@ -93,28 +137,24 @@
name = "github.com/coreos/go-systemd"
packages = ["daemon"]
pruneopts = "UT"
revision = "39ca1b05acc7ad1220e09f133283b8859a8b71ab"
version = "v17"
revision = "95778dfbb74eb7e4dbaf43bf7d71809650ef8076"
version = "v19"
[[projects]]
digest = "1:6fda0d7f5e52b081e075775b1ecebf1ea0c923e7be33604ed0225ae078e701b5"
name = "github.com/coreos/pkg"
packages = [
"health",
"httputil",
"timeutil",
]
digest = "1:385dff5bf83bf1c9d40ef2b0f2e697f0d1973fc9e24ae9401cf41698612bbebc"
name = "github.com/creack/pty"
packages = ["."]
pruneopts = "UT"
revision = "97fdf19511ea361ae1c100dd393cc47f8dcfa1e1"
version = "v4"
revision = "2769f65a3a94eb8f876f44a0459d24ae7ad2e488"
version = "v1.1.7"
[[projects]]
digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39"
digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec"
name = "github.com/davecgh/go-spew"
packages = ["spew"]
pruneopts = "UT"
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73"
version = "v1.1.1"
[[projects]]
branch = "master"
@@ -125,8 +165,7 @@
revision = "027aa4915315a0b2825c0f025cea347829b974fa"
[[projects]]
branch = "master"
digest = "1:15eadee5930d325154e5e1f3c4f4b28ab62f9c0aa949580e8d9edabaf0b64e85"
digest = "1:d4268b2a09b1f736633577c4ac93f2a5356c73742fff5344e2451aeec60a7ad0"
name = "github.com/equinox-io/equinox"
packages = [
".",
@@ -137,10 +176,10 @@
"proto",
]
pruneopts = "UT"
revision = "f24972fa72facf59d05c91c848b65eac38815915"
revision = "5205c98a6c11dc72747ce12fff6cd620a99fde05"
version = "v1.2.0"
[[projects]]
branch = "master"
digest = "1:433763f10d88dba9b533a7ea2fe9f5ee11e57e00306eb97a1f6090fd978e8fa1"
name = "github.com/facebookgo/grace"
packages = ["gracenet"]
@@ -156,13 +195,20 @@
revision = "3f9db97f856818214da2e1057f8ad84803971cff"
[[projects]]
branch = "master"
digest = "1:d4623fc7bf7e281d9107367cc4a9e76ed3e86b1eec1a4e30630c870bef1fedd0"
name = "github.com/getsentry/raven-go"
packages = ["."]
pruneopts = "UT"
revision = "ed7bcb39ff10f39ab08e317ce16df282845852fa"
[[projects]]
digest = "1:0a01355005024757ae2a1f62e8fe68a30b4f5fde7a08ac88b3685e7999ac354b"
name = "github.com/gliderlabs/ssh"
packages = ["."]
pruneopts = "UT"
revision = "5b6cc7030f17095c0cf23bb063b0bfe824fe5f8b"
version = "v0.2.2"
[[projects]]
branch = "master"
digest = "1:3e6afc3ed8a72949aa735c00fddc23427dc9384ccfd51cf0d91a412e668da632"
@@ -172,7 +218,7 @@
revision = "604e922904d35e97f98a774db7881f049cd8d970"
[[projects]]
digest = "1:17fe264ee908afc795734e8c4e63db2accabaf57326dbf21763a7d6b86096260"
digest = "1:239c4c7fd2159585454003d9be7207167970194216193a8a210b8d29576f19c9"
name = "github.com/golang/protobuf"
packages = [
"proto",
@@ -182,8 +228,8 @@
"ptypes/timestamp",
]
pruneopts = "UT"
revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265"
version = "v1.1.0"
revision = "b5d812f8a3706043e23a9cd5babf2e5423744d30"
version = "v1.3.1"
[[projects]]
digest = "1:582b704bebaa06b48c29b0cec224a6058a09c86883aaddabde889cd1a5f73e1b"
@@ -194,20 +240,12 @@
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"
digest = "1:d5f97fc268267ec1b61c3453058c738246fc3e746f14b1ae25161513b7367b0c"
name = "github.com/gorilla/mux"
packages = ["."]
pruneopts = "UT"
revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf"
version = "v1.6.2"
revision = "c5c6c98bc25355028a63748a498942a6398ccd22"
version = "v1.7.1"
[[projects]]
digest = "1:43dd08a10854b2056e615d1b1d22ac94559d822e1f8b6fcc92c1a1057e85188e"
@@ -226,39 +264,58 @@
revision = "8e809c8a86450a29b90dcc9efbf062d0fe6d9746"
[[projects]]
digest = "1:75ab90ae3f5d876167e60f493beadfe66f0ed861a710f283fb06c86437a09538"
name = "github.com/jonboulle/clockwork"
digest = "1:bb81097a5b62634f3e9fec1014657855610c82d19b9a40c17612e32651e35dca"
name = "github.com/jmespath/go-jmespath"
packages = ["."]
pruneopts = "UT"
revision = "2eee05ed794112d45db504eb05aa693efd2b8b09"
version = "v0.1.0"
revision = "c2b33e84"
[[projects]]
branch = "master"
digest = "1:37ce7d7d80531b227023331002c0d42b4b4b291a96798c82a049d03a54ba79e4"
digest = "1:6c41d4f998a03b6604227ccad36edaed6126c397e5d78709ef4814a1145a6757"
name = "github.com/jmoiron/sqlx"
packages = [
".",
"reflectx",
]
pruneopts = "UT"
revision = "d161d7a76b5661016ad0b085869f77fd410f3e6a"
version = "v1.2.0"
[[projects]]
digest = "1:31e761d97c76151dde79e9d28964a812c46efc5baee4085b86f68f0c654450de"
name = "github.com/konsorten/go-windows-terminal-sequences"
packages = ["."]
pruneopts = "UT"
revision = "f55edac94c9bbba5d6182a4be46d86a2c9b5b50e"
version = "v1.0.2"
[[projects]]
digest = "1:bc1c0be40c67b6b4aee09d7508d5a2a52c1c116b1fa43806dad2b0d6b4d4003b"
name = "github.com/lib/pq"
packages = [
".",
"oid",
"scram",
]
pruneopts = "UT"
revision = "90697d60dd844d5ef6ff15135d0203f65d2f53b8"
revision = "51e2106eed1cea199c802d2a49e91e2491b02056"
version = "v1.1.0"
[[projects]]
digest = "1:c658e84ad3916da105a761660dcaeb01e63416c8ec7bc62256a9b411a05fcd67"
digest = "1:2fa7b0155cd54479a755c629de26f888a918e13f8857a2c442205d825368e084"
name = "github.com/mattn/go-colorable"
packages = ["."]
pruneopts = "UT"
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
version = "v0.0.9"
revision = "3a70a971f94a22f2fa562ffcc7a0eb45f5daf045"
version = "v0.1.1"
[[projects]]
digest = "1:d4d17353dbd05cb52a2a52b7fe1771883b682806f68db442b436294926bbfafb"
digest = "1:e150b5fafbd7607e2d638e4e5cf43aa4100124e5593385147b0a74e2733d8b0d"
name = "github.com/mattn/go-isatty"
packages = ["."]
pruneopts = "UT"
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
version = "v0.0.3"
revision = "c2a7a6ca930a4cd0bc33a3f298eb71960732a3a7"
version = "v0.0.7"
[[projects]]
digest = "1:ff5ebae34cfbf047d505ee150de27e60570e8c394b3b8fdbb720ff6ac71985fc"
@@ -269,7 +326,6 @@
version = "v1.0.1"
[[projects]]
branch = "master"
digest = "1:75fa16a231ef40da3e462d651c20b9df20bde0777bdc1ac0982242c79057ee71"
name = "github.com/mholt/caddy"
packages = [
@@ -281,25 +337,31 @@
revision = "d3b731e9255b72d4571a5aac125634cf1b6031dc"
[[projects]]
digest = "1:73f788f2df39614dd7d9e75c8ca5fb8b4a68937b5e6d4a15e4c3a596f6f8c07b"
digest = "1:2b4b4b2e5544c2a11a486c1b631357aa2ddf766e50c1b2483cf809da2c511234"
name = "github.com/miekg/dns"
packages = [
".",
"internal/socket",
]
packages = ["."]
pruneopts = "UT"
revision = "6da3249dfb57fbaa16efafcd8744cee8809d80cd"
revision = "73601d4aed9d844322611759d7f3619110b7c88e"
version = "v1.1.8"
[[projects]]
branch = "master"
digest = "1:8eb17c2ec4df79193ae65b621cd1c0c4697db3bc317fe6afdc76d7f2746abd05"
digest = "1:5d231480e1c64a726869bc4142d270184c419749d34f167646baa21008eb0a79"
name = "github.com/mitchellh/go-homedir"
packages = ["."]
pruneopts = "UT"
revision = "3864e76763d94a6df2f9960b16a20a33da9f9a66"
revision = "af06845cf3004701891bf4fdb884bfe4920b3727"
version = "v1.1.0"
[[projects]]
digest = "1:450b7623b185031f3a456801155c8320209f75d0d4c4e633c6b1e59d44d6e392"
digest = "1:53bc4cd4914cd7cd52139990d5170d6dc99067ae31c56530621b18b35fc30318"
name = "github.com/mitchellh/mapstructure"
packages = ["."]
pruneopts = "UT"
revision = "3536a929edddb9a5b34bd6861dc4a9647cb459fe"
version = "v1.1.2"
[[projects]]
digest = "1:11e62d6050198055e6cd87ed57e5d8c669e84f839c16e16f192374d913d1a70d"
name = "github.com/opentracing/opentracing-go"
packages = [
".",
@@ -307,8 +369,8 @@
"log",
]
pruneopts = "UT"
revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38"
version = "v1.0.2"
revision = "659c90643e714681897ec2521c60567dd21da733"
version = "v1.1.0"
[[projects]]
digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747"
@@ -339,15 +401,14 @@
[[projects]]
branch = "master"
digest = "1:32d10bdfa8f09ecf13598324dba86ab891f11db3c538b6a34d1c3b5b99d7c36b"
digest = "1:2d5cd61daa5565187e1d96bae64dbbc6080dacf741448e9629c64fd93203b0d4"
name = "github.com/prometheus/client_model"
packages = ["go"]
pruneopts = "UT"
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
revision = "fd36f4220a901265f90734c3183c5f0c91daa0b8"
[[projects]]
branch = "master"
digest = "1:e469cd65badf7694aeb44874518606d93c1d59e7735d3754ad442782437d3cc3"
digest = "1:35cf6bdf68db765988baa9c4f10cc5d7dda1126a54bd62e252dbcd0b1fc8da90"
name = "github.com/prometheus/common"
packages = [
"expfmt",
@@ -355,82 +416,90 @@
"model",
]
pruneopts = "UT"
revision = "7600349dcfe1abd18d72d3a1770870d9800a7801"
revision = "a82f4c12f983cc2649298185f296632953e50d3e"
version = "v0.3.0"
[[projects]]
branch = "master"
digest = "1:20d9bb50dbee172242f9bcd6ec24a917dd7a5bb17421bf16a79c33111dea7db1"
digest = "1:49b09905e781d7775c086604cc00083e1832d0783f1f421b79f42657c457d029"
name = "github.com/prometheus/procfs"
packages = [
".",
"internal/util",
"nfs",
"xfs",
]
packages = ["."]
pruneopts = "UT"
revision = "ae68e2d4c00fed4943b5f6698d504a5fe083da8a"
revision = "8368d24ba045f26503eb745b624d930cbe214c79"
[[projects]]
digest = "1:9a6f766efd8d5752adb7052aebb6e3d85255b31a8dff5e58ab4efa740ba9efa0"
digest = "1:1a23fdd843129ef761ffe7651bc5fe7c5b09fbe933e92783ab06cc11c37b7b37"
name = "github.com/rifflock/lfshook"
packages = ["."]
pruneopts = "UT"
revision = "bf539943797a1f34c1f502d07de419b5238ae6c6"
version = "v2.3"
revision = "b9218ef580f59a2e72dad1aa33d660150445d05a"
version = "v2.4"
[[projects]]
digest = "1:9e9193aa51197513b3abcb108970d831fbcf40ef96aa845c4f03276e1fa316d2"
digest = "1:31538f8d774e8bf2fb9380d2d2a82d69e206a7d0603946586926f1819c546c26"
name = "github.com/sirupsen/logrus"
packages = ["."]
packages = [
".",
"hooks/test",
]
pruneopts = "UT"
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
version = "v1.0.5"
revision = "839c75faf7f98a33d445d181f3018b5c3409a45e"
version = "v1.4.2"
[[projects]]
digest = "1:18752d0b95816a1b777505a97f71c7467a8445b8ffb55631a7bf779f6ba4fa83"
digest = "1:7e8d267900c7fa7f35129a2a37596e38ed0f11ca746d6d9ba727980ee138f9f6"
name = "github.com/stretchr/testify"
packages = ["assert"]
packages = [
"assert",
"require",
]
pruneopts = "UT"
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
version = "v1.2.2"
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
version = "v1.2.1"
[[projects]]
branch = "master"
digest = "1:1182d12da264c8fe593504f150c3a479297767991fc1c06796cb67e4f61c4366"
digest = "1:a84d5ec8b40a827962ea250f2cf03434138ccae9d83fcac12fb49b70c70b80cc"
name = "golang.org/x/crypto"
packages = [
"curve25519",
"ed25519",
"ed25519/internal/edwards25519",
"internal/chacha20",
"internal/subtle",
"nacl/box",
"nacl/secretbox",
"poly1305",
"salsa20/salsa",
"ssh",
"ssh/terminal",
]
pruneopts = "UT"
revision = "a49355c7e3f8fe157a85be2f77e6e269a0f89602"
revision = "f416ebab96af27ca70b6e5c23d6a0747530da626"
[[projects]]
branch = "master"
digest = "1:9513f8a0f1f6918181b7d744225b1931ffacafeb57067a1c26893a51058afb80"
digest = "1:52d140f7ab52e491cc1cbc93e6637aa5e9a7f3beae7545d675b02e52ca9d7290"
name = "golang.org/x/net"
packages = [
"bpf",
"context",
"http/httpguts",
"http2",
"http2/hpack",
"idna",
"internal/iana",
"internal/socket",
"internal/timeseries",
"ipv4",
"ipv6",
"trace",
"websocket",
]
pruneopts = "UT"
revision = "32a936f46389aa10549d60bd7833e54b01685d09"
revision = "1da14a5a36f220ea3f03470682b737b1dfd5de22"
[[projects]]
branch = "master"
digest = "1:39ebcc2b11457b703ae9ee2e8cca0f68df21969c6102cb3b705f76cca0ea0239"
name = "golang.org/x/sync"
packages = ["errgroup"]
@@ -439,9 +508,10 @@
[[projects]]
branch = "master"
digest = "1:2e8e74aa73847379a370c2704ff08baf4a7303ae682766e70c23ded3fd9b3f37"
digest = "1:77751d02e939d7078faedaeec10c09af575a09c528d84d18f2cb45a84bd1889a"
name = "golang.org/x/sys"
packages = [
"cpu",
"unix",
"windows",
"windows/registry",
@@ -450,7 +520,7 @@
"windows/svc/mgr",
]
pruneopts = "UT"
revision = "ce36f3865eeb42541ce3f87f32f8462c5687befa"
revision = "12500544f89f9420afe9529ba8940bf72d294972"
[[projects]]
digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18"
@@ -477,30 +547,38 @@
[[projects]]
branch = "master"
digest = "1:601e63e7d4577f907118bec825902505291918859d223bce015539e79f1160e3"
digest = "1:c3076e7defee87de1236f1814beb588f40a75544c60121e6eb38b3b3721783e2"
name = "google.golang.org/genproto"
packages = ["googleapis/rpc/status"]
pruneopts = "UT"
revision = "ff3583edef7de132f219f0efc00e097cabcc0ec0"
revision = "d1146b9035b912113a38af3b138eb2af567b2c67"
[[projects]]
digest = "1:2dab32a43451e320e49608ff4542fdfc653c95dcc35d0065ec9c6c3dd540ed74"
digest = "1:31d87f39886fb38a2b6c097ff3b9f985d6960772170d64a68246f7790e955746"
name = "google.golang.org/grpc"
packages = [
".",
"balancer",
"balancer/base",
"balancer/roundrobin",
"binarylog/grpc_binarylog_v1",
"codes",
"connectivity",
"credentials",
"credentials/internal",
"encoding",
"encoding/proto",
"grpclog",
"internal",
"internal/backoff",
"internal/balancerload",
"internal/binarylog",
"internal/channelz",
"internal/envconfig",
"internal/grpcrand",
"internal/grpcsync",
"internal/syscall",
"internal/transport",
"keepalive",
"metadata",
"naming",
@@ -511,11 +589,10 @@
"stats",
"status",
"tap",
"transport",
]
pruneopts = "UT"
revision = "168a6198bcb0ef175f7dacec0b8691fc141dc9b8"
version = "v1.13.0"
revision = "236199dd5f8031d698fb64091194aecd1c3895b2"
version = "v1.20.0"
[[projects]]
branch = "altsrc-parse-durations"
@@ -530,12 +607,12 @@
source = "https://github.com/cbranch/cli"
[[projects]]
digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202"
digest = "1:4d2e5a73dc1500038e504a8d78b986630e3626dc027bc030ba5c75da257cdb96"
name = "gopkg.in/yaml.v2"
packages = ["."]
pruneopts = "UT"
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
version = "v2.2.1"
revision = "51d6538a90f86fe93ac480b35f37b2be17fef232"
version = "v2.2.2"
[[projects]]
digest = "1:8ffc3ddc31414c0a71220957bb723b16510d7fcb5b3880dc0da4cf6d39c31642"
@@ -565,6 +642,11 @@
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/DATA-DOG/go-sqlmock",
"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",
"github.com/cloudflare/brotli-go",
"github.com/cloudflare/golibs/lrucache",
"github.com/coredns/coredns/core/dnsserver",
@@ -575,12 +657,13 @@
"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/creack/pty",
"github.com/elgs/gosqljson",
"github.com/equinox-io/equinox",
"github.com/facebookgo/grace/gracenet",
"github.com/getsentry/raven-go",
"github.com/gliderlabs/ssh",
"github.com/golang-collections/collections/queue",
"github.com/google/uuid",
"github.com/gorilla/mux",
@@ -589,13 +672,17 @@
"github.com/mattn/go-colorable",
"github.com/miekg/dns",
"github.com/mitchellh/go-homedir",
"github.com/mitchellh/mapstructure",
"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/sirupsen/logrus/hooks/test",
"github.com/stretchr/testify/assert",
"github.com/stretchr/testify/require",
"golang.org/x/crypto/nacl/box",
"golang.org/x/crypto/ssh",
"golang.org/x/crypto/ssh/terminal",
"golang.org/x/net/context",
"golang.org/x/net/http2",
+37 -17
View File
@@ -10,35 +10,35 @@
[[constraint]]
name = "github.com/facebookgo/grace"
branch = "master"
revision = "75cf19382434e82df4dd84953f566b8ad23d6e9e"
[[constraint]]
name = "github.com/getsentry/raven-go"
branch = "master"
revision = "ed7bcb39ff10f39ab08e317ce16df282845852fa"
[[constraint]]
name = "github.com/pkg/errors"
version = "0.8.0"
version = "=0.8.0"
[[constraint]]
name = "github.com/prometheus/client_golang"
version = "0.9.0-pre1"
version = "=0.9.0-pre1"
[[constraint]]
name = "github.com/sirupsen/logrus"
version = "1.0.3"
version = "=1.4.2"
[[constraint]]
name = "github.com/stretchr/testify"
version = "1.2.1"
version = "=1.2.1"
[[constraint]]
name = "golang.org/x/net"
branch = "master"
branch = "master" # master required by github.com/miekg/dns
[[constraint]]
name = "golang.org/x/sync"
branch = "master"
revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
[[constraint]]
name = "gopkg.in/urfave/cli.v2"
@@ -48,36 +48,56 @@
[[constraint]]
name = "zombiezen.com/go/capnproto2"
source = "https://github.com/zombiezen/go-capnproto2"
version = "2.17.1"
version = "=2.17.1"
[[constraint]]
name = "github.com/gorilla/websocket"
version = "1.2.0"
version = "=1.2.0"
[[constraint]]
name = "github.com/coredns/coredns"
branch = "master"
version = "=1.2.0"
[[constraint]]
name = "github.com/miekg/dns"
revision = "6da3249dfb57fbaa16efafcd8744cee8809d80cd"
version = "=1.1.8"
[[constraint]]
name = "github.com/cloudflare/brotli-go"
branch = "master"
revision = "18c9f6c67e3dfc12e0ddaca748d2887f97a7ac28"
[[override]]
name = "github.com/mholt/caddy"
branch = "master"
revision = "d3b731e9255b72d4571a5aac125634cf1b6031dc"
[[constraint]]
branch = "master"
name = "github.com/coreos/go-oidc"
revision = "a93f71fdfe73d2c0f5413c0565eea0af6523a6df"
[[constraint]]
branch = "master"
name = "golang.org/x/crypto"
branch = "master" # master required by github.com/miekg/dns
[[constraint]]
branch = "master"
name = "github.com/cloudflare/golibs"
revision = "333127dbecfcc23a8db7d9a4f52785d23aff44a1"
[[constraint]]
name = "github.com/google/uuid"
version = "=1.1.1"
[[constraint]]
name = "github.com/mitchellh/mapstructure"
version = "1.1.2"
[[constraint]]
name = "github.com/gliderlabs/ssh"
version = "0.2.2"
[[constraint]]
name = "github.com/creack/pty"
version = "1.1.7"
[[constraint]]
name = "github.com/aws/aws-sdk-go"
version = "1.23.9"
+20 -6
View File
@@ -28,11 +28,15 @@ clean:
go clean
.PHONY: cloudflared
cloudflared:
cloudflared: tunnel-deps
go build -v $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/cloudflared
.PHONY: container
container:
docker build -t cloudflare/cloudflared:"$(VERSION)" .
.PHONY: test
test:
test: vet
go test -v -race $(VERSION_FLAGS) ./...
.PHONY: cloudflared-deb
@@ -51,8 +55,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 +69,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 ./...
which go-sumtype # go get github.com/BurntSushi/go-sumtype
go-sumtype $$(go list ./...)
+89
View File
@@ -1,3 +1,92 @@
2019.9.0
- 2019-09-05 TUN-2279: Revert path encoding fix
- 2019-08-30 AUTH-2021 - check error for failing tests
- 2019-08-29 AUTH-2030: Support both authorized_key and short lived cert authentication simultaniously without specifiying at start time
- 2019-08-29 AUTH-2026: Adds support for non-pty sessions and inline command exec
- 2019-08-26 AUTH-1943: Adds session logging
- 2019-08-26 TUN-2162: Decomplect OpenStream to allow finer-grained timeouts
- 2019-08-29 TUN-2260: add name/group to CapnpConnectParameters, remove Scope
2019.8.4
- 2019-08-30 Fix #111: Add support for specifying a specific HTTP Host: header on the origin. (#114)
- 2019-08-22 TUN-2165: Add ClientConfig to tunnelrpc.ConnectResult
- 2019-08-20 AUTH-2014: Checks users login shell
- 2019-08-26 TUN-2243: Revert "STOR-519: Add db-connect, a SQL over HTTPS server"
- 2019-08-27 TUN-2244: Add NO_AUTOUPDATE env var
- 2019-08-22 AUTH-2018: Adds support for authorized keys and short lived certs
- 2019-08-28 AUTH-2022: Adds ssh timeout configuration
- 2019-08-28 TUN-1968: Gracefully diff StreamHandler.UpdateConfig
- 2019-08-26 AUTH-2021 - s3 bucket uploading for SSH logs
- 2019-08-19 AUTH-2004: Adds static host key support
- 2019-07-18 AUTH-1941: Adds initial SSH server implementation
2019.8.3
- 2019-08-20 STOR-519: Add db-connect, a SQL over HTTPS server
- 2019-08-20 Release 2019.8.2
- 2019-08-20 Revert "AUTH-1941: Adds initial SSH server implementation"
- 2019-08-11 TUN-2163: Add GrapQLType method to Scope interface
- 2019-08-06 TUN-2152: Requests with a query in the URL are erroneously escaped
- 2019-07-18 AUTH-1941: Adds initial SSH server implementation
2019.8.1
- 2019-08-05 TUN-2111: Implement custom serialization logic for FallibleConfig and OriginConfig
- 2019-08-06 Revert "TUN-1736: Missing headers when passing an invalid path"
2019.8.0
- 2019-07-11 TUN-1956: Go 1.12 update
- 2019-07-24 TUN-1736: Missing headers when passing an invalid path
- 2019-07-30 TUN-2117: read group/system-name from CLI, send it to edge
- 2019-08-02 TUN-2125: Add PostgresType() to Scope
- 2019-08-05 TUN-2147: Implemented ScopeUnmarshaler
- 2019-07-31 TUN-2110: Implement custom deserialization logic for OriginConfig
- 2019-07-31 AUTH-1972: Deletes token lock file if backoff retry attempts exceeded and intercepts signals until lock is released
2019.7.0
- 2019-05-28 TUN-1913: Define OriginService for each type of origin
- 2019-04-29 Build a docker container
- 2019-06-12 TUN-1952: Group ClientConfig fields by the component that uses the config, and return the part of the config that failed to be applied
- 2019-06-05 TUN-1893: Proxy requests to the origin based on tunnel hostname
- 2019-06-17 TUN-1961: Create EdgeConnectionManager to maintain outbound connections to the edge
- 2019-06-18 TUN-1885: Reconfigure cloudflared on receiving new ClientConfig
- 2019-06-19 TUN-1976: Pass tunnel hostname through header
- 2019-06-20 TUN-1982: Load custom origin CA when OriginCAPool is specified
- 2019-06-26 TUN-2005: Upgrade logrus
- 2019-06-20 TUN-1981: Write response header & body on proxy error to notify eyeballs of failure category
- 2019-06-20 TUN-1977: Validate OriginConfig has valid URL, and use scheme to determine if a HTTPOriginService is expecting HTTP or Unix
- 2019-06-13 DoH: change the media type to application/dns-message
- 2019-06-26 AUTH-1736: Better handling of token revocation
2019.6.0
- 2019-05-17 TUN-1828: Update declarative tunnel config struct
- 2019-05-29 Handle exit code on err
- 2019-05-29 AUTH-1802: Fixed ssh-config templating
- 2019-05-30 TUN-1914: Conflate HTTP and Unix OriginConfig, and add TLS config to WebSocketOriginConfig
- 2019-06-03 AUTH-1811: ssh-gen config fixes
2019.5.0
- 2019-04-25 TUN-1781: ServeStream should return early on error
- 2019-04-30 TUN-1786: Remove low-level Windows service logging
- 2019-05-03 TUN-1807: Send cloudflared version in Connect RPC
- 2019-01-23 AUTH-1557: Short Lived Certs
- 2019-05-13 TUN-1847: Log a distinct message when OpenStream fails while waiting for response headers
- 2019-05-13 AUTH-1706: fixes and testing
- 2019-05-22 TUN-1880: Save debug and warn level log to logfile
- 2019-05-22 AUTH-1781: fixed race condition for short lived certs, doc required config
2019.4.1
- 2019-03-18 TUN-1626: Create new supervisor to establish connection with origintunneld
- 2019-04-04 TUN-1619: Add flag to test declarative tunnels.
- 2019-04-05 TUN-1577: decompose carrier.StartServer to make TestStartServer less flappy
- 2019-03-29 TUN-1606: Define CloudflaredConfig RPC structure, interface for cloudflared's RPC server
- 2019-04-02 TUN-1682: Add context to OpenStream to prevent it from blocking indefinitely.
- 2019-04-16 TUN-1732: cloudflared metrics should track userHostnames
- 2019-04-17 TUN-1734: Pin packages at exact versions
- 2019-04-18 TUN-1669: Update license message in help text. Also fix test
2019.4.0
- 2019-03-28 TUN-1648: ConnectionID is now a UUID
- 2019-04-01 TUN-1673: Conflate Hello and Connect RPCs
2019.3.2
- 2019-03-22 TUN-1637: Free tunnels shouldn't require cert.pem
- 2019-03-18 TUN-1604: Define Connect RPC call
+106
View File
@@ -0,0 +1,106 @@
package awsuploader
import (
"os"
"path/filepath"
"time"
"github.com/sirupsen/logrus"
)
// DirectoryUploadManager is used to manage file uploads on an interval from a directory
type DirectoryUploadManager struct {
logger *logrus.Logger
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 *logrus.Logger, 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().After(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.WithError(err).Error("Cannot upload file to s3 bucket")
} else {
os.Remove(filepath)
}
}
}
}
@@ -0,0 +1,137 @@
package awsuploader
import (
"errors"
"io/ioutil"
"math/rand"
"os"
"path/filepath"
"testing"
"time"
"github.com/sirupsen/logrus"
)
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 := logrus.New()
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
}
+113 -38
View File
@@ -4,7 +4,6 @@
package carrier
import (
"errors"
"io"
"net"
"net/http"
@@ -12,10 +11,16 @@ import (
"strings"
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
"github.com/cloudflare/cloudflared/websocket"
cloudflaredWebsocket "github.com/cloudflare/cloudflared/websocket"
"github.com/gorilla/websocket"
"github.com/sirupsen/logrus"
)
type StartOptions struct {
OriginURL string
Headers http.Header
}
// StdinoutStream is empty struct for wrapping stdin/stdout
// into a single ReadWriter
type StdinoutStream struct {
@@ -32,21 +37,36 @@ 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
// StartClient will copy the data from stdin/stdout over a WebSocket connection
// to the edge (originURL)
func StartServer(logger *logrus.Logger, address, originURL string, shutdownC <-chan struct{}, headers http.Header) error {
func StartClient(logger *logrus.Logger, stream io.ReadWriter, options *StartOptions) error {
return serveStream(logger, stream, options)
}
// StartServer will setup a listener on a specified address/port and then
// forward connections to the origin by calling `Serve()`.
func StartServer(logger *logrus.Logger, 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
}
logger.Info("Started listening on ", address)
return Serve(logger, listener, shutdownC, options)
}
// 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(logger *logrus.Logger, listener net.Listener, shutdownC <-chan struct{}, options *StartOptions) error {
defer listener.Close()
for {
select {
@@ -57,27 +77,27 @@ func StartServer(logger *logrus.Logger, address, originURL string, shutdownC <-c
if err != nil {
return err
}
go serveConnection(logger, conn, originURL, headers)
go serveConnection(logger, conn, options)
}
}
}
// serveConnection handles connections for the StartServer call
func serveConnection(logger *logrus.Logger, c net.Conn, originURL string, headers http.Header) {
// serveConnection handles connections for the Serve() call
func serveConnection(logger *logrus.Logger, c net.Conn, options *StartOptions) {
defer c.Close()
serveStream(logger, originURL, c, headers)
serveStream(logger, c, options)
}
// 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)
func serveStream(logger *logrus.Logger, conn io.ReadWriter, options *StartOptions) error {
wsConn, err := createWebsocketStream(options)
if err != nil {
logger.WithError(err).Errorf("failed to connect to %s\n", originURL)
logger.WithError(err).Errorf("failed to connect to %s\n", options.OriginURL)
return err
}
defer wsConn.Close()
websocket.Stream(wsConn, conn)
cloudflaredWebsocket.Stream(wsConn, conn)
return nil
}
@@ -85,28 +105,17 @@ func serveStream(logger *logrus.Logger, originURL string, conn io.ReadWriter, he
// 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)
func createWebsocketStream(options *StartOptions) (*cloudflaredWebsocket.Conn, error) {
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
if err != nil {
return nil, err
}
req.Header = headers
req.Header = options.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)
wsConn, resp, err := cloudflaredWebsocket.ClientConnect(req, nil)
defer closeRespBody(resp)
if err != nil && isAccessResponse(resp) {
wsConn, err = createAccessAuthenticatedStream(options)
if err != nil {
return nil, err
}
@@ -114,12 +123,72 @@ func createWebsocketStream(originURL string, headers http.Header) (*websocket.Co
return nil, err
}
return &websocket.Conn{Conn: wsConn}, nil
return &cloudflaredWebsocket.Conn{Conn: wsConn}, 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 <= 300 {
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
}
// 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) (*websocket.Conn, error) {
wsConn, resp, err := createAccessWebSocketStream(options)
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)
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) (*websocket.Conn, *http.Response, error) {
req, err := buildAccessRequest(options)
if err != nil {
return nil, nil, err
}
return cloudflaredWebsocket.ClientConnect(req, nil)
}
// 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)
func buildAccessRequest(options *StartOptions) (*http.Request, error) {
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
if err != nil {
return nil, err
}
@@ -131,11 +200,17 @@ func buildAccessRequest(originURL string) (*http.Request, error) {
// 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)
for k, v := range options.Headers {
if len(v) >= 1 {
originRequest.Header.Set(k, v[0])
}
}
return originRequest, nil
}
+41 -8
View File
@@ -48,7 +48,11 @@ func TestStartClient(t *testing.T) {
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(logger, buf, options)
assert.NoError(t, err)
buf.Write([]byte(message))
@@ -58,24 +62,28 @@ 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()
shutdownC := make(chan struct{})
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(logger, 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 +91,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.StatusPermanentRedirect}, false},
{"200 ok", &http.Response{StatusCode: http.StatusOK}, false},
{"redirect with location", &http.Response{StatusCode: http.StatusPermanentRedirect, Header: validLocationHeader}, true},
{"redirect with invalid location", &http.Response{StatusCode: http.StatusPermanentRedirect, 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,
+14 -10
View File
@@ -1,5 +1,6 @@
pinned_go: &pinned_go go=1.11.5-1
pinned_go: &pinned_go go=1.12.7-1
build_dir: &build_dir /cfsetup_build/src/github.com/cloudflare/cloudflared/
default-flavor: stretch
stretch: &stretch
build:
build_dir: *build_dir
@@ -46,15 +47,15 @@ stretch: &stretch
- 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 GOPATH=/cfsetup_build/
- export GOOS=linux
- export GOARCH=386
- make release
release-windows-amd64:
build_dir: *build_dir
builddeps:
@@ -86,6 +87,9 @@ stretch: &stretch
- export GOPATH=/cfsetup_build/
- export GOOS=linux
- export GOARCH=amd64
- sudo chown -R $(whoami) /cfsetup_build/
- go get github.com/BurntSushi/go-sumtype
- export PATH="$GOPATH/bin:$PATH"
- make test
jessie: *stretch
+20 -10
View File
@@ -17,19 +17,29 @@ import (
// 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") == "" {
// 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")
}
headers := buildRequestHeaders(c.StringSlice("header"))
if c.IsSet("service-token-id") {
headers.Add("CF-Access-Client-Id", c.String("service-token-id"))
originURL := "https://" + hostname
// get the headers from the cmdline and add them
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
if c.IsSet(sshTokenIDFlag) {
headers.Add("CF-Access-Client-Id", c.String(sshTokenIDFlag))
}
if c.IsSet("service-token-secret") {
headers.Add("CF-Access-Client-Secret", c.String("service-token-secret"))
if c.IsSet(sshTokenSecretFlag) {
headers.Add("CF-Access-Client-Secret", c.String(sshTokenSecretFlag))
}
if c.NArg() > 0 || c.IsSet("url") {
options := &carrier.StartOptions{
OriginURL: originURL,
Headers: headers,
}
if c.NArg() > 0 || c.IsSet(sshURLFlag) {
localForwarder, err := config.ValidateUrl(c)
if err != nil {
logger.WithError(err).Error("Error validating origin URL")
@@ -40,10 +50,10 @@ func ssh(c *cli.Context) error {
logger.WithError(err).Error("Error validating origin URL")
return errors.Wrap(err, "error validating origin URL")
}
return carrier.StartServer(logger, forwarder.Host, "https://"+hostname, shutdownC, headers)
return carrier.StartServer(logger, forwarder.Host, shutdownC, options)
}
return carrier.StartClient(logger, "https://"+hostname, &carrier.StdinoutStream{}, headers)
return carrier.StartClient(logger, &carrier.StdinoutStream{}, options)
}
func buildRequestHeaders(values []string) http.Header {
+104 -13
View File
@@ -6,9 +6,12 @@ import (
"net/url"
"os"
"strings"
"text/template"
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
"github.com/cloudflare/cloudflared/sshgen"
"github.com/cloudflare/cloudflared/validation"
"golang.org/x/net/idna"
"github.com/cloudflare/cloudflared/log"
@@ -16,6 +19,31 @@ import (
cli "gopkg.in/urfave/cli.v2"
)
const (
sshHostnameFlag = "hostname"
sshURLFlag = "url"
sshHeaderFlag = "header"
sshTokenIDFlag = "service-token-id"
sshTokenSecretFlag = "service-token-secret"
sshGenCertFlag = "short-lived-cert"
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 (
@@ -93,35 +121,57 @@ func Commands() []*cli.Command {
Description: `The ssh 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,
Usage: "specify the hostname of your application.",
},
&cli.StringFlag{
Name: "url",
Usage: "specifics the host:port to forward data to Cloudflare edge.",
Name: sshURLFlag,
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",
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",
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.",
},
},
},
{
Name: "ssh-config",
Action: sshConfig,
Usage: "ssh-config",
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: 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.",
},
},
},
},
},
@@ -205,8 +255,49 @@ 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 {
// 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("https://" + hostname)
if err != nil {
return err
}
// this fetchToken function mutates the appURL param. We should refactor that
fetchTokenURL := &url.URL{}
*fetchTokenURL = *originURL
token, err := token.FetchToken(fetchTokenURL)
if err != nil {
return err
}
if err := sshgen.GenerateShortLivedCertificate(originURL, token); err != nil {
return err
}
return nil
}
+28
View File
@@ -0,0 +1,28 @@
package buildinfo
import (
"runtime"
"github.com/sirupsen/logrus"
)
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 *logrus.Logger) {
logger.Infof("Version %s", bi.CloudflaredVersion)
logger.Infof("GOOS: %s, GOVersion: %s, GoArch: %s", bi.GoOS, bi.GoVersion, bi.GoArch)
}
+17 -10
View File
@@ -11,17 +11,15 @@ import (
"github.com/cloudflare/cloudflared/log"
"github.com/cloudflare/cloudflared/metrics"
"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 (
@@ -62,10 +60,15 @@ func main() {
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.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()
@@ -124,10 +127,14 @@ func action(version string, shutdownC, graceShutdownC chan struct{}) cli.ActionF
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)
}
}
+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
}
+123 -32
View File
@@ -1,40 +1,145 @@
package token
import (
"context"
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"os/signal"
"syscall"
"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/origin"
"github.com/coreos/go-oidc/jose"
"github.com/coreos/go-oidc/oidc"
homedir "github.com/mitchellh/go-homedir"
)
const (
keyName = "token"
)
var logger = log.CreateLogger()
type lock struct {
lockFilePath string
backoff *origin.BackoffHandler
sigHandler *signalHandler
}
type signalHandler struct {
sigChannel chan os.Signal
signals []os.Signal
}
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
}
// FetchToken will either load a stored token or generate a new one
func FetchToken(appURL *url.URL) (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)
if err != nil {
return "", err
}
@@ -44,7 +149,7 @@ func FetchToken(appURL *url.URL) (string, error) {
// GetTokenIfExists will return the token from local storage if it exists
func GetTokenIfExists(url *url.URL) (string, error) {
path, err := generateFilePathForTokenURL(url)
path, err := path.GenerateFilePathFromURL(url, keyName)
if err != nil {
return "", err
}
@@ -57,32 +162,18 @@ func GetTokenIfExists(url *url.URL) (string, error) {
return "", err
}
claims, err := token.Claims()
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
}
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:
}
}
+352 -26
View File
@@ -1,36 +1,79 @@
package tunnel
import (
"context"
"fmt"
"io/ioutil"
"net"
"net/url"
"os"
"runtime"
"runtime/trace"
"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/config"
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
"github.com/cloudflare/cloudflared/cmd/sqlgateway"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/hello"
"github.com/cloudflare/cloudflared/metrics"
"github.com/cloudflare/cloudflared/origin"
"github.com/cloudflare/cloudflared/signal"
"github.com/cloudflare/cloudflared/sshlog"
"github.com/cloudflare/cloudflared/sshserver"
"github.com/cloudflare/cloudflared/supervisor"
"github.com/cloudflare/cloudflared/tlsconfig"
"github.com/cloudflare/cloudflared/tunneldns"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"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/pkg/errors"
"golang.org/x/crypto/ssh/terminal"
"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 = "/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"
)
var (
shutdownC chan struct{}
@@ -234,9 +277,8 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
return err
}
buildInfo := origin.GetBuildInfo()
logger.Infof("Build info: %+v", *buildInfo)
logger.Infof("Version %s", version)
buildInfo := buildinfo.GetBuildInfo(version)
buildInfo.Log(logger)
logClientOptions(c)
if c.IsSet("proxy-dns") {
@@ -252,16 +294,6 @@ 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")
@@ -279,6 +311,33 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
go writePidFile(connectedSignal, c.String("pidfile"))
}
cloudflaredID, err := uuid.NewRandom()
if err != nil {
logger.WithError(err).Error("Cannot generate cloudflared ID")
return err
}
ctx, cancel := context.WithCancel(context.Background())
go func() {
<-shutdownC
cancel()
}()
if c.IsSet("use-declarative-tunnels") {
return startDeclarativeTunnel(ctx, c, cloudflaredID, buildInfo, &listeners)
}
// 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()
autoupdater := updater.NewAutoUpdater(c.Duration("autoupdate-freq"), &listeners)
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()
@@ -287,6 +346,7 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
}
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")
@@ -301,6 +361,46 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
c.Set("url", "https://"+helloListener.Addr().String())
}
if c.IsSet("ssh-server") {
if runtime.GOOS != "darwin" && runtime.GOOS != "linux" {
logger.Errorf("--ssh-server is not supported on %s", runtime.GOOS)
return errors.New(fmt.Sprintf("--ssh-server is not supported on %s", runtime.GOOS))
}
logger.Infof("ssh-server set")
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 {
logger.WithError(err).Error("Cannot create uploader for SSH Server")
return errors.Wrap(err, "Cannot create uploader for SSH Server")
}
os.Mkdir(sshLogFileDirectory, 0600)
uploadManager := awsuploader.NewDirectoryUploadManager(logger, uploader, sshLogFileDirectory, 30*time.Minute, shutdownC)
uploadManager.Start()
}
logManager := sshlog.New()
sshServerAddress := "127.0.0.1:" + c.String(sshPortFlag)
server, err := sshserver.New(logManager, logger, sshServerAddress, shutdownC, c.Duration(sshIdleTimeoutFlag), c.Duration(sshMaxTimeoutFlag))
if err != nil {
logger.WithError(err).Error("Cannot create new SSH Server")
return errors.Wrap(err, "Cannot create new SSH Server")
}
wg.Add(1)
go func() {
defer wg.Done()
if err = server.Start(); err != nil && err != ssh.ErrServerClosed {
logger.WithError(err).Error("SSH server error")
}
}()
c.Set("url", "ssh://"+sshServerAddress)
}
if host := hostnameFromURI(c.String("url")); host != "" {
listener, err := net.Listen("tcp", "127.0.0.1:")
if err != nil {
@@ -323,7 +423,7 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
wg.Add(1)
go func() {
defer wg.Done()
errC <- origin.StartTunnelDaemon(tunnelConfig, graceShutdownC, connectedSignal)
errC <- origin.StartTunnelDaemon(ctx, tunnelConfig, connectedSignal, cloudflaredID)
}()
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, c.Duration("grace-period"))
@@ -348,6 +448,126 @@ func Before(c *cli.Context) error {
return nil
}
func startDeclarativeTunnel(ctx context.Context,
c *cli.Context,
cloudflaredID uuid.UUID,
buildInfo *buildinfo.BuildInfo,
listeners *gracenet.Net,
) error {
reverseProxyOrigin, err := defaultOriginConfig(c)
if err != nil {
logger.WithError(err)
return err
}
reverseProxyConfig, err := pogs.NewReverseProxyConfig(
c.String("hostname"),
reverseProxyOrigin,
c.Uint64("retries"),
c.Duration("proxy-connection-timeout"),
c.Uint64("compression-quality"),
)
if err != nil {
logger.WithError(err).Error("Cannot initialize default client config because reverse proxy config is invalid")
return err
}
defaultClientConfig := &pogs.ClientConfig{
Version: pogs.InitVersion(),
SupervisorConfig: &pogs.SupervisorConfig{
AutoUpdateFrequency: c.Duration("autoupdate-freq"),
MetricsUpdateFrequency: c.Duration("metrics-update-freq"),
GracePeriod: c.Duration("grace-period"),
},
EdgeConnectionConfig: &pogs.EdgeConnectionConfig{
NumHAConnections: uint8(c.Int("ha-connections")),
HeartbeatInterval: c.Duration("heartbeat-interval"),
Timeout: c.Duration("dial-edge-timeout"),
MaxFailedHeartbeats: c.Uint64("heartbeat-count"),
},
DoHProxyConfigs: []*pogs.DoHProxyConfig{},
ReverseProxyConfigs: []*pogs.ReverseProxyConfig{reverseProxyConfig},
}
autoupdater := updater.NewAutoUpdater(defaultClientConfig.SupervisorConfig.AutoUpdateFrequency, listeners)
originCert, err := getOriginCert(c)
if err != nil {
logger.WithError(err).Error("error getting origin cert")
return err
}
toEdgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c)
if err != nil {
logger.WithError(err).Error("unable to create TLS config to connect with edge")
return err
}
tags, err := NewTagSliceFromCLI(c.StringSlice("tag"))
if err != nil {
logger.WithError(err).Error("unable to parse tag")
return err
}
name := c.String("name")
group := c.String("group")
if group == "" {
err := fmt.Errorf("--group must be specified")
logger.WithError(err).Error("unable to parse group name")
return err
}
cloudflaredConfig := &connection.CloudflaredConfig{
BuildInfo: buildInfo,
CloudflaredID: cloudflaredID,
Name: name,
Group: group,
Tags: tags,
}
serviceDiscoverer, err := serviceDiscoverer(c, logger)
if err != nil {
logger.WithError(err).Error("unable to create service discoverer")
return err
}
supervisor, err := supervisor.NewSupervisor(defaultClientConfig, originCert, toEdgeTLSConfig,
serviceDiscoverer, cloudflaredConfig, autoupdater, updater.SupportAutoUpdate(), logger)
if err != nil {
logger.WithError(err).Error("unable to create Supervisor")
return err
}
return supervisor.Run(ctx)
}
func defaultOriginConfig(c *cli.Context) (pogs.OriginConfig, error) {
if c.IsSet("hello-world") {
return &pogs.HelloWorldOriginConfig{}, nil
}
originConfig := &pogs.HTTPOriginConfig{
TCPKeepAlive: c.Duration("proxy-tcp-keepalive"),
DialDualStack: !c.Bool("proxy-no-happy-eyeballs"),
TLSHandshakeTimeout: c.Duration("proxy-tls-timeout"),
TLSVerify: !c.Bool("no-tls-verify"),
OriginCAPool: c.String("origin-ca-pool"),
OriginServerName: c.String("origin-server-name"),
MaxIdleConnections: c.Uint64("proxy-keepalive-connections"),
IdleConnectionTimeout: c.Duration("proxy-keepalive-timeout"),
ProxyConnectionTimeout: c.Duration("proxy-connection-timeout"),
ExpectContinueTimeout: c.Duration("proxy-expect-continue-timeout"),
ChunkedEncoding: c.Bool("no-chunked-encoding"),
}
if c.IsSet("unix-socket") {
unixSocket, err := config.ValidateUnixSocket(c)
if err != nil {
return nil, errors.Wrap(err, "error validating --unix-socket")
}
originConfig.URLString = unixSocket
}
originAddr, err := config.ValidateUrl(c)
if err != nil {
return nil, errors.Wrap(err, "error validating origin URL")
}
originConfig.URLString = originAddr
return originConfig, nil
}
func waitToShutdown(wg *sync.WaitGroup,
errC chan error,
shutdownC, graceShutdownC chan struct{},
@@ -421,15 +641,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",
@@ -444,7 +665,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
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 +684,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 +708,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.",
@@ -584,6 +811,13 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
EnvVars: []string{"TUNNEL_HELLO_WORLD"},
Hidden: shouldHide,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "ssh-server",
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.",
@@ -636,6 +870,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.",
@@ -689,5 +935,85 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
EnvVars: []string{"TUNNEL_TRACE_OUTPUT"},
Hidden: shouldHide,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "use-declarative-tunnels",
Usage: "Test establishing connections with declarative tunnel methods.",
EnvVars: []string{"TUNNEL_USE_DECLARATIVE"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "name",
Usage: "Friendly name for this cloudflared instance.",
EnvVars: []string{"TUNNEL_DECLARATIVE_NAME"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "group",
Usage: "The group whose behavior this cloudflared instance will adopt. This behavior can be configured by editing the group's 'intent' in the Declarative Tunnel UI.",
EnvVars: []string{"TUNNEL_DECLARATIVE_GROUP"},
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: "22",
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,
}),
}
}
+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"))
}
+55 -147
View File
@@ -3,31 +3,29 @@ package tunnel
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/hex"
"fmt"
"io/ioutil"
"math/rand"
"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/connection"
"github.com/cloudflare/cloudflared/origin"
"github.com/cloudflare/cloudflared/tlsconfig"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/cloudflare/cloudflared/validation"
"golang.org/x/crypto/ssh/terminal"
"github.com/sirupsen/logrus"
"gopkg.in/urfave/cli.v2"
"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"
)
var (
@@ -49,11 +47,13 @@ func findDefaultOriginCertPath() string {
return ""
}
func generateRandomClientID() string {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
id := make([]byte, 32)
r.Read(id)
return hex.EncodeToString(id)
func generateRandomClientID(logger *logrus.Logger) (string, error) {
u, err := uuid.NewRandom()
if err != nil {
logger.WithError(err).Error("couldn't create UUID for client ID")
return "", err
}
return u.String(), nil
}
func handleDeprecatedOptions(c *cli.Context) error {
@@ -147,7 +147,7 @@ If you don't have a certificate signed by Cloudflare, run the command:
func prepareTunnelConfig(
c *cli.Context,
buildInfo *origin.BuildInfo,
buildInfo *buildinfo.BuildInfo,
version string, logger,
transportLogger *logrus.Logger,
) (*origin.TunnelConfig, error) {
@@ -159,7 +159,10 @@ func prepareTunnelConfig(
isFreeTunnel := hostname == ""
clientID := c.String("id")
if !c.IsSet("id") {
clientID = generateRandomClientID()
clientID, err = generateRandomClientID(logger)
if err != nil {
return nil, err
}
}
tags, err := NewTagSliceFromCLI(c.StringSlice("tag"))
@@ -184,7 +187,7 @@ func prepareTunnelConfig(
}
}
originCertPool, err := loadCertPool(c, logger)
originCertPool, err := tlsconfig.LoadOriginCA(c, logger)
if err != nil {
logger.WithError(err).Error("Error loading cert pool")
return nil, errors.Wrap(err, "Error loading cert pool")
@@ -233,147 +236,52 @@ func prepareTunnelConfig(
return nil, errors.Wrap(err, "unable to connect to the origin")
}
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")
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,
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,
TransportLogger: transportLogger,
UseDeclarativeTunnel: c.Bool("use-declarative-tunnels"),
}, 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))
}
func serviceDiscoverer(c *cli.Context, logger *logrus.Logger) (connection.EdgeServiceDiscoverer, error) {
// If --edge is specfied, resolve edge server addresses
if len(c.StringSlice("edge")) > 0 {
return connection.NewEdgeHostnameResolver(c.StringSlice("edge"))
}
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
// Otherwise lookup edge server addresses through service discovery
return connection.NewEdgeAddrResolver(logger)
}
func isRunningFromTerminal() bool {
+2
View File
@@ -59,6 +59,8 @@ func initLogFile(c *cli.Context, loggers ...*logrus.Logger) error {
}
defer f.Close()
pathMap := lfshook.PathMap{
logrus.DebugLevel: filePath,
logrus.WarnLevel: filePath,
logrus.InfoLevel: filePath,
logrus.ErrorLevel: filePath,
logrus.FatalLevel: filePath,
+84 -26
View File
@@ -1,6 +1,7 @@
package updater
import (
"context"
"os"
"runtime"
"time"
@@ -14,6 +15,7 @@ import (
)
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."
@@ -75,30 +77,6 @@ func Update(_ *cli.Context) error {
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
}
}
}
// Checks for an update and applies it if one is available
func loggedUpdate() UpdateOutcome {
updateOutcome := checkForUpdateAndApply()
@@ -112,7 +90,88 @@ func loggedUpdate() UpdateOutcome {
return updateOutcome
}
// AutoUpdater periodically checks for new version of cloudflared.
type AutoUpdater struct {
configurable *configurable
listeners *gracenet.Net
updateConfigChan chan *configurable
}
// 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) *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),
}
}
func (a *AutoUpdater) Run(ctx context.Context) error {
ticker := time.NewTicker(a.configurable.freq)
for {
if a.configurable.enabled {
updateOutcome := loggedUpdate()
if updateOutcome.Updated {
os.Args = append(os.Args, "--is-autoupdated=true")
pid, err := a.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 <-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) bool {
if !SupportAutoUpdate() {
return false
}
return !c.Bool("no-autoupdate") && c.Duration("autoupdate-freq") != 0
}
func SupportAutoUpdate() bool {
if runtime.GOOS == "windows" {
logger.Info(noUpdateOnWindowsMessage)
return false
@@ -122,8 +181,7 @@ 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 {
+26
View File
@@ -0,0 +1,26 @@
package updater
import (
"context"
"testing"
"github.com/facebookgo/grace/gracenet"
"github.com/stretchr/testify/assert"
)
func TestDisabledAutoUpdater(t *testing.T) {
listeners := &gracenet.Net{}
autoupdater := NewAutoUpdater(0, listeners)
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)
}
-3
View File
@@ -135,14 +135,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:
+81
View File
@@ -0,0 +1,81 @@
package connection
import (
"context"
"net"
"time"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/tunnelrpc"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
rpc "zombiezen.com/go/capnproto2/rpc"
)
const (
openStreamTimeout = 30 * time.Second
)
type dialError struct {
cause error
}
func (e dialError) Error() string {
return e.cause.Error()
}
type Connection struct {
id uuid.UUID
muxer *h2mux.Muxer
}
func newConnection(muxer *h2mux.Muxer, edgeIP *net.TCPAddr) (*Connection, error) {
id, err := uuid.NewRandom()
if err != nil {
return nil, err
}
return &Connection{
id: id,
muxer: muxer,
}, nil
}
func (c *Connection) Serve(ctx context.Context) error {
// Serve doesn't return until h2mux is shutdown
return c.muxer.Serve(ctx)
}
// Connect is used to establish connections with cloudflare's edge network
func (c *Connection) Connect(ctx context.Context, parameters *tunnelpogs.ConnectParameters, logger *logrus.Entry) (*pogs.ConnectResult, error) {
openStreamCtx, cancel := context.WithTimeout(ctx, openStreamTimeout)
defer cancel()
rpcConn, err := c.newRPConn(openStreamCtx, logger)
if err != nil {
return nil, errors.Wrap(err, "cannot create new RPC connection")
}
defer rpcConn.Close()
tsClient := tunnelpogs.TunnelServer_PogsClient{Client: rpcConn.Bootstrap(ctx)}
return tsClient.Connect(ctx, parameters)
}
func (c *Connection) Shutdown() {
c.muxer.Shutdown()
}
func (c *Connection) newRPConn(ctx context.Context, logger *logrus.Entry) (*rpc.Conn, error) {
stream, err := c.muxer.OpenRPCStream(ctx)
if err != nil {
return nil, err
}
return rpc.NewConn(
tunnelrpc.NewTransportLogger(logger.WithField("rpc", "connect"), rpc.StreamTransport(stream)),
tunnelrpc.ConnLog(logger.WithField("rpc", "connect")),
), nil
}
+130 -19
View File
@@ -1,14 +1,15 @@
package origin
package connection
import (
"context"
"crypto/tls"
"fmt"
"net"
"sync"
"time"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/sirupsen/logrus"
)
const (
@@ -22,6 +23,9 @@ const (
dotServerName = "cloudflare-dns.com"
dotServerAddr = "1.1.1.1:853"
dotTimeout = time.Duration(15 * time.Second)
// SRV record resolution TTL
resolveEdgeAddrTTL = 1 * time.Hour
)
var friendlyDNSErrorLines = []string{
@@ -34,20 +38,65 @@ var friendlyDNSErrorLines = []string{
` https://developers.cloudflare.com/1.1.1.1/setting-up-1.1.1.1/`,
}
func ResolveEdgeIPs(logger *log.Logger, addresses []string) ([]*net.TCPAddr, error) {
if len(addresses) > 0 {
var tcpAddrs []*net.TCPAddr
for _, address := range addresses {
// Addresses specified (for testing, usually)
tcpAddr, err := net.ResolveTCPAddr("tcp", address)
if err != nil {
return nil, err
}
tcpAddrs = append(tcpAddrs, tcpAddr)
}
return tcpAddrs, nil
// EdgeServiceDiscoverer is an interface for looking up Cloudflare's edge network addresses
type EdgeServiceDiscoverer interface {
// Addr returns an address to connect to cloudflare's edge network
Addr() *net.TCPAddr
// AvailableAddrs returns the number of unique addresses
AvailableAddrs() uint8
// Refresh rediscover Cloudflare's edge network addresses
Refresh() error
}
// EdgeAddrResolver discovers the addresses of Cloudflare's edge network through SRV record.
// It implements EdgeServiceDiscoverer interface
type EdgeAddrResolver struct {
sync.Mutex
// Addrs to connect to cloudflare's edge network
addrs []*net.TCPAddr
// index of the next element to use in addrs
nextAddrIndex int
logger *logrus.Entry
}
func NewEdgeAddrResolver(logger *logrus.Logger) (EdgeServiceDiscoverer, error) {
r := &EdgeAddrResolver{
logger: logger.WithField("subsystem", " edgeAddrResolver"),
}
// HA service discovery lookup
if err := r.Refresh(); err != nil {
return nil, err
}
return r, nil
}
func (r *EdgeAddrResolver) Addr() *net.TCPAddr {
r.Lock()
defer r.Unlock()
addr := r.addrs[r.nextAddrIndex]
r.nextAddrIndex = (r.nextAddrIndex + 1) % len(r.addrs)
return addr
}
func (r *EdgeAddrResolver) AvailableAddrs() uint8 {
r.Lock()
defer r.Unlock()
return uint8(len(r.addrs))
}
func (r *EdgeAddrResolver) Refresh() error {
newAddrs, err := EdgeDiscovery(r.logger)
if err != nil {
return err
}
r.Lock()
defer r.Unlock()
r.addrs = newAddrs
r.nextAddrIndex = 0
return nil
}
// HA service discovery lookup
func EdgeDiscovery(logger *logrus.Entry) ([]*net.TCPAddr, error) {
_, addrs, err := net.LookupSRV(srvService, srvProto, srvName)
if err != nil {
// Try to fall back to DoT from Cloudflare directly.
@@ -78,7 +127,7 @@ func ResolveEdgeIPs(logger *log.Logger, addresses []string) ([]*net.TCPAddr, err
var resolvedIPsPerCNAME [][]*net.TCPAddr
var lookupErr error
for _, addr := range addrs {
ips, err := ResolveSRVToTCP(addr)
ips, err := resolveSRVToTCP(addr)
if err != nil || len(ips) == 0 {
// don't return early, we might be able to resolve other addresses
lookupErr = err
@@ -86,14 +135,14 @@ func ResolveEdgeIPs(logger *log.Logger, addresses []string) ([]*net.TCPAddr, err
}
resolvedIPsPerCNAME = append(resolvedIPsPerCNAME, ips)
}
ips := FlattenServiceIPs(resolvedIPsPerCNAME)
ips := flattenServiceIPs(resolvedIPsPerCNAME)
if lookupErr == nil && len(ips) == 0 {
return nil, fmt.Errorf("Unknown service discovery error")
}
return ips, lookupErr
}
func ResolveSRVToTCP(srv *net.SRV) ([]*net.TCPAddr, error) {
func resolveSRVToTCP(srv *net.SRV) ([]*net.TCPAddr, error) {
ips, err := net.LookupIP(srv.Target)
if err != nil {
return nil, err
@@ -107,7 +156,7 @@ func ResolveSRVToTCP(srv *net.SRV) ([]*net.TCPAddr, error) {
// FlattenServiceIPs transposes and flattens the input slices such that the
// first element of the n inner slices are the first n elements of the result.
func FlattenServiceIPs(ipsByService [][]*net.TCPAddr) []*net.TCPAddr {
func flattenServiceIPs(ipsByService [][]*net.TCPAddr) []*net.TCPAddr {
var result []*net.TCPAddr
for len(ipsByService) > 0 {
filtered := ipsByService[:0]
@@ -141,3 +190,65 @@ func fallbackResolver(serverName, serverAddress string) *net.Resolver {
},
}
}
// EdgeHostnameResolver discovers the addresses of Cloudflare's edge network via a list of server hostnames.
// It implements EdgeServiceDiscoverer interface, and is used mainly for testing connectivity.
type EdgeHostnameResolver struct {
sync.Mutex
// hostnames of edge servers
hostnames []string
// Addrs to connect to cloudflare's edge network
addrs []*net.TCPAddr
// index of the next element to use in addrs
nextAddrIndex int
}
func NewEdgeHostnameResolver(edgeHostnames []string) (EdgeServiceDiscoverer, error) {
r := &EdgeHostnameResolver{
hostnames: edgeHostnames,
}
if err := r.Refresh(); err != nil {
return nil, err
}
return r, nil
}
func (r *EdgeHostnameResolver) Addr() *net.TCPAddr {
r.Lock()
defer r.Unlock()
addr := r.addrs[r.nextAddrIndex]
r.nextAddrIndex = (r.nextAddrIndex + 1) % len(r.addrs)
return addr
}
func (r *EdgeHostnameResolver) AvailableAddrs() uint8 {
r.Lock()
defer r.Unlock()
return uint8(len(r.addrs))
}
func (r *EdgeHostnameResolver) Refresh() error {
newAddrs, err := ResolveAddrs(r.hostnames)
if err != nil {
return err
}
r.Lock()
defer r.Unlock()
r.addrs = newAddrs
r.nextAddrIndex = 0
return nil
}
// Resolve 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
}
@@ -1,4 +1,4 @@
package origin
package connection
import (
"net"
@@ -7,8 +7,26 @@ import (
"github.com/stretchr/testify/assert"
)
type mockEdgeServiceDiscoverer struct {
}
func (mr *mockEdgeServiceDiscoverer) Addr() *net.TCPAddr {
return &net.TCPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: 63102,
}
}
func (mr *mockEdgeServiceDiscoverer) AvailableAddrs() uint8 {
return 1
}
func (mr *mockEdgeServiceDiscoverer) Refresh() error {
return nil
}
func TestFlattenServiceIPs(t *testing.T) {
result := FlattenServiceIPs([][]*net.TCPAddr{
result := flattenServiceIPs([][]*net.TCPAddr{
[]*net.TCPAddr{
&net.TCPAddr{Port: 1},
&net.TCPAddr{Port: 2},
+286
View File
@@ -0,0 +1,286 @@
package connection
import (
"context"
"crypto/tls"
"fmt"
"net"
"sync"
"time"
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const (
quickStartLink = "https://developers.cloudflare.com/argo-tunnel/quickstart/"
faqLink = "https://developers.cloudflare.com/argo-tunnel/faq/"
)
// EdgeManager manages connections with the edge
type EdgeManager struct {
// streamHandler handles stream opened by the edge
streamHandler h2mux.MuxedStreamHandler
// TLSConfig is the TLS configuration to connect with edge
tlsConfig *tls.Config
// cloudflaredConfig is the cloudflared configuration that is determined when the process first starts
cloudflaredConfig *CloudflaredConfig
// serviceDiscoverer returns the next edge addr to connect to
serviceDiscoverer EdgeServiceDiscoverer
// state is attributes of ConnectionManager that can change during runtime.
state *edgeManagerState
logger *logrus.Entry
}
// EdgeConnectionManagerConfigurable is the configurable attributes of a EdgeConnectionManager
type EdgeManagerConfigurable struct {
TunnelHostnames []h2mux.TunnelHostname
*pogs.EdgeConnectionConfig
}
type CloudflaredConfig struct {
CloudflaredID uuid.UUID
Tags []pogs.Tag
BuildInfo *buildinfo.BuildInfo
Name string
Group string
}
func NewEdgeManager(
streamHandler h2mux.MuxedStreamHandler,
edgeConnMgrConfigurable *EdgeManagerConfigurable,
userCredential []byte,
tlsConfig *tls.Config,
serviceDiscoverer EdgeServiceDiscoverer,
cloudflaredConfig *CloudflaredConfig,
logger *logrus.Logger,
) *EdgeManager {
return &EdgeManager{
streamHandler: streamHandler,
tlsConfig: tlsConfig,
cloudflaredConfig: cloudflaredConfig,
serviceDiscoverer: serviceDiscoverer,
state: newEdgeConnectionManagerState(edgeConnMgrConfigurable, userCredential),
logger: logger.WithField("subsystem", "connectionManager"),
}
}
func (em *EdgeManager) Run(ctx context.Context) error {
defer em.shutdown()
resolveEdgeIPTicker := time.Tick(resolveEdgeAddrTTL)
for {
select {
case <-ctx.Done():
return errors.Wrap(ctx.Err(), "EdgeConnectionManager terminated")
case <-resolveEdgeIPTicker:
if err := em.serviceDiscoverer.Refresh(); err != nil {
em.logger.WithError(err).Warn("Cannot refresh Cloudflare edge addresses")
}
default:
time.Sleep(1 * time.Second)
}
// Create/delete connection one at a time, so we don't need to adjust for connections that are being created/deleted
// in shouldCreateConnection or shouldReduceConnection calculation
if em.state.shouldCreateConnection(em.serviceDiscoverer.AvailableAddrs()) {
if err := em.newConnection(ctx); err != nil {
em.logger.WithError(err).Error("cannot create new connection")
}
} else if em.state.shouldReduceConnection() {
if err := em.closeConnection(ctx); err != nil {
em.logger.WithError(err).Error("cannot close connection")
}
}
}
}
func (em *EdgeManager) UpdateConfigurable(newConfigurable *EdgeManagerConfigurable) {
em.logger.Infof("New edge connection manager configuration %+v", newConfigurable)
em.state.updateConfigurable(newConfigurable)
}
func (em *EdgeManager) newConnection(ctx context.Context) error {
edgeIP := em.serviceDiscoverer.Addr()
edgeConn, err := em.dialEdge(ctx, edgeIP)
if err != nil {
return errors.Wrap(err, "dial edge error")
}
configurable := em.state.getConfigurable()
// Establish a muxed connection with the edge
// Client mux handshake with agent server
muxer, err := h2mux.Handshake(edgeConn, edgeConn, h2mux.MuxerConfig{
Timeout: configurable.Timeout,
Handler: em.streamHandler,
IsClient: true,
HeartbeatInterval: configurable.HeartbeatInterval,
MaxHeartbeats: configurable.MaxFailedHeartbeats,
Logger: em.logger.WithField("subsystem", "muxer"),
})
if err != nil {
return errors.Wrap(err, "couldn't perform handshake with edge")
}
h2muxConn, err := newConnection(muxer, edgeIP)
if err != nil {
return errors.Wrap(err, "couldn't create h2mux connection")
}
go em.serveConn(ctx, h2muxConn)
connResult, err := h2muxConn.Connect(ctx, &pogs.ConnectParameters{
CloudflaredID: em.cloudflaredConfig.CloudflaredID,
CloudflaredVersion: em.cloudflaredConfig.BuildInfo.CloudflaredVersion,
NumPreviousAttempts: 0,
OriginCert: em.state.getUserCredential(),
Name: em.cloudflaredConfig.Name,
Group: em.cloudflaredConfig.Group,
Tags: em.cloudflaredConfig.Tags,
}, em.logger)
if err != nil {
h2muxConn.Shutdown()
return errors.Wrap(err, "couldn't connect to edge")
}
if connErr := connResult.Err; connErr != nil {
if !connErr.ShouldRetry {
return errors.Wrap(connErr, em.noRetryMessage())
}
return errors.Wrapf(connErr, "edge responded with RetryAfter=%v", connErr.RetryAfter)
}
em.state.newConnection(h2muxConn)
em.logger.Infof("connected to %s", connResult.ServerInfo.LocationName)
return nil
}
func (em *EdgeManager) closeConnection(ctx context.Context) error {
conn := em.state.getFirstConnection()
if conn == nil {
return fmt.Errorf("no connection to close")
}
conn.Shutdown()
return nil
}
func (em *EdgeManager) serveConn(ctx context.Context, conn *Connection) {
err := conn.Serve(ctx)
em.logger.WithError(err).Warn("Connection closed")
em.state.closeConnection(conn)
}
func (em *EdgeManager) dialEdge(ctx context.Context, edgeIP *net.TCPAddr) (*tls.Conn, error) {
timeout := em.state.getConfigurable().Timeout
// Inherit from parent context so we can cancel (Ctrl-C) while dialing
dialCtx, dialCancel := context.WithTimeout(ctx, timeout)
defer dialCancel()
dialer := net.Dialer{DualStack: true}
edgeConn, err := dialer.DialContext(dialCtx, "tcp", edgeIP.String())
if err != nil {
return nil, dialError{cause: errors.Wrap(err, "DialContext error")}
}
tlsEdgeConn := tls.Client(edgeConn, em.tlsConfig)
tlsEdgeConn.SetDeadline(time.Now().Add(timeout))
if err = tlsEdgeConn.Handshake(); err != nil {
return nil, dialError{cause: errors.Wrap(err, "Handshake with edge error")}
}
// clear the deadline on the conn; h2mux has its own timeouts
tlsEdgeConn.SetDeadline(time.Time{})
return tlsEdgeConn, nil
}
func (em *EdgeManager) noRetryMessage() string {
messageTemplate := "cloudflared could not register an Argo Tunnel on your account. Please confirm the following before trying again:" +
"1. You have Argo Smart Routing enabled in your account, See Enable Argo section of %s." +
"2. Your credential at %s is still valid. See %s."
return fmt.Sprintf(messageTemplate, quickStartLink, em.state.getConfigurable().UserCredentialPath, faqLink)
}
func (em *EdgeManager) shutdown() {
em.state.shutdown()
}
type edgeManagerState struct {
sync.RWMutex
configurable *EdgeManagerConfigurable
userCredential []byte
conns map[uuid.UUID]*Connection
}
func newEdgeConnectionManagerState(configurable *EdgeManagerConfigurable, userCredential []byte) *edgeManagerState {
return &edgeManagerState{
configurable: configurable,
userCredential: userCredential,
conns: make(map[uuid.UUID]*Connection),
}
}
func (ems *edgeManagerState) shouldCreateConnection(availableEdgeAddrs uint8) bool {
ems.RLock()
defer ems.RUnlock()
expectedHAConns := ems.configurable.NumHAConnections
if availableEdgeAddrs < expectedHAConns {
expectedHAConns = availableEdgeAddrs
}
return uint8(len(ems.conns)) < expectedHAConns
}
func (ems *edgeManagerState) shouldReduceConnection() bool {
ems.RLock()
defer ems.RUnlock()
return uint8(len(ems.conns)) > ems.configurable.NumHAConnections
}
func (ems *edgeManagerState) newConnection(conn *Connection) {
ems.Lock()
defer ems.Unlock()
ems.conns[conn.id] = conn
}
func (ems *edgeManagerState) closeConnection(conn *Connection) {
ems.Lock()
defer ems.Unlock()
delete(ems.conns, conn.id)
}
func (ems *edgeManagerState) getFirstConnection() *Connection {
ems.RLock()
defer ems.RUnlock()
for _, conn := range ems.conns {
return conn
}
return nil
}
func (ems *edgeManagerState) shutdown() {
ems.Lock()
defer ems.Unlock()
for _, conn := range ems.conns {
conn.Shutdown()
}
}
func (ems *edgeManagerState) getConfigurable() *EdgeManagerConfigurable {
ems.Lock()
defer ems.Unlock()
return ems.configurable
}
func (ems *edgeManagerState) updateConfigurable(newConfigurable *EdgeManagerConfigurable) {
ems.Lock()
defer ems.Unlock()
ems.configurable = newConfigurable
}
func (ems *edgeManagerState) getUserCredential() []byte {
ems.RLock()
defer ems.RUnlock()
return ems.userCredential
}
+77
View File
@@ -0,0 +1,77 @@
package connection
import (
"testing"
"time"
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
"github.com/stretchr/testify/assert"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
var (
configurable = &EdgeManagerConfigurable{
[]h2mux.TunnelHostname{
"http.example.com",
"ws.example.com",
"hello.example.com",
},
&pogs.EdgeConnectionConfig{
NumHAConnections: 1,
HeartbeatInterval: 1 * time.Second,
Timeout: 5 * time.Second,
MaxFailedHeartbeats: 3,
UserCredentialPath: "/etc/cloudflared/cert.pem",
},
}
cloudflaredConfig = &CloudflaredConfig{
CloudflaredID: uuid.New(),
Tags: []pogs.Tag{
{Name: "pool", Value: "east-6"},
},
BuildInfo: &buildinfo.BuildInfo{
GoOS: "linux",
GoVersion: "1.12",
GoArch: "amd64",
CloudflaredVersion: "2019.6.0",
},
}
)
type mockStreamHandler struct {
}
func (msh *mockStreamHandler) ServeStream(*h2mux.MuxedStream) error {
return nil
}
func mockEdgeManager() *EdgeManager {
return NewEdgeManager(
&mockStreamHandler{},
configurable,
[]byte{},
nil,
&mockEdgeServiceDiscoverer{},
cloudflaredConfig,
logrus.New(),
)
}
func TestUpdateConfigurable(t *testing.T) {
m := mockEdgeManager()
newConfigurable := &EdgeManagerConfigurable{
[]h2mux.TunnelHostname{
"second.example.com",
},
&pogs.EdgeConnectionConfig{
NumHAConnections: 2,
},
}
m.UpdateConfigurable(newConfigurable)
assert.Equal(t, newConfigurable, m.state.getConfigurable())
}
+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}
)
+53 -26
View File
@@ -1,7 +1,6 @@
package h2mux
import (
"bytes"
"context"
"io"
"strings"
@@ -94,6 +93,14 @@ 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.
// After the handshake completes, it is possible to open and accept streams.
func Handshake(
@@ -201,9 +208,9 @@ func Handshake(
initialStreamWindow: m.config.DefaultWindowSize,
streamWindowMax: m.config.MaxWindowSize,
streamWriteBufferMaxLen: m.config.StreamWriteBufferMaxLen,
r: m.r,
metricsUpdater: m.muxMetricsUpdater,
bytesRead: inBoundCounter,
r: m.r,
metricsUpdater: m.muxMetricsUpdater,
bytesRead: inBoundCounter,
}
m.muxWriter = &MuxWriter{
f: m.f,
@@ -379,33 +386,53 @@ 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, MuxedStreamRequest{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, MuxedStreamRequest{stream: stream, body: 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) AwaitResponseHeaders(ctx context.Context, stream *MuxedStream) error {
select {
case <-ctx.Done():
return ErrResponseHeadersTimeout
case <-m.abortChan:
return ErrResponseHeadersConnectionClosed
case <-stream.responseHeadersReceived:
return nil
}
}
+246 -196
View File
@@ -15,7 +15,15 @@ import (
"testing"
"time"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"golang.org/x/sync/errgroup"
)
const (
testOpenStreamTimeout = time.Millisecond * 5000
testHandshakeTimeout = time.Millisecond * 1000
)
func TestMain(m *testing.M) {
@@ -35,11 +43,12 @@ type DefaultMuxerPair struct {
doneC chan struct{}
}
func NewDefaultMuxerPair() *DefaultMuxerPair {
func NewDefaultMuxerPair(t assert.TestingT, 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()),
@@ -49,7 +58,7 @@ func NewDefaultMuxerPair() *DefaultMuxerPair {
},
OriginConn: origin,
EdgeMuxConfig: MuxerConfig{
Timeout: time.Second,
Timeout: testHandshakeTimeout,
IsClient: false,
Name: "edge",
Logger: log.NewEntry(log.New()),
@@ -60,13 +69,16 @@ func NewDefaultMuxerPair() *DefaultMuxerPair {
EdgeConn: edge,
doneC: make(chan struct{}),
}
assert.NoError(t, p.Handshake())
return p
}
func NewCompressedMuxerPair(quality CompressionSetting) *DefaultMuxerPair {
func NewCompressedMuxerPair(t assert.TestingT, 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,
@@ -83,44 +95,28 @@ func NewCompressedMuxerPair(quality CompressionSetting) *DefaultMuxerPair {
EdgeConn: edge,
doneC: make(chan struct{}),
}
assert.NoError(t, p.Handshake())
return p
}
func (p *DefaultMuxerPair) Handshake(t *testing.T) {
edgeErrC := make(chan error)
originErrC := make(chan error)
go func() {
var err error
func (p *DefaultMuxerPair) Handshake() 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)
edgeErrC <- err
}()
go func() {
var err error
return errors.Wrap(err, "edge handshake failure")
})
errGroup.Go(func() (err error) {
p.OriginMux, err = Handshake(p.OriginConn, p.OriginConn, p.OriginMuxConfig)
originErrC <- err
}()
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 +151,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, 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 +182,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 +191,10 @@ func TestSingleStream(t *testing.T) {
}
return nil
})
muxPair.HandshakeAndServe(t)
muxPair := NewDefaultMuxerPair(t, f)
muxPair.Serve(t)
stream, err := muxPair.EdgeMux.OpenStream(
stream, err := muxPair.OpenEdgeMuxStream(
[]Header{{Name: "test-header", Value: "headerValue"}},
nil,
)
@@ -222,7 +222,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 +229,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 +262,10 @@ func TestSingleStreamLargeResponseBody(t *testing.T) {
return nil
})
muxPair.HandshakeAndServe(t)
muxPair := NewDefaultMuxerPair(t, f)
muxPair.Serve(t)
stream, err := muxPair.EdgeMux.OpenStream(
stream, err := muxPair.OpenEdgeMuxStream(
[]Header{{Name: "test-header", Value: "headerValue"}},
nil,
)
@@ -295,10 +293,7 @@ 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 {
f := MuxedStreamFunc(func(stream *MuxedStream) error {
if len(stream.Headers) != 1 {
t.Fatalf("expected %d headers, got %d", 1, len(stream.Headers))
}
@@ -314,15 +309,18 @@ func TestMultipleStreams(t *testing.T) {
log.Debugf("Wrote body for stream %s", stream.Headers[0].Value)
return nil
})
muxPair.HandshakeAndServe(t)
muxPair := NewDefaultMuxerPair(t, 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,
)
@@ -373,13 +371,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,63 +402,48 @@ func TestMultipleStreamsFlowControl(t *testing.T) {
}
return nil
})
muxPair.HandshakeAndServe(t)
muxPair := NewDefaultMuxerPair(t, 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) {
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"},
})
@@ -479,18 +461,19 @@ func TestGracefulShutdown(t *testing.T) {
log.Debugf("Handler ends")
return nil
})
muxPair.HandshakeAndServe(t)
muxPair := NewDefaultMuxerPair(t, 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))
n, err := io.ReadFull(stream, responseBody)
@@ -511,8 +494,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 +516,10 @@ func TestUnexpectedShutdown(t *testing.T) {
}
return nil
})
muxPair.HandshakeAndServe(t)
muxPair := NewDefaultMuxerPair(t, f)
muxPair.Serve(t)
stream, err := muxPair.EdgeMux.OpenStream(
stream, err := muxPair.OpenEdgeMuxStream(
[]Header{{Name: "test-header", Value: "headerValue"}},
nil,
)
@@ -580,9 +564,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, EchoHandler)
muxPair.Serve(t)
switch i {
case 0:
@@ -597,22 +580,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, EchoHandler)
muxPair.Serve(t)
stream, err := muxPair.EdgeMux.OpenStream(
stream, err := muxPair.OpenEdgeMuxStream(
[]Header{
{Name: ":method", Value: "RPC"},
{Name: ":scheme", Value: "capnp"},
@@ -626,7 +608,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"},
@@ -688,8 +670,6 @@ func AssertIfPipeReadable(t *testing.T, pipe io.ReadCloser) {
func TestMultipleStreamsWithDictionaries(t *testing.T) {
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 +692,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 +724,8 @@ func TestMultipleStreamsWithDictionaries(t *testing.T) {
return nil
})
muxPair.HandshakeAndServe(t)
muxPair := NewCompressedMuxerPair(t, q, f)
muxPair.Serve(t)
var wg sync.WaitGroup
@@ -782,25 +762,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 {
@@ -836,42 +817,47 @@ func TestMultipleStreamsWithDictionaries(t *testing.T) {
}
}
func sampleSiteHandler(stream *MuxedStream) error {
var contentType string
var pathHeader Header
func sampleSiteHandler(files map[string][]byte) MuxedStreamFunc {
return func(stream *MuxedStream) error {
var contentType string
var pathHeader Header
for _, h := range stream.Headers {
if h.Name == ":path" {
pathHeader = h
break
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},
})
log.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)
log.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 +869,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, 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 +947,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, 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, 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())
}
+48 -1
View File
@@ -68,7 +68,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 +78,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 *ReadyList, 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 {
@@ -185,6 +213,25 @@ 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
}
func (s *MuxedStream) getReceiveWindow() uint32 {
s.writeLock.Lock()
defer s.writeLock.Unlock()
+27
View File
@@ -98,3 +98,30 @@ func TestMuxedStreamEOF(t *testing.T) {
assert.Equal(t, 0, n)
}
}
func TestIsRPCStream(t *testing.T) {
tests := []struct {
stream *MuxedStream
isRPCStream bool
}{
{
stream: &MuxedStream{},
isRPCStream: false,
},
{
stream: &MuxedStream{Headers: RPCHeaders()},
isRPCStream: true,
},
{
stream: &MuxedStream{Headers: []Header{
{Name: ":method", Value: "rpc"},
{Name: ":scheme", Value: "Capnp"},
{Name: ":path", Value: "/"},
}},
isRPCStream: false,
},
}
for _, test := range tests {
assert.Equal(t, test.isRPCStream, test.stream.IsRPCStream())
}
}
+6
View File
@@ -11,6 +11,10 @@ import (
"golang.org/x/net/http2"
)
const (
CloudflaredProxyTunnelHostnameHeader = "cf-cloudflared-proxy-tunnel-hostname"
)
type MuxReader struct {
// f is used to read HTTP2 frames.
f *http2.Framer
@@ -235,6 +239,8 @@ func (r *MuxReader) receiveHeaderData(frame *http2.MetaHeadersFrame) error {
if r.dictionaries.write != nil {
continue
}
case CloudflaredProxyTunnelHostnameHeader:
stream.tunnelHostname = TunnelHostname(header.Value)
}
headers = append(headers, Header{Name: header.Name, Value: header.Value})
}
+107
View File
@@ -0,0 +1,107 @@
package h2mux
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
var (
methodHeader = Header{
Name: ":method",
Value: "GET",
}
schemeHeader = Header{
Name: ":scheme",
Value: "https",
}
pathHeader = Header{
Name: ":path",
Value: "/api/tunnels",
}
tunnelHostnameHeader = Header{
Name: CloudflaredProxyTunnelHostnameHeader,
Value: "tunnel.example.com",
}
respStatusHeader = Header{
Name: ":status",
Value: "200",
}
)
type mockOriginStreamHandler struct {
stream *MuxedStream
}
func (mosh *mockOriginStreamHandler) ServeStream(stream *MuxedStream) error {
mosh.stream = stream
// Echo tunnel hostname in header
stream.WriteHeaders([]Header{respStatusHeader})
return nil
}
func getCloudflaredProxyTunnelHostnameHeader(stream *MuxedStream) string {
for _, header := range stream.Headers {
if header.Name == CloudflaredProxyTunnelHostnameHeader {
return header.Value
}
}
return ""
}
func assertOpenStreamSucceed(t *testing.T, stream *MuxedStream, err error) {
assert.NoError(t, err)
assert.Len(t, stream.Headers, 1)
assert.Equal(t, respStatusHeader, stream.Headers[0])
}
func TestMissingHeaders(t *testing.T) {
originHandler := &mockOriginStreamHandler{}
muxPair := NewDefaultMuxerPair(t, originHandler.ServeStream)
muxPair.Serve(t)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
reqHeaders := []Header{
{
Name: "content-type",
Value: "application/json",
},
}
// Request doesn't contain CloudflaredProxyTunnelHostnameHeader
stream, err := muxPair.EdgeMux.OpenStream(ctx, reqHeaders, nil)
assertOpenStreamSucceed(t, stream, err)
assert.Empty(t, originHandler.stream.method)
assert.Empty(t, originHandler.stream.path)
assert.False(t, originHandler.stream.TunnelHostname().IsSet())
}
func TestReceiveHeaderData(t *testing.T) {
originHandler := &mockOriginStreamHandler{}
muxPair := NewDefaultMuxerPair(t, originHandler.ServeStream)
muxPair.Serve(t)
reqHeaders := []Header{
methodHeader,
schemeHeader,
pathHeader,
tunnelHostnameHeader,
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
reqHeaders = append(reqHeaders, tunnelHostnameHeader)
stream, err := muxPair.EdgeMux.OpenStream(ctx, reqHeaders, nil)
assertOpenStreamSucceed(t, stream, err)
assert.Equal(t, methodHeader.Value, originHandler.stream.method)
assert.Equal(t, pathHeader.Value, originHandler.stream.path)
assert.True(t, originHandler.stream.TunnelHostname().IsSet())
assert.Equal(t, tunnelHostnameHeader.Value, originHandler.stream.TunnelHostname().String())
}
-19
View File
@@ -1,19 +0,0 @@
package origin
import (
"runtime"
)
type BuildInfo struct {
GoOS string `json:"go_os"`
GoVersion string `json:"go_version"`
GoArch string `json:"go_arch"`
}
func GetBuildInfo() *BuildInfo {
return &BuildInfo{
GoOS: runtime.GOOS,
GoVersion: runtime.Version(),
GoArch: runtime.GOARCH,
}
}
+13 -2
View File
@@ -55,8 +55,9 @@ type TunnelMetrics struct {
regFail *prometheus.CounterVec
rpcFail *prometheus.CounterVec
muxerMetrics *muxerMetrics
tunnelsHA tunnelsForHA
muxerMetrics *muxerMetrics
tunnelsHA tunnelsForHA
userHostnamesCounts *prometheus.CounterVec
}
func newMuxerMetrics() *muxerMetrics {
@@ -364,6 +365,15 @@ func NewTunnelMetrics() *TunnelMetrics {
)
prometheus.MustRegister(registerFail)
userHostnamesCounts := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "user_hostnames_counts",
Help: "Which user hostnames cloudflared is serving",
},
[]string{"userHostname"},
)
prometheus.MustRegister(userHostnamesCounts)
registerSuccess := prometheus.NewCounter(
prometheus.CounterOpts{
Name: "tunnel_register_success",
@@ -389,6 +399,7 @@ func NewTunnelMetrics() *TunnelMetrics {
regSuccess: registerSuccess,
regFail: registerFail,
rpcFail: rpcFail,
userHostnamesCounts: userHostnamesCounts,
}
}
+20 -3
View File
@@ -6,6 +6,9 @@ import (
"net"
"time"
"github.com/sirupsen/logrus"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/signal"
"github.com/google/uuid"
@@ -33,6 +36,8 @@ type Supervisor struct {
// currently-connecting tunnels to finish connecting so we can reset backoff timer
nextConnectedIndex int
nextConnectedSignal chan struct{}
logger *logrus.Entry
}
type resolveResult struct {
@@ -50,6 +55,7 @@ func NewSupervisor(config *TunnelConfig) *Supervisor {
config: config,
tunnelErrors: make(chan tunnelError),
tunnelsConnecting: map[int]chan struct{}{},
logger: config.Logger.WithField("subsystem", "supervisor"),
}
}
@@ -123,8 +129,10 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal, u
}
func (s *Supervisor) initialize(ctx context.Context, connectedSignal *signal.Signal, u uuid.UUID) error {
logger := s.config.Logger
edgeIPs, err := ResolveEdgeIPs(logger, s.config.EdgeAddrs)
logger := s.logger
edgeIPs, err := s.resolveEdgeIPs()
if err != nil {
logger.Infof("ResolveEdgeIPs err")
return err
@@ -214,6 +222,15 @@ func (s *Supervisor) getEdgeIP(index int) *net.TCPAddr {
return s.edgeIPs[index%len(s.edgeIPs)]
}
func (s *Supervisor) resolveEdgeIPs() ([]*net.TCPAddr, error) {
// If --edge is specfied, resolve edge server addresses
if len(s.config.EdgeAddrs) > 0 {
return connection.ResolveAddrs(s.config.EdgeAddrs)
}
// Otherwise lookup edge server addresses through service discovery
return connection.EdgeDiscovery(s.logger)
}
func (s *Supervisor) refreshEdgeIPs() {
if s.resolverC != nil {
return
@@ -223,7 +240,7 @@ func (s *Supervisor) refreshEdgeIPs() {
}
s.resolverC = make(chan resolveResult)
go func() {
edgeIPs, err := ResolveEdgeIPs(s.config.Logger, s.config.EdgeAddrs)
edgeIPs, err := s.resolveEdgeIPs()
s.resolverC <- resolveResult{edgeIPs: edgeIPs, err: err}
}()
}
+146 -158
View File
@@ -14,8 +14,10 @@ import (
"sync"
"time"
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/signal"
"github.com/cloudflare/cloudflared/streamhandler"
"github.com/cloudflare/cloudflared/tunnelrpc"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/cloudflare/cloudflared/validation"
@@ -33,43 +35,45 @@ import (
const (
dialTimeout = 15 * time.Second
openStreamTimeout = 30 * time.Second
lbProbeUserAgentPrefix = "Mozilla/5.0 (compatible; Cloudflare-Traffic-Manager/1.0; +https://www.cloudflare.com/traffic-manager/;"
TagHeaderNamePrefix = "Cf-Warp-Tag-"
DuplicateConnectionError = "EDUPCONN"
)
type TunnelConfig struct {
BuildInfo *buildinfo.BuildInfo
ClientID string
ClientTlsConfig *tls.Config
CloseConnOnce *sync.Once // Used to close connectedSignal no more than once
CompressionQuality uint64
EdgeAddrs []string
GracePeriod time.Duration
HAConnections int
HTTPTransport http.RoundTripper
HeartbeatInterval time.Duration
Hostname string
HTTPHostHeader string
IncidentLookup IncidentLookup
IsAutoupdated bool
IsFreeTunnel bool
LBPool string
Logger *log.Logger
MaxHeartbeats uint64
Metrics *TunnelMetrics
MetricsUpdateFreq time.Duration
NoChunkedEncoding bool
OriginCert []byte
ReportedVersion string
Retries uint
RunFromTerminal bool
Tags []tunnelpogs.Tag
TlsConfig *tls.Config
TransportLogger *log.Logger
UseDeclarativeTunnel bool
WSGI bool
// OriginUrl may not be used if a user specifies a unix socket.
OriginUrl string
EdgeAddrs []string
Hostname string
OriginCert []byte
TlsConfig *tls.Config
ClientTlsConfig *tls.Config
Retries uint
HeartbeatInterval time.Duration
MaxHeartbeats uint64
ClientID string
BuildInfo *BuildInfo
ReportedVersion string
LBPool string
Tags []tunnelpogs.Tag
HAConnections int
HTTPTransport http.RoundTripper
Metrics *TunnelMetrics
MetricsUpdateFreq time.Duration
TransportLogger *log.Logger
Logger *log.Logger
IsAutoupdated bool
GracePeriod time.Duration
RunFromTerminal bool
NoChunkedEncoding bool
WSGI bool
CompressionQuality uint64
IncidentLookup IncidentLookup
CloseConnOnce *sync.Once // Used to close connectedSignal no more than once
IsFreeTunnel bool
}
type dialError struct {
@@ -137,28 +141,8 @@ func (c *TunnelConfig) RegistrationOptions(connectionID uint8, OriginLocalIP str
}
}
func StartTunnelDaemon(config *TunnelConfig, shutdownC <-chan struct{}, connectedSignal *signal.Signal) error {
ctx, cancel := context.WithCancel(context.Background())
go func() {
<-shutdownC
cancel()
}()
u, err := uuid.NewRandom()
if err != nil {
return err
}
// If a user specified negative HAConnections, we will treat it as requesting 1 connection
if config.HAConnections > 1 {
return NewSupervisor(config).Run(ctx, connectedSignal, u)
} else {
addrs, err := ResolveEdgeIPs(config.Logger, config.EdgeAddrs)
if err != nil {
return err
}
return ServeTunnelLoop(ctx, config, addrs[0], 0, connectedSignal, u)
}
func StartTunnelDaemon(ctx context.Context, config *TunnelConfig, connectedSignal *signal.Signal, cloudflaredID uuid.UUID) error {
return NewSupervisor(config).Run(ctx, connectedSignal, cloudflaredID)
}
func ServeTunnelLoop(ctx context.Context,
@@ -323,11 +307,7 @@ func RegisterTunnel(
uuid uuid.UUID,
) error {
config.TransportLogger.Debug("initiating RPC stream to register")
stream, err := muxer.OpenStream([]h2mux.Header{
{Name: ":method", Value: "RPC"},
{Name: ":scheme", Value: "capnp"},
{Name: ":path", Value: "*"},
}, nil)
stream, err := openStream(ctx, muxer)
if err != nil {
// RPC stream open error
return newClientRegisterTunnelError(err, config.Metrics.rpcFail)
@@ -383,6 +363,8 @@ func RegisterTunnel(
}
}
config.Metrics.userHostnamesCounts.WithLabelValues(registration.Url).Inc()
config.Logger.Infof("Route propagating, it may take up to 1 minute for your new route to become functional")
return nil
}
@@ -405,11 +387,8 @@ func processRegisterTunnelError(err string, permanentFailure bool, metrics *Tunn
func UnregisterTunnel(muxer *h2mux.Muxer, gracePeriod time.Duration, logger *log.Logger) error {
logger.Debug("initiating RPC stream to unregister")
stream, err := muxer.OpenStream([]h2mux.Header{
{Name: ":method", Value: "RPC"},
{Name: ":scheme", Value: "capnp"},
{Name: ":path", Value: "*"},
}, nil)
ctx := context.Background()
stream, err := openStream(ctx, muxer)
if err != nil {
// RPC stream open error
return err
@@ -418,7 +397,6 @@ func UnregisterTunnel(muxer *h2mux.Muxer, gracePeriod time.Duration, logger *log
// stream response error
return err
}
ctx := context.Background()
conn := rpc.NewConn(
tunnelrpc.NewTransportLogger(logger.WithField("subsystem", "rpc-unregister"), rpc.StreamTransport(stream)),
tunnelrpc.ConnLog(logger.WithField("subsystem", "rpc-transport")),
@@ -429,6 +407,16 @@ func UnregisterTunnel(muxer *h2mux.Muxer, gracePeriod time.Duration, logger *log
return ts.UnregisterTunnel(ctx, gracePeriod.Nanoseconds())
}
func openStream(ctx context.Context, muxer *h2mux.Muxer) (*h2mux.MuxedStream, error) {
openStreamCtx, cancel := context.WithTimeout(ctx, openStreamTimeout)
defer cancel()
return muxer.OpenStream(openStreamCtx, []h2mux.Header{
{Name: ":method", Value: "RPC"},
{Name: ":scheme", Value: "capnp"},
{Name: ":path", Value: "*"},
}, nil)
}
func LogServerInfo(
promise tunnelrpc.ServerInfo_Promise,
connectionID uint8,
@@ -449,39 +437,6 @@ func LogServerInfo(
metrics.registerServerLocation(uint8ToString(connectionID), serverInfo.LocationName)
}
func H2RequestHeadersToH1Request(h2 []h2mux.Header, h1 *http.Request) error {
for _, header := range h2 {
switch header.Name {
case ":method":
h1.Method = header.Value
case ":scheme":
case ":authority":
// Otherwise the host header will be based on the origin URL
h1.Host = header.Value
case ":path":
u, err := url.Parse(header.Value)
if err != nil {
return fmt.Errorf("unparseable path")
}
resolved := h1.URL.ResolveReference(u)
// prevent escaping base URL
if !strings.HasPrefix(resolved.String(), h1.URL.String()) {
return fmt.Errorf("invalid path")
}
h1.URL = resolved
case "content-length":
contentLength, err := strconv.ParseInt(header.Value, 10, 64)
if err != nil {
return fmt.Errorf("unparseable content length")
}
h1.ContentLength = contentLength
default:
h1.Header.Add(http.CanonicalHeaderKey(header.Name), header.Value)
}
}
return nil
}
func H1ResponseToH2Response(h1 *http.Response) (h2 []h2mux.Header) {
h2 = []h2mux.Header{{Name: ":status", Value: fmt.Sprintf("%d", h1.StatusCode)}}
for headerName, headerValues := range h1.Header {
@@ -492,17 +447,14 @@ func H1ResponseToH2Response(h1 *http.Response) (h2 []h2mux.Header) {
return
}
func FindCfRayHeader(h1 *http.Request) string {
return h1.Header.Get("Cf-Ray")
}
type TunnelHandler struct {
originUrl string
muxer *h2mux.Muxer
httpClient http.RoundTripper
tlsConfig *tls.Config
tags []tunnelpogs.Tag
metrics *TunnelMetrics
originUrl string
httpHostHeader string
muxer *h2mux.Muxer
httpClient http.RoundTripper
tlsConfig *tls.Config
tags []tunnelpogs.Tag
metrics *TunnelMetrics
// connectionID is only used by metrics, and prometheus requires labels to be string
connectionID string
logger *log.Logger
@@ -523,6 +475,7 @@ func NewTunnelHandler(ctx context.Context,
}
h := &TunnelHandler{
originUrl: originURL,
httpHostHeader: config.HTTPHostHeader,
httpClient: config.HTTPTransport,
tlsConfig: config.ClientTlsConfig,
tags: config.Tags,
@@ -575,65 +528,103 @@ func (h *TunnelHandler) AppendTagHeaders(r *http.Request) {
func (h *TunnelHandler) ServeStream(stream *h2mux.MuxedStream) error {
h.metrics.incrementRequests(h.connectionID)
defer h.metrics.decrementConcurrentRequests(h.connectionID)
req, reqErr := h.createRequest(stream)
if reqErr != nil {
h.logError(stream, reqErr)
return reqErr
}
cfRay := streamhandler.FindCfRayHeader(req)
lbProbe := streamhandler.IsLBProbeRequest(req)
h.logRequest(req, cfRay, lbProbe)
var resp *http.Response
var respErr error
if websocket.IsWebSocketUpgrade(req) {
resp, respErr = h.serveWebsocket(stream, req)
} else {
resp, respErr = h.serveHTTP(stream, req)
}
if respErr != nil {
h.logError(stream, respErr)
return respErr
}
h.logResponseOk(resp, cfRay, lbProbe)
return nil
}
func (h *TunnelHandler) createRequest(stream *h2mux.MuxedStream) (*http.Request, error) {
req, err := http.NewRequest("GET", h.originUrl, h2mux.MuxedStreamReader{MuxedStream: stream})
if err != nil {
h.logger.WithError(err).Panic("Unexpected error from http.NewRequest")
return nil, errors.Wrap(err, "Unexpected error from http.NewRequest")
}
err = H2RequestHeadersToH1Request(stream.Headers, req)
err = streamhandler.H2RequestHeadersToH1Request(stream.Headers, req)
if err != nil {
h.logger.WithError(err).Error("invalid request received")
return nil, errors.Wrap(err, "invalid request received")
}
h.AppendTagHeaders(req)
cfRay := FindCfRayHeader(req)
lbProbe := isLBProbeRequest(req)
h.logRequest(req, cfRay, lbProbe)
if websocket.IsWebSocketUpgrade(req) {
conn, response, err := websocket.ClientConnect(req, h.tlsConfig)
if err != nil {
h.logError(stream, err)
} else {
stream.WriteHeaders(H1ResponseToH2Response(response))
defer conn.Close()
// Copy to/from stream to the undelying connection. Use the underlying
// connection because cloudflared doesn't operate on the message themselves
websocket.Stream(conn.UnderlyingConn(), stream)
h.metrics.incrementResponses(h.connectionID, "200")
h.logResponse(response, cfRay, lbProbe)
}
} else {
// Support for WSGI Servers by switching transfer encoding from chunked to gzip/deflate
if h.noChunkedEncoding {
req.TransferEncoding = []string{"gzip", "deflate"}
cLength, err := strconv.Atoi(req.Header.Get("Content-Length"))
if err == nil {
req.ContentLength = int64(cLength)
}
}
return req, nil
}
// Request origin to keep connection alive to improve performance
req.Header.Set("Connection", "keep-alive")
func (h *TunnelHandler) serveWebsocket(stream *h2mux.MuxedStream, req *http.Request) (*http.Response, error) {
if h.httpHostHeader != "" {
req.Header.Set("Host", h.httpHostHeader)
req.Host = h.httpHostHeader
}
response, err := h.httpClient.RoundTrip(req)
conn, response, err := websocket.ClientConnect(req, h.tlsConfig)
if err != nil {
return nil, err
}
defer conn.Close()
err = stream.WriteHeaders(H1ResponseToH2Response(response))
if err != nil {
return nil, errors.Wrap(err, "Error writing response header")
}
// Copy to/from stream to the undelying connection. Use the underlying
// connection because cloudflared doesn't operate on the message themselves
websocket.Stream(conn.UnderlyingConn(), stream)
return response, nil
}
if err != nil {
h.logError(stream, err)
} else {
defer response.Body.Close()
stream.WriteHeaders(H1ResponseToH2Response(response))
if h.isEventStream(response) {
h.writeEventStream(stream, response.Body)
} else {
// Use CopyBuffer, because Copy only allocates a 32KiB buffer, and cross-stream
// compression generates dictionary on first write
io.CopyBuffer(stream, response.Body, make([]byte, 512*1024))
}
h.metrics.incrementResponses(h.connectionID, "200")
h.logResponse(response, cfRay, lbProbe)
func (h *TunnelHandler) serveHTTP(stream *h2mux.MuxedStream, req *http.Request) (*http.Response, error) {
// Support for WSGI Servers by switching transfer encoding from chunked to gzip/deflate
if h.noChunkedEncoding {
req.TransferEncoding = []string{"gzip", "deflate"}
cLength, err := strconv.Atoi(req.Header.Get("Content-Length"))
if err == nil {
req.ContentLength = int64(cLength)
}
}
h.metrics.decrementConcurrentRequests(h.connectionID)
return nil
// Request origin to keep connection alive to improve performance
req.Header.Set("Connection", "keep-alive")
if h.httpHostHeader != "" {
req.Header.Set("Host", h.httpHostHeader)
req.Host = h.httpHostHeader
}
response, err := h.httpClient.RoundTrip(req)
if err != nil {
return nil, errors.Wrap(err, "Error proxying request to origin")
}
defer response.Body.Close()
err = stream.WriteHeaders(H1ResponseToH2Response(response))
if err != nil {
return nil, errors.Wrap(err, "Error writing response header")
}
if h.isEventStream(response) {
h.writeEventStream(stream, response.Body)
} else {
// Use CopyBuffer, because Copy only allocates a 32KiB buffer, and cross-stream
// compression generates dictionary on first write
io.CopyBuffer(stream, response.Body, make([]byte, 512*1024))
}
return response, nil
}
func (h *TunnelHandler) writeEventStream(stream *h2mux.MuxedStream, responseBody io.ReadCloser) {
@@ -681,7 +672,8 @@ func (h *TunnelHandler) logRequest(req *http.Request, cfRay string, lbProbe bool
}
}
func (h *TunnelHandler) logResponse(r *http.Response, cfRay string, lbProbe bool) {
func (h *TunnelHandler) logResponseOk(r *http.Response, cfRay string, lbProbe bool) {
h.metrics.incrementResponses(h.connectionID, "200")
logger := log.NewEntry(h.logger)
if cfRay != "" {
logger = logger.WithField("CF-RAY", cfRay)
@@ -708,10 +700,6 @@ func uint8ToString(input uint8) string {
return strconv.FormatUint(uint64(input), 10)
}
func isLBProbeRequest(req *http.Request) bool {
return strings.HasPrefix(req.UserAgent(), lbProbeUserAgentPrefix)
}
// Print out the given lines in a nice ASCII box.
func asciiBox(lines []string, padding int) (box []string) {
maxLen := maxLen(lines)
+235
View File
@@ -0,0 +1,235 @@
// Package client defines and implements interface to proxy to HTTP, websocket and hello world origins
package originservice
import (
"bufio"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/hello"
"github.com/cloudflare/cloudflared/log"
"github.com/cloudflare/cloudflared/websocket"
"github.com/pkg/errors"
)
// OriginService is an interface to proxy requests to different type of origins
type OriginService interface {
Proxy(stream *h2mux.MuxedStream, req *http.Request) (resp *http.Response, err error)
URL() *url.URL
Summary() string
Shutdown()
}
// HTTPService talks to origin using HTTP/HTTPS
type HTTPService struct {
client http.RoundTripper
originURL *url.URL
chunkedEncoding bool
}
func NewHTTPService(transport http.RoundTripper, url *url.URL, chunkedEncoding bool) OriginService {
return &HTTPService{
client: transport,
originURL: url,
chunkedEncoding: chunkedEncoding,
}
}
func (hc *HTTPService) Proxy(stream *h2mux.MuxedStream, req *http.Request) (*http.Response, error) {
// Support for WSGI Servers by switching transfer encoding from chunked to gzip/deflate
if !hc.chunkedEncoding {
req.TransferEncoding = []string{"gzip", "deflate"}
cLength, err := strconv.Atoi(req.Header.Get("Content-Length"))
if err == nil {
req.ContentLength = int64(cLength)
}
}
// Request origin to keep connection alive to improve performance
req.Header.Set("Connection", "keep-alive")
resp, err := hc.client.RoundTrip(req)
if err != nil {
return nil, errors.Wrap(err, "error proxying request to HTTP origin")
}
defer resp.Body.Close()
err = stream.WriteHeaders(h1ResponseToH2Response(resp))
if err != nil {
return nil, errors.Wrap(err, "error writing response header to HTTP origin")
}
if isEventStream(resp) {
writeEventStream(stream, resp.Body)
} else {
// Use CopyBuffer, because Copy only allocates a 32KiB buffer, and cross-stream
// compression generates dictionary on first write
io.CopyBuffer(stream, resp.Body, make([]byte, 512*1024))
}
return resp, nil
}
func (hc *HTTPService) URL() *url.URL {
return hc.originURL
}
func (hc *HTTPService) Summary() string {
return fmt.Sprintf("HTTP service listening on %s", hc.originURL)
}
func (hc *HTTPService) Shutdown() {}
// WebsocketService talks to origin using WS/WSS
type WebsocketService struct {
tlsConfig *tls.Config
originURL *url.URL
shutdownC chan struct{}
}
func NewWebSocketService(tlsConfig *tls.Config, url *url.URL) (OriginService, error) {
listener, err := net.Listen("tcp", "127.0.0.1:")
if err != nil {
return nil, errors.Wrap(err, "cannot start Websocket Proxy Server")
}
shutdownC := make(chan struct{})
go func() {
websocket.StartProxyServer(log.CreateLogger(), listener, url.String(), shutdownC)
}()
return &WebsocketService{
tlsConfig: tlsConfig,
originURL: url,
shutdownC: shutdownC,
}, nil
}
func (wsc *WebsocketService) Proxy(stream *h2mux.MuxedStream, req *http.Request) (*http.Response, error) {
if !websocket.IsWebSocketUpgrade(req) {
return nil, fmt.Errorf("request is not a websocket connection")
}
conn, response, err := websocket.ClientConnect(req, wsc.tlsConfig)
if err != nil {
return nil, err
}
defer conn.Close()
err = stream.WriteHeaders(h1ResponseToH2Response(response))
if err != nil {
return nil, errors.Wrap(err, "error writing response header to websocket origin")
}
// Copy to/from stream to the undelying connection. Use the underlying
// connection because cloudflared doesn't operate on the message themselves
websocket.Stream(conn.UnderlyingConn(), stream)
return response, nil
}
func (wsc *WebsocketService) URL() *url.URL {
return wsc.originURL
}
func (wsc *WebsocketService) Summary() string {
return fmt.Sprintf("Websocket listening on %s", wsc.originURL)
}
func (wsc *WebsocketService) Shutdown() {
close(wsc.shutdownC)
}
// HelloWorldService talks to the hello world example origin
type HelloWorldService struct {
client http.RoundTripper
listener net.Listener
originURL *url.URL
shutdownC chan struct{}
}
func NewHelloWorldService(transport http.RoundTripper) (OriginService, error) {
listener, err := hello.CreateTLSListener("127.0.0.1:")
if err != nil {
return nil, errors.Wrap(err, "cannot start Hello World Server")
}
shutdownC := make(chan struct{})
go func() {
hello.StartHelloWorldServer(log.CreateLogger(), listener, shutdownC)
}()
return &HelloWorldService{
client: transport,
listener: listener,
originURL: &url.URL{
Scheme: "https",
Host: listener.Addr().String(),
},
shutdownC: shutdownC,
}, nil
}
func (hwc *HelloWorldService) Proxy(stream *h2mux.MuxedStream, req *http.Request) (*http.Response, error) {
// Request origin to keep connection alive to improve performance
req.Header.Set("Connection", "keep-alive")
resp, err := hwc.client.RoundTrip(req)
if err != nil {
return nil, errors.Wrap(err, "error proxying request to Hello World origin")
}
defer resp.Body.Close()
err = stream.WriteHeaders(h1ResponseToH2Response(resp))
if err != nil {
return nil, errors.Wrap(err, "error writing response header to Hello World origin")
}
// Use CopyBuffer, because Copy only allocates a 32KiB buffer, and cross-stream
// compression generates dictionary on first write
io.CopyBuffer(stream, resp.Body, make([]byte, 512*1024))
return resp, nil
}
func (hwc *HelloWorldService) URL() *url.URL {
return hwc.originURL
}
func (hwc *HelloWorldService) Summary() string {
return fmt.Sprintf("Hello World service listening on %s", hwc.originURL)
}
func (hwc *HelloWorldService) Shutdown() {
hwc.listener.Close()
}
func isEventStream(resp *http.Response) bool {
// Check if content-type is text/event-stream. We need to check if the header value starts with text/event-stream
// because text/event-stream; charset=UTF-8 is also valid
// Ref: https://tools.ietf.org/html/rfc7231#section-3.1.1.1
for _, contentType := range resp.Header["content-type"] {
if strings.HasPrefix(strings.ToLower(contentType), "text/event-stream") {
return true
}
}
return false
}
func writeEventStream(stream *h2mux.MuxedStream, respBody io.ReadCloser) {
reader := bufio.NewReader(respBody)
for {
line, err := reader.ReadBytes('\n')
if err != nil {
break
}
stream.Write(line)
}
}
func h1ResponseToH2Response(h1 *http.Response) (h2 []h2mux.Header) {
h2 = []h2mux.Header{{Name: ":status", Value: fmt.Sprintf("%d", h1.StatusCode)}}
for headerName, headerValues := range h1.Header {
for _, headerValue := range headerValues {
h2 = append(h2, h2mux.Header{Name: strings.ToLower(headerName), Value: headerValue})
}
}
return
}
+60
View File
@@ -0,0 +1,60 @@
package originservice
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsEventStream(t *testing.T) {
tests := []struct {
resp *http.Response
isEventStream bool
}{
{
resp: &http.Response{},
isEventStream: false,
},
{
// isEventStream checks all headers
resp: &http.Response{
Header: http.Header{
"accept": []string{"text/html"},
"content-type": []string{"text/event-stream"},
},
},
isEventStream: true,
},
{
// Content-Type and text/event-stream are case-insensitive. text/event-stream can be followed by OWS parameter
resp: &http.Response{
Header: http.Header{
"content-type": []string{"Text/event-stream;charset=utf-8"},
},
},
isEventStream: true,
},
{
// Content-Type and text/event-stream are case-insensitive. text/event-stream can be followed by OWS parameter
resp: &http.Response{
Header: http.Header{
"content-type": []string{"appication/json", "text/html", "Text/event-stream;charset=utf-8"},
},
},
isEventStream: true,
},
{
// Not an event stream because the content-type value doesn't start with text/event-stream
resp: &http.Response{
Header: http.Header{
"content-type": []string{" text/event-stream"},
},
},
isEventStream: false,
},
}
for _, test := range tests {
assert.Equal(t, test.isEventStream, isEventStream(test.resp), "Header: %v", test.resp.Header)
}
}
+188
View File
@@ -0,0 +1,188 @@
package sshgen
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"time"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
cfpath "github.com/cloudflare/cloudflared/cmd/cloudflared/path"
"github.com/coreos/go-oidc/jose"
homedir "github.com/mitchellh/go-homedir"
gossh "golang.org/x/crypto/ssh"
)
const (
signEndpoint = "/cdn-cgi/access/cert_sign"
keyName = "cf_key"
)
// signPayload represents the request body sent to the sign handler API
type signPayload struct {
PublicKey string `json:"public_key"`
JWT string `json:"jwt"`
Issuer string `json:"issuer"`
}
// signResponse represents the response body from the sign handler API
type signResponse struct {
KeyID string `json:"id"`
Certificate string `json:"certificate"`
ExpiresAt time.Time `json:"expires_at"`
}
// ErrorResponse struct stores error information after any error-prone function
type errorResponse struct {
Status int `json:"status"`
Message string `json:"message"`
}
var mockRequest func(url, contentType string, body io.Reader) (*http.Response, error) = nil
// GenerateShortLivedCertificate generates and stores a keypair for short lived certs
func GenerateShortLivedCertificate(appURL *url.URL, token string) error {
fullName, err := cfpath.GenerateFilePathFromURL(appURL, keyName)
if err != nil {
return err
}
cert, err := handleCertificateGeneration(token, fullName)
if err != nil {
return err
}
name := fullName + "-cert.pub"
if err := writeKey(name, []byte(cert)); err != nil {
return err
}
return nil
}
// handleCertificateGeneration takes a JWT and uses it build a signPayload
// to send to the Sign endpoint with the public key from the keypair it generated
func handleCertificateGeneration(token, fullName string) (string, error) {
if token == "" {
return "", errors.New("invalid token")
}
jwt, err := jose.ParseJWT(token)
if err != nil {
return "", err
}
claims, err := jwt.Claims()
if err != nil {
return "", err
}
issuer, _, err := claims.StringClaim("iss")
if err != nil {
return "", err
}
pub, err := generateKeyPair(fullName)
if err != nil {
return "", err
}
buf, err := json.Marshal(&signPayload{
PublicKey: string(pub),
JWT: token,
Issuer: issuer,
})
if err != nil {
return "", err
}
var res *http.Response
if mockRequest != nil {
res, err = mockRequest(issuer+signEndpoint, "application/json", bytes.NewBuffer(buf))
} else {
res, err = http.Post(issuer+signEndpoint, "application/json", bytes.NewBuffer(buf))
}
if err != nil {
return "", err
}
defer res.Body.Close()
decoder := json.NewDecoder(res.Body)
if res.StatusCode != 200 {
var errResponse errorResponse
if err := decoder.Decode(&errResponse); err != nil {
return "", err
}
return "", fmt.Errorf("%d: %s", errResponse.Status, errResponse.Message)
}
var signRes signResponse
if err := decoder.Decode(&signRes); err != nil {
return "", err
}
return signRes.Certificate, err
}
// generateKeyPair creates a EC keypair (P256) and stores them in the homedir.
// returns the generated public key from the successful keypair generation
func generateKeyPair(fullName string) ([]byte, error) {
pubKeyName := fullName + ".pub"
exist, err := config.FileExists(pubKeyName)
if err != nil {
return nil, err
}
if exist {
return ioutil.ReadFile(pubKeyName)
}
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
parsed, err := x509.MarshalECPrivateKey(key)
if err != nil {
return nil, err
}
if err := writeKey(fullName, pem.EncodeToMemory(&pem.Block{
Type: "EC PRIVATE KEY",
Bytes: parsed,
})); err != nil {
return nil, err
}
pub, err := gossh.NewPublicKey(&key.PublicKey)
if err != nil {
return nil, err
}
data := gossh.MarshalAuthorizedKey(pub)
if err := writeKey(pubKeyName, data); err != nil {
return nil, err
}
return data, nil
}
// writeKey will write a key to disk in DER format (it's a standard pem key)
func writeKey(filename string, data []byte) error {
filepath, err := homedir.Expand(filename)
if err != nil {
return err
}
return ioutil.WriteFile(filepath, data, 0600)
}
+112
View File
@@ -0,0 +1,112 @@
package sshgen
import (
"crypto/rand"
"crypto/rsa"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
"time"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
cfpath "github.com/cloudflare/cloudflared/cmd/cloudflared/path"
"github.com/coreos/go-oidc/jose"
"github.com/stretchr/testify/assert"
)
const (
audTest = "cf-test-aud"
nonceTest = "asfd"
)
type signingArguments struct {
Principals []string `json:"principals"`
ClientPubKey string `json:"public_key"`
Duration string `json:"duration"`
}
func TestCertGenSuccess(t *testing.T) {
url, _ := url.Parse("https://cf-test-access.com/testpath")
token := tokenGenerator()
fullName, err := cfpath.GenerateFilePathFromURL(url, keyName)
assert.NoError(t, err)
pubKeyName := fullName + ".pub"
certKeyName := fullName + "-cert.pub"
defer func() {
os.Remove(fullName)
os.Remove(pubKeyName)
os.Remove(certKeyName)
}()
resp := signingArguments{
Principals: []string{"dalton"},
ClientPubKey: "ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg+0rYq4mNGIAHiH1xPOJXfmOpTEwFIcyXzGJieTOhRs8AAAAIbmlzdHAyNTYAAABBBJIcsq02e8ZaofJXOZKp7yQdKW/JIouJ90lybr76hHIRrZBL1t4JEimfLvNDphPrTW9VDQaIcBSKNaxRqHOS8jezoJbhFGWhqQAAAAEAAAAgZWU5OTliNGRkZmFmNjgxNDEwMTVhMDJiY2ZhMTdiN2UAAAAKAAAABmF1c3RpbgAAAABc1KFoAAAAAFzUohwAAAAAAAAARwAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAEEEeAuYR56XaxvH5Z1p0hDCTQ7wC4dbj0Gc+LOKu1f94og2ilZTv9tutg8cZrqAT97REmGH6j9KIOVLGsPVjajSKAAAAGQAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAEkAAAAhAORY9ZO3TQsrUm6ajnVW+arbnVfTkxYBYFlVoeOEXKZuAAAAIG96A8nQnTuprWXLSemWL68RXC1NVKnBOIPD2Z7UIOB1",
Duration: "3m",
}
w := httptest.NewRecorder()
respJson, err := json.Marshal(resp)
assert.NoError(t, err)
w.Write(respJson)
mockRequest = func(url, contentType string, body io.Reader) (*http.Response, error) {
assert.Contains(t, "/cdn-cgi/access/cert_sign", url)
assert.Equal(t, "application/json", contentType)
buf, err := ioutil.ReadAll(body)
assert.NoError(t, err)
assert.NotEmpty(t, buf)
return w.Result(), nil
}
err = GenerateShortLivedCertificate(url, token)
assert.NoError(t, err)
exist, err := config.FileExists(fullName)
assert.NoError(t, err)
if !exist {
assert.FailNow(t, fmt.Sprintf("key should exist at: %s", fullName), fullName)
return
}
exist, err = config.FileExists(pubKeyName)
assert.NoError(t, err)
if !exist {
assert.FailNow(t, fmt.Sprintf("key should exist at: %s", pubKeyName), pubKeyName)
return
}
exist, err = config.FileExists(certKeyName)
assert.NoError(t, err)
if !exist {
assert.FailNow(t, fmt.Sprintf("key should exist at: %s", certKeyName), certKeyName)
return
}
}
func tokenGenerator() string {
iat := time.Now().Unix()
exp := time.Now().Add(time.Minute * 5).Unix()
claims := jose.Claims{}
claims.Add("aud", audTest)
claims.Add("iat", iat)
claims.Add("nonce", nonceTest)
claims.Add("exp", exp)
k, err := rsa.GenerateKey(rand.Reader, 512)
if err != nil {
return ""
}
signer := jose.NewSignerRSA("asdf", *k)
token, terr := jose.NewSignedJWT(claims, signer)
if terr != nil {
return ""
}
return token.Encode()
}
+156
View File
@@ -0,0 +1,156 @@
package sshlog
import (
"bufio"
"fmt"
"os"
"path/filepath"
"sync"
"time"
"github.com/sirupsen/logrus"
)
const (
logTimeFormat = "2006-01-02T15-04-05.000"
megabyte = 1024 * 1024
)
// Logger will buffer and write events to disk
type Logger struct {
sync.Mutex
filename string
file *os.File
writeBuffer *bufio.Writer
logger *logrus.Logger
done chan struct{}
once sync.Once
}
// NewLogger creates a Logger instance. A buffer is created that needs to be
// drained and closed when the caller is finished, so instances should call
// Close when finished with this Logger instance. Writes will be flushed to disk
// every second (fsync). filename is the name of the logfile to be created. The
// logger variable is a logrus that will log all i/o, filesystem error etc, that
// that shouldn't end execution of the logger, but are useful to report to the
// caller.
func NewLogger(filename string, logger *logrus.Logger) (*Logger, error) {
f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(0600))
if err != nil {
return nil, err
}
l := &Logger{filename: filename,
file: f,
writeBuffer: bufio.NewWriter(f),
logger: logger,
done: make(chan struct{})}
go l.writer()
return l, nil
}
// Writes to a log buffer. Implements the io.Writer interface.
func (l *Logger) Write(p []byte) (n int, err error) {
l.Lock()
defer l.Unlock()
return l.writeBuffer.Write(p)
}
// Close drains anything left in the buffer and cleans up any resources still
// in use.
func (l *Logger) Close() error {
l.once.Do(func() {
close(l.done)
})
if err := l.write(); err != nil {
return err
}
return l.file.Close()
}
// writer is the run loop that handles draining the write buffer and syncing
// data to disk.
func (l *Logger) writer() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := l.write(); err != nil {
l.logger.Errorln(err)
}
case <-l.done:
return
}
}
}
// write does the actual system write calls to disk and does a rotation if the
// file size limit has been reached. Since the rotation happens at the end,
// the rotation is a soft limit (aka the file can be bigger than the max limit
// because of the final buffer flush)
func (l *Logger) write() error {
l.Lock()
defer l.Unlock()
if l.writeBuffer.Buffered() <= 0 {
return nil
}
if err := l.writeBuffer.Flush(); err != nil {
return err
}
if err := l.file.Sync(); err != nil {
return err
}
if l.shouldRotate() {
return l.rotate()
}
return nil
}
// shouldRotate checks to see if the current file should be rotated to a new
// logfile.
func (l *Logger) shouldRotate() bool {
info, err := l.file.Stat()
if err != nil {
return false
}
return info.Size() >= 100*megabyte
}
// rotate creates a new logfile with the existing filename and renames the
// existing file with a current timestamp.
func (l *Logger) rotate() error {
if err := l.file.Close(); err != nil {
return err
}
// move the existing file
newname := rotationName(l.filename)
if err := os.Rename(l.filename, newname); err != nil {
return fmt.Errorf("can't rename log file: %s", err)
}
f, err := os.OpenFile(l.filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(0600))
if err != nil {
return fmt.Errorf("failed to open new logfile %s", err)
}
l.file = f
l.writeBuffer = bufio.NewWriter(f)
return nil
}
// rotationName creates a new filename from the given name, inserting a timestamp
// between the filename and the extension.
func rotationName(name string) string {
dir := filepath.Dir(name)
filename := filepath.Base(name)
ext := filepath.Ext(filename)
prefix := filename[:len(filename)-len(ext)]
t := time.Now()
timestamp := t.Format(logTimeFormat)
return filepath.Join(dir, fmt.Sprintf("%s-%s%s", prefix, timestamp, ext))
}
+23
View File
@@ -0,0 +1,23 @@
package sshlog
import (
"io"
"github.com/sirupsen/logrus"
)
// Manager be managing logs bruh
type Manager interface {
NewLogger(string, *logrus.Logger) (io.WriteCloser, error)
}
type manager struct{}
// New creates a new instance of a log manager
func New() Manager {
return &manager{}
}
func (m *manager) NewLogger(name string, logger *logrus.Logger) (io.WriteCloser, error) {
return NewLogger(name, logger)
}
+109
View File
@@ -0,0 +1,109 @@
//+build !windows
package sshserver
import (
"fmt"
"io/ioutil"
"os"
"path"
"github.com/gliderlabs/ssh"
"github.com/pkg/errors"
gossh "golang.org/x/crypto/ssh"
)
var (
systemConfigPath = "/etc/cloudflared/"
authorizedKeysDir = ".cloudflared/authorized_keys"
)
func (s *SSHServer) configureAuthentication() {
caCert, err := getCACert()
if err != nil {
s.logger.Info(err)
}
s.caCert = caCert
s.PublicKeyHandler = s.authenticationHandler
}
func (s *SSHServer) authenticationHandler(ctx ssh.Context, key ssh.PublicKey) bool {
cert, ok := key.(*gossh.Certificate)
if !ok {
return s.authorizedKeyHandler(ctx, key)
}
return s.shortLivedCertHandler(ctx, cert)
}
func (s *SSHServer) authorizedKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool {
sshUser, err := s.getUserFunc(ctx.User())
if err != nil {
s.logger.Debugf("Invalid user: %s", ctx.User())
return false
}
authorizedKeysPath := path.Join(sshUser.HomeDir, authorizedKeysDir)
if _, err := os.Stat(authorizedKeysPath); os.IsNotExist(err) {
s.logger.Debugf("authorized_keys file %s not found", authorizedKeysPath)
return false
}
authorizedKeysBytes, err := ioutil.ReadFile(authorizedKeysPath)
if err != nil {
s.logger.WithError(err).Errorf("Failed to load authorized_keys %s", authorizedKeysPath)
return false
}
for len(authorizedKeysBytes) > 0 {
// Skips invalid keys. Returns error if no valid keys remain.
pubKey, _, _, rest, err := ssh.ParseAuthorizedKey(authorizedKeysBytes)
authorizedKeysBytes = rest
if err != nil {
s.logger.Errorf("Invalid key(s) found in %s", authorizedKeysPath)
return false
}
if ssh.KeysEqual(pubKey, key) {
ctx.SetValue("sshUser", sshUser)
return true
}
}
s.logger.Debugf("Matching public key not found in %s", authorizedKeysPath)
return false
}
func (s *SSHServer) shortLivedCertHandler(ctx ssh.Context, cert *gossh.Certificate) bool {
if !ssh.KeysEqual(s.caCert, cert.SignatureKey) {
s.logger.Debug("CA certificate does not match user certificate signer")
return false
}
checker := gossh.CertChecker{}
if err := checker.CheckCert(ctx.User(), cert); err != nil {
s.logger.Debug(err)
return false
} else {
sshUser, err := s.getUserFunc(ctx.User())
if err != nil {
s.logger.Debugf("Invalid user: %s", ctx.User())
return false
}
ctx.SetValue("sshUser", sshUser)
}
return true
}
func getCACert() (ssh.PublicKey, error) {
caCertPath := path.Join(systemConfigPath, "ca.pub")
caCertBytes, err := ioutil.ReadFile(caCertPath)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("Failed to load CA certertificate %s", caCertPath))
}
caCert, _, _, _, err := ssh.ParseAuthorizedKey(caCertBytes)
if err != nil {
return nil, errors.Wrap(err, "Failed to parse CA Certificate")
}
return caCert, nil
}
+197
View File
@@ -0,0 +1,197 @@
//+build !windows
package sshserver
import (
"context"
"io/ioutil"
"net"
"os"
"os/user"
"path"
"sync"
"testing"
"github.com/cloudflare/cloudflared/log"
"github.com/gliderlabs/ssh"
"github.com/sirupsen/logrus"
"github.com/sirupsen/logrus/hooks/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
gossh "golang.org/x/crypto/ssh"
)
const (
validPrincipal = "testUser"
testDir = "testdata"
testUserKeyFilename = "id_rsa.pub"
testCAFilename = "ca.pub"
testOtherCAFilename = "other_ca.pub"
testUserCertFilename = "id_rsa-cert.pub"
)
var logger, hook = test.NewNullLogger()
func TestMain(m *testing.M) {
authorizedKeysDir = testUserKeyFilename
logger.SetLevel(logrus.DebugLevel)
code := m.Run()
os.Exit(code)
}
func TestPublicKeyAuth_Success(t *testing.T) {
context, cancel := newMockContext(validPrincipal)
defer cancel()
sshServer := SSHServer{getUserFunc: getMockUser}
pubKey := getKey(t, testUserKeyFilename)
assert.True(t, sshServer.authorizedKeyHandler(context, pubKey))
}
func TestPublicKeyAuth_MissingKey(t *testing.T) {
context, cancel := newMockContext(validPrincipal)
defer cancel()
sshServer := SSHServer{logger: logger, getUserFunc: getMockUser}
pubKey := getKey(t, testOtherCAFilename)
assert.False(t, sshServer.authorizedKeyHandler(context, pubKey))
assert.Contains(t, hook.LastEntry().Message, "Matching public key not found in")
}
func TestPublicKeyAuth_InvalidUser(t *testing.T) {
context, cancel := newMockContext("notAUser")
defer cancel()
sshServer := SSHServer{logger: logger, getUserFunc: lookupUser}
pubKey := getKey(t, testUserKeyFilename)
assert.False(t, sshServer.authorizedKeyHandler(context, pubKey))
assert.Contains(t, hook.LastEntry().Message, "Invalid user")
}
func TestPublicKeyAuth_MissingFile(t *testing.T) {
currentUser, err := user.Current()
require.Nil(t, err)
context, cancel := newMockContext(currentUser.Username)
defer cancel()
sshServer := SSHServer{Server: ssh.Server{}, logger: logger, getUserFunc: lookupUser}
pubKey := getKey(t, testUserKeyFilename)
assert.False(t, sshServer.authorizedKeyHandler(context, pubKey))
assert.Contains(t, hook.LastEntry().Message, "not found")
}
func TestShortLivedCerts_Success(t *testing.T) {
context, cancel := newMockContext(validPrincipal)
defer cancel()
caCert := getKey(t, testCAFilename)
sshServer := SSHServer{logger: log.CreateLogger(), caCert: caCert, getUserFunc: getMockUser}
userCert, ok := getKey(t, testUserCertFilename).(*gossh.Certificate)
require.True(t, ok)
assert.True(t, sshServer.shortLivedCertHandler(context, userCert))
}
func TestShortLivedCerts_CAsDontMatch(t *testing.T) {
context, cancel := newMockContext(validPrincipal)
defer cancel()
caCert := getKey(t, testOtherCAFilename)
sshServer := SSHServer{logger: logger, caCert: caCert, getUserFunc: getMockUser}
userCert, ok := getKey(t, testUserCertFilename).(*gossh.Certificate)
require.True(t, ok)
assert.False(t, sshServer.shortLivedCertHandler(context, userCert))
assert.Equal(t, "CA certificate does not match user certificate signer", hook.LastEntry().Message)
}
func TestShortLivedCerts_UserDoesNotExist(t *testing.T) {
context, cancel := newMockContext(validPrincipal)
defer cancel()
caCert := getKey(t, testCAFilename)
sshServer := SSHServer{logger: logger, caCert: caCert, getUserFunc: lookupUser}
userCert, ok := getKey(t, testUserCertFilename).(*gossh.Certificate)
require.True(t, ok)
assert.False(t, sshServer.shortLivedCertHandler(context, userCert))
assert.Contains(t, hook.LastEntry().Message, "Invalid user")
}
func TestShortLivedCerts_InvalidPrincipal(t *testing.T) {
context, cancel := newMockContext("notAUser")
defer cancel()
caCert := getKey(t, testCAFilename)
sshServer := SSHServer{logger: logger, caCert: caCert, getUserFunc: lookupUser}
userCert, ok := getKey(t, testUserCertFilename).(*gossh.Certificate)
require.True(t, ok)
assert.False(t, sshServer.shortLivedCertHandler(context, userCert))
assert.Contains(t, hook.LastEntry().Message, "not in the set of valid principals for given certificate")
}
func getMockUser(_ string) (*User, error) {
return &User{
Username: validPrincipal,
HomeDir: testDir,
}, nil
}
func getKey(t *testing.T, filename string) ssh.PublicKey {
path := path.Join(testDir, filename)
bytes, err := ioutil.ReadFile(path)
require.Nil(t, err)
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(bytes)
require.Nil(t, err)
return pubKey
}
type mockSSHContext struct {
context.Context
*sync.Mutex
}
func newMockContext(user string) (*mockSSHContext, context.CancelFunc) {
innerCtx, cancel := context.WithCancel(context.Background())
mockCtx := &mockSSHContext{innerCtx, &sync.Mutex{}}
mockCtx.SetValue("user", user)
return mockCtx, cancel
}
func (ctx *mockSSHContext) SetValue(key, value interface{}) {
ctx.Context = context.WithValue(ctx.Context, key, value)
}
func (ctx *mockSSHContext) User() string {
return ctx.Value("user").(string)
}
func (ctx *mockSSHContext) SessionID() string {
return ""
}
func (ctx *mockSSHContext) ClientVersion() string {
return ""
}
func (ctx *mockSSHContext) ServerVersion() string {
return ""
}
func (ctx *mockSSHContext) RemoteAddr() net.Addr {
return nil
}
func (ctx *mockSSHContext) LocalAddr() net.Addr {
return nil
}
func (ctx *mockSSHContext) Permissions() *ssh.Permissions {
return nil
}
+226
View File
@@ -0,0 +1,226 @@
// Taken from https://github.com/golang/go/blob/ad644d2e86bab85787879d41c2d2aebbd7c57db8/src/os/user/user.go
// and modified to return login shell in User struct. cloudflared requires cgo for compilation because of this addition.
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build aix darwin dragonfly freebsd !android,linux netbsd openbsd solaris
// +build cgo,!osusergo
package sshserver
import (
"fmt"
"strconv"
"strings"
"syscall"
"unsafe"
)
/*
#cgo solaris CFLAGS: -D_POSIX_PTHREAD_SEMANTICS
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
#include <grp.h>
#include <stdlib.h>
static int mygetpwuid_r(int uid, struct passwd *pwd,
char *buf, size_t buflen, struct passwd **result) {
return getpwuid_r(uid, pwd, buf, buflen, result);
}
static int mygetpwnam_r(const char *name, struct passwd *pwd,
char *buf, size_t buflen, struct passwd **result) {
return getpwnam_r(name, pwd, buf, buflen, result);
}
static int mygetgrgid_r(int gid, struct group *grp,
char *buf, size_t buflen, struct group **result) {
return getgrgid_r(gid, grp, buf, buflen, result);
}
static int mygetgrnam_r(const char *name, struct group *grp,
char *buf, size_t buflen, struct group **result) {
return getgrnam_r(name, grp, buf, buflen, result);
}
*/
import "C"
type UnknownUserIdError int
func (e UnknownUserIdError) Error() string {
return "user: unknown userid " + strconv.Itoa(int(e))
}
// UnknownUserError is returned by Lookup when
// a user cannot be found.
type UnknownUserError string
func (e UnknownUserError) Error() string {
return "user: unknown user " + string(e)
}
// UnknownGroupIdError is returned by LookupGroupId when
// a group cannot be found.
type UnknownGroupIdError string
func (e UnknownGroupIdError) Error() string {
return "group: unknown groupid " + string(e)
}
// UnknownGroupError is returned by LookupGroup when
// a group cannot be found.
type UnknownGroupError string
func (e UnknownGroupError) Error() string {
return "group: unknown group " + string(e)
}
type User struct {
// Uid is the user ID.
// On POSIX systems, this is a decimal number representing the uid.
// On Windows, this is a security identifier (SID) in a string format.
// On Plan 9, this is the contents of /dev/user.
Uid string
// Gid is the primary group ID.
// On POSIX systems, this is a decimal number representing the gid.
// On Windows, this is a SID in a string format.
// On Plan 9, this is the contents of /dev/user.
Gid string
// Username is the login name.
Username string
// Name is the user's real or display name.
// It might be blank.
// On POSIX systems, this is the first (or only) entry in the GECOS field
// list.
// On Windows, this is the user's display name.
// On Plan 9, this is the contents of /dev/user.
Name string
// HomeDir is the path to the user's home directory (if they have one).
HomeDir string
/****************** Begin added code ******************/
// Login shell
Shell string
/****************** End added code ******************/
}
func lookupUser(username string) (*User, error) {
var pwd C.struct_passwd
var result *C.struct_passwd
nameC := make([]byte, len(username)+1)
copy(nameC, username)
buf := alloc(userBuffer)
defer buf.free()
err := retryWithBuffer(buf, func() syscall.Errno {
// mygetpwnam_r is a wrapper around getpwnam_r to avoid
// passing a size_t to getpwnam_r, because for unknown
// reasons passing a size_t to getpwnam_r doesn't work on
// Solaris.
return syscall.Errno(C.mygetpwnam_r((*C.char)(unsafe.Pointer(&nameC[0])),
&pwd,
(*C.char)(buf.ptr),
C.size_t(buf.size),
&result))
})
if err != nil {
return nil, fmt.Errorf("user: lookup username %s: %v", username, err)
}
if result == nil {
return nil, UnknownUserError(username)
}
return buildUser(&pwd), err
}
func buildUser(pwd *C.struct_passwd) *User {
u := &User{
Uid: strconv.FormatUint(uint64(pwd.pw_uid), 10),
Gid: strconv.FormatUint(uint64(pwd.pw_gid), 10),
Username: C.GoString(pwd.pw_name),
Name: C.GoString(pwd.pw_gecos),
HomeDir: C.GoString(pwd.pw_dir),
/****************** Begin added code ******************/
Shell: C.GoString(pwd.pw_shell),
/****************** End added code ******************/
}
// The pw_gecos field isn't quite standardized. Some docs
// say: "It is expected to be a comma separated list of
// personal data where the first item is the full name of the
// user."
if i := strings.Index(u.Name, ","); i >= 0 {
u.Name = u.Name[:i]
}
return u
}
type bufferKind C.int
const (
userBuffer = bufferKind(C._SC_GETPW_R_SIZE_MAX)
)
func (k bufferKind) initialSize() C.size_t {
sz := C.sysconf(C.int(k))
if sz == -1 {
// DragonFly and FreeBSD do not have _SC_GETPW_R_SIZE_MAX.
// Additionally, not all Linux systems have it, either. For
// example, the musl libc returns -1.
return 1024
}
if !isSizeReasonable(int64(sz)) {
// Truncate. If this truly isn't enough, retryWithBuffer will error on the first run.
return maxBufferSize
}
return C.size_t(sz)
}
type memBuffer struct {
ptr unsafe.Pointer
size C.size_t
}
func alloc(kind bufferKind) *memBuffer {
sz := kind.initialSize()
return &memBuffer{
ptr: C.malloc(sz),
size: sz,
}
}
func (mb *memBuffer) resize(newSize C.size_t) {
mb.ptr = C.realloc(mb.ptr, newSize)
mb.size = newSize
}
func (mb *memBuffer) free() {
C.free(mb.ptr)
}
// retryWithBuffer repeatedly calls f(), increasing the size of the
// buffer each time, until f succeeds, fails with a non-ERANGE error,
// or the buffer exceeds a reasonable limit.
func retryWithBuffer(buf *memBuffer, f func() syscall.Errno) error {
for {
errno := f()
if errno == 0 {
return nil
} else if errno != syscall.ERANGE {
return errno
}
newSize := buf.size * 2
if !isSizeReasonable(int64(newSize)) {
return fmt.Errorf("internal buffer exceeds %d bytes", maxBufferSize)
}
buf.resize(newSize)
}
}
const maxBufferSize = 1 << 20
func isSizeReasonable(sz int64) bool {
return sz > 0 && sz <= maxBufferSize
}
+110
View File
@@ -0,0 +1,110 @@
//+build !windows
package sshserver
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/gliderlabs/ssh"
"github.com/pkg/errors"
)
const (
rsaFilename = "ssh_host_rsa_key"
ecdsaFilename = "ssh_host_ecdsa_key"
)
func (s *SSHServer) configureHostKeys() error {
if _, err := os.Stat(systemConfigPath); os.IsNotExist(err) {
if err := os.MkdirAll(systemConfigPath, 0755); err != nil {
return errors.Wrap(err, fmt.Sprintf("Error creating %s directory", systemConfigPath))
}
}
if err := s.configureHostKey(s.ensureECDSAKeyExists); err != nil {
return err
}
if err := s.configureHostKey(s.ensureRSAKeyExists); err != nil {
return err
}
return nil
}
func (s *SSHServer) configureHostKey(keyFunc func() (string, error)) error {
path, err := keyFunc()
if err != nil {
return err
}
if err := s.SetOption(ssh.HostKeyFile(path)); err != nil {
return errors.Wrap(err, "Could not set SSH host key")
}
return nil
}
func (s *SSHServer) ensureRSAKeyExists() (string, error) {
keyPath := filepath.Join(systemConfigPath, rsaFilename)
if _, err := os.Stat(keyPath); os.IsNotExist(err) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return "", errors.Wrap(err, "Error generating RSA host key")
}
privateKey := &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(key),
}
if err = writePrivateKey(keyPath, privateKey); err != nil {
return "", err
}
s.logger.Debug("Created new RSA SSH host key: ", keyPath)
}
return keyPath, nil
}
func (s *SSHServer) ensureECDSAKeyExists() (string, error) {
keyPath := filepath.Join(systemConfigPath, ecdsaFilename)
if _, err := os.Stat(keyPath); os.IsNotExist(err) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return "", errors.Wrap(err, "Error generating ECDSA host key")
}
keyBytes, err := x509.MarshalECPrivateKey(key)
if err != nil {
return "", errors.Wrap(err, "Error marshalling ECDSA key")
}
privateKey := &pem.Block{
Type: "EC PRIVATE KEY",
Bytes: keyBytes,
}
if err = writePrivateKey(keyPath, privateKey); err != nil {
return "", err
}
s.logger.Debug("Created new ECDSA SSH host key: ", keyPath)
}
return keyPath, nil
}
func writePrivateKey(keyPath string, privateKey *pem.Block) error {
if err := ioutil.WriteFile(keyPath, pem.EncodeToMemory(privateKey), 0600); err != nil {
return errors.Wrap(err, fmt.Sprintf("Error writing host key to %s", keyPath))
}
return nil
}
+227
View File
@@ -0,0 +1,227 @@
//+build !windows
package sshserver
import (
"errors"
"fmt"
"io"
"os"
"os/exec"
"os/user"
"strconv"
"syscall"
"time"
"unsafe"
"github.com/cloudflare/cloudflared/sshlog"
"github.com/creack/pty"
"github.com/gliderlabs/ssh"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
type SSHServer struct {
ssh.Server
logger *logrus.Logger
shutdownC chan struct{}
caCert ssh.PublicKey
getUserFunc func(string) (*User, error)
logManager sshlog.Manager
}
func New(logManager sshlog.Manager, logger *logrus.Logger, address string, shutdownC chan struct{}, idleTimeout, maxTimeout time.Duration) (*SSHServer, error) {
currentUser, err := user.Current()
if err != nil {
return nil, err
}
if currentUser.Uid != "0" {
return nil, errors.New("cloudflared SSH server needs to run as root")
}
sshServer := SSHServer{
Server: ssh.Server{Addr: address, MaxTimeout: maxTimeout, IdleTimeout: idleTimeout},
logger: logger,
shutdownC: shutdownC,
getUserFunc: lookupUser,
logManager: logManager,
}
if err := sshServer.configureHostKeys(); err != nil {
return nil, err
}
sshServer.configureAuthentication()
return &sshServer, nil
}
func (s *SSHServer) Start() error {
s.logger.Infof("Starting SSH server at %s", s.Addr)
go func() {
<-s.shutdownC
if err := s.Close(); err != nil {
s.logger.WithError(err).Error("Cannot close SSH server")
}
}()
s.Handle(s.connectionHandler)
return s.ListenAndServe()
}
func (s *SSHServer) connectionHandler(session ssh.Session) {
sessionID, err := uuid.NewRandom()
if err != nil {
if _, err := io.WriteString(session, "Failed to generate session ID\n"); err != nil {
s.logger.WithError(err).Error("Failed to generate session ID: Failed to write to SSH session")
}
s.CloseSession(session)
}
// Get uid and gid of user attempting to login
sshUser, ok := session.Context().Value("sshUser").(*User)
if !ok || sshUser == nil {
s.logger.Error("Error retrieving credentials from session")
s.CloseSession(session)
return
}
uidInt, err := stringToUint32(sshUser.Uid)
if err != nil {
s.logger.WithError(err).Error("Invalid user")
s.CloseSession(session)
return
}
gidInt, err := stringToUint32(sshUser.Gid)
if err != nil {
s.logger.WithError(err).Error("Invalid user group")
s.CloseSession(session)
return
}
// Spawn shell under user
var cmd *exec.Cmd
if session.RawCommand() != "" {
cmd = exec.Command(sshUser.Shell, "-c", session.RawCommand())
} else {
cmd = exec.Command(sshUser.Shell)
}
// Supplementary groups are not explicitly specified. They seem to be inherited by default.
cmd.SysProcAttr = &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: uidInt, Gid: gidInt}, Setsid: true}
cmd.Env = append(cmd.Env, session.Environ()...)
cmd.Env = append(cmd.Env, fmt.Sprintf("USER=%s", sshUser.Username))
cmd.Env = append(cmd.Env, fmt.Sprintf("HOME=%s", sshUser.HomeDir))
cmd.Dir = sshUser.HomeDir
ptyReq, winCh, isPty := session.Pty()
var shellInput io.WriteCloser
var shellOutput io.ReadCloser
if isPty {
cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term))
shellInput, shellOutput, err = s.startPtySession(cmd, winCh)
if err != nil {
s.logger.WithError(err).Error("Failed to start pty session")
close(s.shutdownC)
return
}
} else {
shellInput, shellOutput, err = s.startNonPtySession(cmd)
if err != nil {
s.logger.WithError(err).Error("Failed to start non-pty session")
close(s.shutdownC)
return
}
}
// Write incoming commands to shell
go func() {
if _, err := io.Copy(shellInput, session); err != nil {
s.logger.WithError(err).Error("Failed to write incoming command to pty")
}
}()
pr, pw := io.Pipe()
defer pr.Close()
defer pw.Close()
logger, err := s.logManager.NewLogger(fmt.Sprintf("%s-session.log", sessionID), s.logger)
if err != nil {
if _, err := io.WriteString(session, "Failed to create log\n"); err != nil {
s.logger.WithError(err).Error("Failed to create log: Failed to write to SSH session")
}
s.CloseSession(session)
}
defer logger.Close()
go func() {
io.Copy(logger, pr)
}()
// Write outgoing command output to both the command recorder, and remote user
mw := io.MultiWriter(pw, session)
if _, err := io.Copy(mw, shellOutput); err != nil {
s.logger.WithError(err).Error("Failed to write command output to user")
}
// Wait for all resources associated with cmd to be released
// Returns error if shell exited with a non-zero status or received a signal
if err := cmd.Wait(); err != nil {
s.logger.WithError(err).Debug("Shell did not close correctly")
}
}
func (s *SSHServer) CloseSession(session ssh.Session) {
if err := session.Exit(1); err != nil {
s.logger.WithError(err).Error("Failed to close SSH session")
}
}
func (s *SSHServer) startNonPtySession(cmd *exec.Cmd) (io.WriteCloser, io.ReadCloser, error) {
in, err := cmd.StdinPipe()
if err != nil {
return nil, nil, err
}
out, err := cmd.StdoutPipe()
if err != nil {
return nil, nil, err
}
cmd.Stderr = cmd.Stdout
if err = cmd.Start(); err != nil {
return nil, nil, err
}
return in, out, nil
}
func (s *SSHServer) startPtySession(cmd *exec.Cmd, winCh <-chan ssh.Window) (io.WriteCloser, io.ReadCloser, error) {
tty, err := pty.Start(cmd)
if err != nil {
return nil, nil, err
}
// Handle terminal window size changes
go func() {
for win := range winCh {
if errNo := setWinsize(tty, win.Width, win.Height); errNo != 0 {
s.logger.WithError(err).Error("Failed to set pty window size")
close(s.shutdownC)
return
}
}
}()
return tty, tty, nil
}
// Sets PTY window size for terminal
func setWinsize(f *os.File, w, h int) syscall.Errno {
_, _, errNo := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ),
uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(h), uint16(w), 0, 0})))
return errNo
}
func stringToUint32(str string) (uint32, error) {
uid, err := strconv.ParseUint(str, 10, 32)
return uint32(uid), err
}
+22
View File
@@ -0,0 +1,22 @@
//+build windows
package sshserver
import (
"errors"
"time"
"github.com/cloudflare/cloudflared/sshlog"
"github.com/sirupsen/logrus"
)
type SSHServer struct{}
func New(_ sshlog.Manager, _ *logrus.Logger, _ string, _ chan struct{}, _, _ time.Duration) (*SSHServer, error) {
return nil, errors.New("cloudflared ssh server is not supported on windows")
}
func (s *SSHServer) Start() error {
return errors.New("cloudflared ssh server is not supported on windows")
}
+27
View File
@@ -0,0 +1,27 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAQEA0c6EklYvC9B041qEGWDNuot6G4tTVm9LCQC0vA+v2n25ru9CINV6
8IljmXBORXBwfG6PdLhg0SEabZUbsNX5WrIVbGovcghKS6GRsqI5+Quhm+o8eG042JE/hB
oYdZ19TcMEyPOGzHsx0U/BSN9ZJWVCxqN51iI6qyhz9f6jlX2LQBFEvXlhxgF3owBEf8UC
Zt/UvbZdmeeyKNQElPmiVLIJEAPCueECp7a2mjCiP3zqjDvSeeGk4CelB/1qZZ4V2n7fvb
HZjAB5JJs4KXs5o8KgvQnqgQMxiLFZ4PATt4+mxEzh4JymppbqJOo2rYwOA3TAIEWWtYRV
/ZKJ0AyhhQAAA8gciO8XHIjvFwAAAAdzc2gtcnNhAAABAQDRzoSSVi8L0HTjWoQZYM26i3
obi1NWb0sJALS8D6/afbmu70Ig1XrwiWOZcE5FcHB8bo90uGDRIRptlRuw1flashVsai9y
CEpLoZGyojn5C6Gb6jx4bTjYkT+EGhh1nX1NwwTI84bMezHRT8FI31klZULGo3nWIjqrKH
P1/qOVfYtAEUS9eWHGAXejAER/xQJm39S9tl2Z57Io1ASU+aJUsgkQA8K54QKntraaMKI/
fOqMO9J54aTgJ6UH/WplnhXaft+9sdmMAHkkmzgpezmjwqC9CeqBAzGIsVng8BO3j6bETO
HgnKamluok6jatjA4DdMAgRZa1hFX9konQDKGFAAAAAwEAAQAAAQEApVzGdKhk8ETevAst
rurze6JPHcKUbr3NQE1EJi2fBvCtF0oQrtxTx54h2GAB8Q0MO6bQfsiL1ojm0ZQCfUBJBs
jxxb9zoccS98Vilo7ybm5SdBcMjkZX1am1jCMdQCZfCpk4/kGi7yvyOe1IhG01UBodpX5X
mwTjhN+fdjW7LSiW6cKPClN49CZKgmtvI27FCt+/TtMzdCXOiJxJ4yZCzCRhSgssV0gWI1
0VJr/MHirKUvv/qCLAuOBxIr9UgdduRZUpNX+KS2rfhFEbjnUqc/57aAakpQmuPB5I+s9G
DnrF0HSHpq7u1XC1SvYlnFBN/0A7Hw/MX2SaBFH7mc9AAQAAAIAFuTHr6O8tCvWEawfxC0
qiAPQ+Yy1vthq5uewmuQujMutUnc9JAUl32PdU1DbS7APC1Dg9XL7SyAB6A+ZpRJRAKgCY
SneAKE6hOytH+yM206aekrz6VuZiSpBqpfEqDibVAaZIO8sv/9dtZd6kWemxNErPQoKJey
Z7/cuWUWQovAAAAIEA6ugIlVj1irPmElyCCt5YfPv2x8Dl54ELoP/WsffsrPHNQog64hFd
ahD7Wq63TA566bN85fkx8OVU5TbbEQmkHgOEV6nDRY2YsBSqIOblA/KehtfdUIqZB0iNBh
Gn6TV/z6HwnSR3gKv4b66Gveek6LfRAG3mbsLCgyRAbYgn6YUAAACBAOSlf+n1eh6yjtvF
Zecq3Zslj7O8cUs17PQx4vQ7sXNCFrIZdevWPIn9sVrt7/hsTrXunDz6eXCeclB35KZe3H
WPVjRoD+xnr5+sXx2qXOnKCR0LdFybso6IR5bXAI6DNSNfP7D9LPEQ+R73Jk0jPuLYzocS
iM89KZiuGpzr01gBAAAAEW1pa2VAQzAyWTUwVEdKR0g4AQ==
-----END OPENSSH PRIVATE KEY-----
+1
View File
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDRzoSSVi8L0HTjWoQZYM26i3obi1NWb0sJALS8D6/afbmu70Ig1XrwiWOZcE5FcHB8bo90uGDRIRptlRuw1flashVsai9yCEpLoZGyojn5C6Gb6jx4bTjYkT+EGhh1nX1NwwTI84bMezHRT8FI31klZULGo3nWIjqrKHP1/qOVfYtAEUS9eWHGAXejAER/xQJm39S9tl2Z57Io1ASU+aJUsgkQA8K54QKntraaMKI/fOqMO9J54aTgJ6UH/WplnhXaft+9sdmMAHkkmzgpezmjwqC9CeqBAzGIsVng8BO3j6bETOHgnKamluok6jatjA4DdMAgRZa1hFX9konQDKGF mike@C02Y50TGJGH8
+49
View File
@@ -0,0 +1,49 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAgEA60Kneo87qPsh+zErWFl7vx93c7fyTxbZ9lUNqafgXy/BLOCc/nQS
McosVSLsQrbHlhYzfmZEhTiubmuYUrHchmsn1ml1HIqP8T5aDgtNbLqYnS4H5oO4Sj1+XH
lQtU7n7zHXgca9SnMWt1Fhkx1mvkeiOKs0eq7hV2TuIZxfmbYfIVvJGwrL0uWzbSEE1gvx
gTXZHxEChIQyrNviljgi4u2MD/cIi6KMeYUnaTL1FxO9G4GIFiy7ueHRwOZPIFHgYm+Vrt
X7XafSF0///zCrC63zzWt/6A06hFepOz2VXvm7SdckaR7qMXAb7kipsc0+dKk9ggU7Fqpx
ZY5cVeZo9RlRVhRXGDy7mABA/FMwvv+qYCgJ3nlZbdKbaiPLQu8ScTlJ9sMI06/ZiEY04b
meZ0ASM52gaDGjrFbbnuHNf5XV/oreEUhtCrryFnoIxmKgHznGjZ55q77FtTHnrAKFmKFP
11s3MLIX9o4RgtriOtl4KenkIfUumgtrwY/UGjOaOQUOrVH1am54wkUiVEF0Qd3AD8KCl/
l/xT5+t6cOspZ9GIhwa2NBmRjN/wVGp+Yrb08Re3kxPCX9bs5iLe+kHN0vuFr7RDo+eUoi
SPhWl6FUqx2W9NZqekmEgKn3oKrfbGaMH1VLkaKWlzQ4xJzP0iadQbIXGryLEYASydemZt
sAAAdQ/ovjxf6L48UAAAAHc3NoLXJzYQAAAgEA60Kneo87qPsh+zErWFl7vx93c7fyTxbZ
9lUNqafgXy/BLOCc/nQSMcosVSLsQrbHlhYzfmZEhTiubmuYUrHchmsn1ml1HIqP8T5aDg
tNbLqYnS4H5oO4Sj1+XHlQtU7n7zHXgca9SnMWt1Fhkx1mvkeiOKs0eq7hV2TuIZxfmbYf
IVvJGwrL0uWzbSEE1gvxgTXZHxEChIQyrNviljgi4u2MD/cIi6KMeYUnaTL1FxO9G4GIFi
y7ueHRwOZPIFHgYm+VrtX7XafSF0///zCrC63zzWt/6A06hFepOz2VXvm7SdckaR7qMXAb
7kipsc0+dKk9ggU7FqpxZY5cVeZo9RlRVhRXGDy7mABA/FMwvv+qYCgJ3nlZbdKbaiPLQu
8ScTlJ9sMI06/ZiEY04bmeZ0ASM52gaDGjrFbbnuHNf5XV/oreEUhtCrryFnoIxmKgHznG
jZ55q77FtTHnrAKFmKFP11s3MLIX9o4RgtriOtl4KenkIfUumgtrwY/UGjOaOQUOrVH1am
54wkUiVEF0Qd3AD8KCl/l/xT5+t6cOspZ9GIhwa2NBmRjN/wVGp+Yrb08Re3kxPCX9bs5i
Le+kHN0vuFr7RDo+eUoiSPhWl6FUqx2W9NZqekmEgKn3oKrfbGaMH1VLkaKWlzQ4xJzP0i
adQbIXGryLEYASydemZtsAAAADAQABAAACABUYzBYEhDAaHSj+dsmcdKll8/tPko4fGXqq
k+gT4t4GVUdl+Q4kcIFAhQs5b4BoDava39FE8H4V4CaMxYMc6g6vy0nB+TuO/Wt/0OmTf+
TxMsBdoV29kCgwLYWzZ1Zq9geQK6g6nzzu5ymXRa3ApDcKC3UTfUhHKHQC3AvtjvEk0NPX
/EfNhwuph5aQsHNVbNnOb2MGznf9tuGjckVQUWiSLs47s+t5rykylJ8tb6cbIQk3a3G5nz
gDFSE8Rfo6/Wk2YnDkRX9XjlKC3Q0QWzZX6hYQvs6baRT3G3jxg9SZhn8PqPc4S34VdJvA
rl8AbcpeZuKi/3J/5F1cD9GwMNcl4gM87piF20/r9mMvC4zBAEgyF8WBi4OjSu0+ccsEsb
GSpxKK04OPTB7p8mLJ8hQUiREg5OuPEEcAoDSuHgdliE7nDHzuImbpTcAZcWhkJaUdBWI6
qcnGPARzxAOmuzkY8Gq0MtcWge5QxnLWJyrfy43M984Cvxql/maLUij4eTbMDDwV7Qx30V
P2tJp5+hOnitRwB6cQIg5N7/cTQdJ6eiFYuw0v3IfHjYmaolY8F3u38Zv2PPk50CorPRDG
esx0a9Elm2UKPb145MtHGZtLH2mayRnDjnxr25iLwgokI06tCLCNvbkYLA7wVpJn81eKmZ
tQBtbfqBSiDiLjCrehAAABAQDh8vmgPR95Kx1DeBxzMSja7TStP5V58JcUvLP4dAN6zqtt
rMuKDfJRSIVYGkU8vXlME5E6phWP7R5lYY7+kLDbeZrYQg1AxkK8y4fUYkCLBuEcCjzWDK
oqZQNskk4urbCdBIP6AR7d/LMCHBb9rk2iOuUeos6JHRKbPGP1lvH3hLkbH9CA0F41sz86
JFg6u/XaRQ2CyhS7y7SQ8dmaANGz9LGdIRqIoZ8Hfht8t1VRbM9fzSb3xoxUItbHpk9R9g
GZsHSryi7AtRmHt0uBrWIv6RbIY0epCbjdCLvHflbkPgwBM7UndgkOSIwQ4SQF8Fs+e9/r
hV05h0Y81vd1RZvOAAABAQD5EgW3SpmYzeMmiP7MKkfIlbZtwVmRu4anTzWxlk5pJ9GXnC
QoInULCipWAOeJbuLIgRWLU4VzhOUbYLNKQPXECARfgoto2VXoXZZ2q2O4aXaCpeyU6nE8
VKbp4nU1jEg5hWB3PRwZ8Pzs4A93/9mrpVzLmCT+LW9Rlnp6tTpqcUKGugg8vr64SSgqnV
ZFyQgHgw+ZGOG9w714urS3U97WNTeHXAs0p2YBOu5XW3JQ3jkRo7YyZF3+TtBxbgfHRZfH
O2mFcMBD3Sn4t+LAbgnLye3S2/WZf/gQwdVB7BgrVqguzQ2hGoOxNiwadkIDsxb6r/u3n6
2lScpHFDS0WnpRAAABAQDxzkV52VX6wAWkQe/2KFH9wTG0XFANmZUnnTPR8wd+b9E7HIr0
Mdd8iAHOhLRvTy8mih53GGBptXK7GdABMZtkqDErbXhuC8xbi9uRLEHiRe/oBfWr8vYIZY
awiw3/EqxaTv0HBMicdr2S31Bs2/mjrVuJH0wAaI9ueQnZizzjgWuzeNZMWq1qk0akUUdm
PDVd58yBkt8lKlkOG0LJAn6JEG9oH9XiTFShHzu1dQmoC2bKVHdxL8WCcYFVtmyoMRcLZq
u6d4nyKha02cYZB5hM3VcizJI5HY/A+H3fBkRR0hXgkU5R89w+8x9VSJkNVx+JGC7ziK4a
kUjfOmR5WBdrAAAAE3Rlc3RAY2xvdWRmbGFyZS5jb20BAgMEBQYH
-----END OPENSSH PRIVATE KEY-----
+1
View File
@@ -0,0 +1 @@
ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgOsuFqKdzp/nC3wQfKVJBdHa8axtGryKplPkDjdSXT4kAAAADAQABAAACAQDrQqd6jzuo+yH7MStYWXu/H3dzt/JPFtn2VQ2pp+BfL8Es4Jz+dBIxyixVIuxCtseWFjN+ZkSFOK5ua5hSsdyGayfWaXUcio/xPloOC01supidLgfmg7hKPX5ceVC1TufvMdeBxr1Kcxa3UWGTHWa+R6I4qzR6ruFXZO4hnF+Zth8hW8kbCsvS5bNtIQTWC/GBNdkfEQKEhDKs2+KWOCLi7YwP9wiLoox5hSdpMvUXE70bgYgWLLu54dHA5k8gUeBib5Wu1ftdp9IXT///MKsLrfPNa3/oDTqEV6k7PZVe+btJ1yRpHuoxcBvuSKmxzT50qT2CBTsWqnFljlxV5mj1GVFWFFcYPLuYAED8UzC+/6pgKAneeVlt0ptqI8tC7xJxOUn2wwjTr9mIRjThuZ5nQBIznaBoMaOsVtue4c1/ldX+it4RSG0KuvIWegjGYqAfOcaNnnmrvsW1MeesAoWYoU/XWzcwshf2jhGC2uI62Xgp6eQh9S6aC2vBj9QaM5o5BQ6tUfVqbnjCRSJUQXRB3cAPwoKX+X/FPn63pw6yln0YiHBrY0GZGM3/BUan5itvTxF7eTE8Jf1uzmIt76Qc3S+4WvtEOj55SiJI+FaXoVSrHZb01mp6SYSAqfegqt9sZowfVUuRopaXNDjEnM/SJp1BshcavIsRgBLJ16Zm2wAAAAAAAAAAAAAAAQAAAA10ZXN0VXNlckB0ZXN0AAAADAAAAAh0ZXN0VXNlcgAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAABFwAAAAdzc2gtcnNhAAAAAwEAAQAAAQEA0c6EklYvC9B041qEGWDNuot6G4tTVm9LCQC0vA+v2n25ru9CINV68IljmXBORXBwfG6PdLhg0SEabZUbsNX5WrIVbGovcghKS6GRsqI5+Quhm+o8eG042JE/hBoYdZ19TcMEyPOGzHsx0U/BSN9ZJWVCxqN51iI6qyhz9f6jlX2LQBFEvXlhxgF3owBEf8UCZt/UvbZdmeeyKNQElPmiVLIJEAPCueECp7a2mjCiP3zqjDvSeeGk4CelB/1qZZ4V2n7fvbHZjAB5JJs4KXs5o8KgvQnqgQMxiLFZ4PATt4+mxEzh4JymppbqJOo2rYwOA3TAIEWWtYRV/ZKJ0AyhhQAAAQ8AAAAHc3NoLXJzYQAAAQC2lL+6JYTGOdz1zNnck6onrFcVpO2onCVAKP8HdLoCeH0/upIugaCocPKuzoURYEfiHQotviNeprE/2CyAroJ5VBdqWftEeHn3FFvBCQ1gwRQ7oci4C5n72t0vjWWE6WBylS0RqpJjr6EQ8a1vuwIqAQrEJPp2yNLjRH2WD7eicBh5f43VKOMr73DtyTh4xoF0C2sNBROudt58npTaYqRHQgoI25V/aCmuYBgM3wdAGcoEZGoSerMfhID7GcWkvemq2hF8mQsspG3zgnyQXk+ahagmefzxutDnr3KdrZ637La0/XwABvBZ9L4l5RiEilVI1Shl96F2qbBW2YZ64pUQ test@cloudflare.com
+1
View File
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDrQqd6jzuo+yH7MStYWXu/H3dzt/JPFtn2VQ2pp+BfL8Es4Jz+dBIxyixVIuxCtseWFjN+ZkSFOK5ua5hSsdyGayfWaXUcio/xPloOC01supidLgfmg7hKPX5ceVC1TufvMdeBxr1Kcxa3UWGTHWa+R6I4qzR6ruFXZO4hnF+Zth8hW8kbCsvS5bNtIQTWC/GBNdkfEQKEhDKs2+KWOCLi7YwP9wiLoox5hSdpMvUXE70bgYgWLLu54dHA5k8gUeBib5Wu1ftdp9IXT///MKsLrfPNa3/oDTqEV6k7PZVe+btJ1yRpHuoxcBvuSKmxzT50qT2CBTsWqnFljlxV5mj1GVFWFFcYPLuYAED8UzC+/6pgKAneeVlt0ptqI8tC7xJxOUn2wwjTr9mIRjThuZ5nQBIznaBoMaOsVtue4c1/ldX+it4RSG0KuvIWegjGYqAfOcaNnnmrvsW1MeesAoWYoU/XWzcwshf2jhGC2uI62Xgp6eQh9S6aC2vBj9QaM5o5BQ6tUfVqbnjCRSJUQXRB3cAPwoKX+X/FPn63pw6yln0YiHBrY0GZGM3/BUan5itvTxF7eTE8Jf1uzmIt76Qc3S+4WvtEOj55SiJI+FaXoVSrHZb01mp6SYSAqfegqt9sZowfVUuRopaXNDjEnM/SJp1BshcavIsRgBLJ16Zm2w== test@cloudflare.com
+27
View File
@@ -0,0 +1,27 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAQEAzBO7TXxbpk7sGQm/Wa29N/NFe5uuoEQGC5hxfihmcvVgeKeNKiSS
snxzCE1Y6SmNMoE4aQs92wtcn48GmxRwZSXbCqLq2CJrHfe9B2k3aPkJZpQkFMshcJGo7p
G0Vlo7dWAbYf99/YKddf290uLK7vxw9ty0pM1hXSXHNShv1b+bTQm/COMZ5jNsncjc1yBH
KGkFVHee9Dh4Z0xLlHipIyyNXXzI0RFYuHSNJz9GD310XQLIIroptr7+/7g6+sPPGsNlI+
95OScba1/PQ2b/qy+KyIwNIMSd9ziJy5xnO7Vo3LrqQrza1Pkn2i29PljUcbc/F0hhXNIq
ITdNWwVqsQAAA8iKllTIipZUyAAAAAdzc2gtcnNhAAABAQDME7tNfFumTuwZCb9Zrb0380
V7m66gRAYLmHF+KGZy9WB4p40qJJKyfHMITVjpKY0ygThpCz3bC1yfjwabFHBlJdsKourY
Imsd970HaTdo+QlmlCQUyyFwkajukbRWWjt1YBth/339gp11/b3S4sru/HD23LSkzWFdJc
c1KG/Vv5tNCb8I4xnmM2ydyNzXIEcoaQVUd570OHhnTEuUeKkjLI1dfMjREVi4dI0nP0YP
fXRdAsgiuim2vv7/uDr6w88aw2Uj73k5JxtrX89DZv+rL4rIjA0gxJ33OInLnGc7tWjcuu
pCvNrU+SfaLb0+WNRxtz8XSGFc0iohN01bBWqxAAAAAwEAAQAAAQAKEtNFEOVpQS4QUlXa
tGPJtj1wy4+EI7d0rRK1GoNsG0amzgZ+1Q1UuCXpe//uinmIy64gKUjlXhs1WRcHYqvlok
e8r6wN/Szybr8q9Xuht+FJ6fgZ+qjs6JPBKvoO5SdYNOVFIhpzABaLs3nCRiWkRFvDI8Pa
+rRap7m8mwFiOJtmdiIZYFxzw6xXwTsGCrWPKgTv3FKGZzXnCB9i7jC2vwT1MDYbcnzEH4
Ba4dxI8bp6WWEX0biRIXj3jCtLb5gisNTSxdZs254Syh75HEXunSh2YO+yVSWQtZj19ewW
6Rb1Z3x5rVfXcgSkg7gZd9EpbckIIg6+MFSH3wdGW6atAAAAgQDFXiMuNd4ZYwdyhjlM5n
nFqQDXGgnwyNdiIqAapoqTdF5aZwNnbTU0fCFaDMLCQAHgntcgCEsW9A4HzDzYhOABKElv
j973vXWF165wFiZwuKSfroq/6JH6CiIcjiqpszbnqSOzy1hq913RWILS6e9yMjxRv8PUjm
E+IkcnfcFUwAAAAIEA+jwI3ICe8PGEIezV2tvQFeQy2Z2wGslu1yvqfTYEztSmtygns3wn
ZBb+cBXCnpqUCtznG7hZhq7I4m1I47BYznULwwFiBTVtBASG5wNP7zeVKTVZ4SKprze+Fe
I/nUZDJ5Q26um7eDbhvZ/n95GY+fucMVHoSBfX1wE16XBfp88AAACBANDHcgC4qP2oyOw/
+p9HineMQd/ppG3fePe07jyZXLHLf0rByFveFgRAQ1m77O7FtP3fFKy3Y9nNy18LGq35ZK
Blsz2B23bO8NuffgAhchDG7KzKFXCo+AraIj5znp/znK5zIkaiiSOQaYywJ36EooYVpRtj
ep5ap6bBFDZ2e+V/AAAAEW1pa2VAQzAyWTUwVEdKR0g4AQ==
-----END OPENSSH PRIVATE KEY-----
+1
View File
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDME7tNfFumTuwZCb9Zrb0380V7m66gRAYLmHF+KGZy9WB4p40qJJKyfHMITVjpKY0ygThpCz3bC1yfjwabFHBlJdsKourYImsd970HaTdo+QlmlCQUyyFwkajukbRWWjt1YBth/339gp11/b3S4sru/HD23LSkzWFdJcc1KG/Vv5tNCb8I4xnmM2ydyNzXIEcoaQVUd570OHhnTEuUeKkjLI1dfMjREVi4dI0nP0YPfXRdAsgiuim2vv7/uDr6w88aw2Uj73k5JxtrX89DZv+rL4rIjA0gxJ33OInLnGc7tWjcuupCvNrU+SfaLb0+WNRxtz8XSGFc0iohN01bBWqx mike@C02Y50TGJGH8
+69
View File
@@ -0,0 +1,69 @@
package streamhandler
import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/pkg/errors"
)
const (
lbProbeUserAgentPrefix = "Mozilla/5.0 (compatible; Cloudflare-Traffic-Manager/1.0; +https://www.cloudflare.com/traffic-manager/;"
)
func FindCfRayHeader(h1 *http.Request) string {
return h1.Header.Get("Cf-Ray")
}
func IsLBProbeRequest(req *http.Request) bool {
return strings.HasPrefix(req.UserAgent(), lbProbeUserAgentPrefix)
}
func createRequest(stream *h2mux.MuxedStream, url *url.URL) (*http.Request, error) {
req, err := http.NewRequest(http.MethodGet, url.String(), h2mux.MuxedStreamReader{MuxedStream: stream})
if err != nil {
return nil, errors.Wrap(err, "unexpected error from http.NewRequest")
}
err = H2RequestHeadersToH1Request(stream.Headers, req)
if err != nil {
return nil, errors.Wrap(err, "invalid request received")
}
return req, nil
}
func H2RequestHeadersToH1Request(h2 []h2mux.Header, h1 *http.Request) error {
for _, header := range h2 {
switch header.Name {
case ":method":
h1.Method = header.Value
case ":scheme":
case ":authority":
// Otherwise the host header will be based on the origin URL
h1.Host = header.Value
case ":path":
u, err := url.Parse(header.Value)
if err != nil {
return fmt.Errorf("unparseable path")
}
resolved := h1.URL.ResolveReference(u)
// prevent escaping base URL
if !strings.HasPrefix(resolved.String(), h1.URL.String()) {
return fmt.Errorf("invalid path")
}
h1.URL = resolved
case "content-length":
contentLength, err := strconv.ParseInt(header.Value, 10, 64)
if err != nil {
return fmt.Errorf("unparseable content length")
}
h1.ContentLength = contentLength
default:
h1.Header.Add(http.CanonicalHeaderKey(header.Name), header.Value)
}
}
return nil
}
+190
View File
@@ -0,0 +1,190 @@
package streamhandler
import (
"context"
"fmt"
"net/http"
"strconv"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/tunnelhostnamemapper"
"github.com/cloudflare/cloudflared/tunnelrpc"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"zombiezen.com/go/capnproto2/rpc"
)
const (
statusPseudoHeader = ":status"
)
type httpErrorStatus struct {
status string
text []byte
}
var (
statusBadRequest = newHTTPErrorStatus(http.StatusBadRequest)
statusNotFound = newHTTPErrorStatus(http.StatusNotFound)
statusBadGateway = newHTTPErrorStatus(http.StatusBadGateway)
)
func newHTTPErrorStatus(status int) *httpErrorStatus {
return &httpErrorStatus{
status: strconv.Itoa(status),
text: []byte(http.StatusText(status)),
}
}
// StreamHandler handles new stream opened by the edge. The streams can be used to proxy requests or make RPC.
type StreamHandler struct {
// newConfigChan is a send-only channel to notify Supervisor of a new ClientConfig
newConfigChan chan<- *pogs.ClientConfig
// useConfigResultChan is a receive-only channel for Supervisor to communicate the result of applying a new ClientConfig
useConfigResultChan <-chan *pogs.UseConfigurationResult
// originMapper maps tunnel hostname to origin service
tunnelHostnameMapper *tunnelhostnamemapper.TunnelHostnameMapper
logger *logrus.Entry
}
// NewStreamHandler creates a new StreamHandler
func NewStreamHandler(newConfigChan chan<- *pogs.ClientConfig,
useConfigResultChan <-chan *pogs.UseConfigurationResult,
logger *logrus.Logger,
) *StreamHandler {
return &StreamHandler{
newConfigChan: newConfigChan,
useConfigResultChan: useConfigResultChan,
tunnelHostnameMapper: tunnelhostnamemapper.NewTunnelHostnameMapper(),
logger: logger.WithField("subsystem", "streamHandler"),
}
}
// UseConfiguration implements ClientService
func (s *StreamHandler) UseConfiguration(ctx context.Context, config *pogs.ClientConfig) (*pogs.UseConfigurationResult, error) {
select {
case <-ctx.Done():
err := fmt.Errorf("Timeout while sending new config to Supervisor")
s.logger.Error(err)
return nil, err
case s.newConfigChan <- config:
}
select {
case <-ctx.Done():
err := fmt.Errorf("Timeout applying new configuration")
s.logger.Error(err)
return nil, err
case result := <-s.useConfigResultChan:
return result, nil
}
}
// UpdateConfig replaces current originmapper mapping with mappings from newConfig
func (s *StreamHandler) UpdateConfig(newConfig []*pogs.ReverseProxyConfig) (failedConfigs []*pogs.FailedConfig) {
// Delete old configs that aren't in the `newConfig`
toRemove := s.tunnelHostnameMapper.ToRemove(newConfig)
for _, hostnameToRemove := range toRemove {
s.tunnelHostnameMapper.Delete(hostnameToRemove)
}
// Add new configs that weren't in the old mapper
toAdd := s.tunnelHostnameMapper.ToAdd(newConfig)
for _, tunnelConfig := range toAdd {
tunnelHostname := tunnelConfig.TunnelHostname
originSerice, err := tunnelConfig.OriginConfigJSONHandler.OriginConfig.Service()
if err != nil {
s.logger.WithField("tunnelHostname", tunnelHostname).WithError(err).Error("Invalid origin service config")
failedConfigs = append(failedConfigs, &pogs.FailedConfig{
Config: tunnelConfig,
Reason: tunnelConfig.FailReason(err),
})
continue
}
s.tunnelHostnameMapper.Add(tunnelConfig.TunnelHostname, originSerice)
s.logger.WithField("tunnelHostname", tunnelHostname).Infof("New origin service config: %v", originSerice.Summary())
}
return
}
// ServeStream implements MuxedStreamHandler interface
func (s *StreamHandler) ServeStream(stream *h2mux.MuxedStream) error {
if stream.IsRPCStream() {
return s.serveRPC(stream)
}
if err := s.serveRequest(stream); err != nil {
s.logger.Error(err)
return err
}
return nil
}
func (s *StreamHandler) serveRPC(stream *h2mux.MuxedStream) error {
stream.WriteHeaders([]h2mux.Header{{Name: ":status", Value: "200"}})
main := pogs.ClientService_ServerToClient(s)
rpcLogger := s.logger.WithField("subsystem", "clientserver-rpc")
rpcConn := rpc.NewConn(
tunnelrpc.NewTransportLogger(rpcLogger, rpc.StreamTransport(stream)),
rpc.MainInterface(main.Client),
tunnelrpc.ConnLog(s.logger.WithField("subsystem", "clientserver-rpc-transport")),
)
return rpcConn.Wait()
}
func (s *StreamHandler) serveRequest(stream *h2mux.MuxedStream) error {
tunnelHostname := stream.TunnelHostname()
if !tunnelHostname.IsSet() {
s.writeErrorStatus(stream, statusBadRequest)
return fmt.Errorf("stream doesn't have tunnelHostname")
}
originService, ok := s.tunnelHostnameMapper.Get(tunnelHostname)
if !ok {
s.writeErrorStatus(stream, statusNotFound)
return fmt.Errorf("cannot map tunnel hostname %s to origin", tunnelHostname)
}
req, err := createRequest(stream, originService.URL())
if err != nil {
s.writeErrorStatus(stream, statusBadRequest)
return errors.Wrap(err, "cannot create request")
}
logger := s.requestLogger(req, tunnelHostname)
logger.Debugf("Request Headers %+v", req.Header)
resp, err := originService.Proxy(stream, req)
if err != nil {
s.writeErrorStatus(stream, statusBadGateway)
return errors.Wrap(err, "cannot proxy request")
}
logger.WithField("status", resp.Status).Debugf("Response Headers %+v", resp.Header)
return nil
}
func (s *StreamHandler) requestLogger(req *http.Request, tunnelHostname h2mux.TunnelHostname) *logrus.Entry {
cfRay := FindCfRayHeader(req)
lbProbe := IsLBProbeRequest(req)
logger := s.logger.WithField("tunnelHostname", tunnelHostname)
if cfRay != "" {
logger = logger.WithField("CF-RAY", cfRay)
logger.Debugf("%s %s %s", req.Method, req.URL, req.Proto)
} else if lbProbe {
logger.Debugf("Load Balancer health check %s %s %s", req.Method, req.URL, req.Proto)
} else {
logger.Warnf("Requests %v does not have CF-RAY header. Please open a support ticket with Cloudflare.", req)
}
return logger
}
func (s *StreamHandler) writeErrorStatus(stream *h2mux.MuxedStream, status *httpErrorStatus) {
stream.WriteHeaders([]h2mux.Header{
{
Name: statusPseudoHeader,
Value: status.status,
},
})
stream.Write(status.text)
}
+227
View File
@@ -0,0 +1,227 @@
package streamhandler
import (
"context"
"io"
"net"
"net/http"
"net/http/httptest"
"strconv"
"sync"
"testing"
"time"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"golang.org/x/sync/errgroup"
)
const (
testOpenStreamTimeout = time.Millisecond * 5000
testHandshakeTimeout = time.Millisecond * 1000
)
var (
testTunnelHostname = h2mux.TunnelHostname("123.cftunnel.com")
baseHeaders = []h2mux.Header{
{Name: ":method", Value: "GET"},
{Name: ":scheme", Value: "http"},
{Name: ":authority", Value: "example.com"},
{Name: ":path", Value: "/"},
}
tunnelHostnameHeader = h2mux.Header{
Name: h2mux.CloudflaredProxyTunnelHostnameHeader,
Value: testTunnelHostname.String(),
}
)
func TestServeRequest(t *testing.T) {
configChan := make(chan *pogs.ClientConfig)
useConfigResultChan := make(chan *pogs.UseConfigurationResult)
streamHandler := NewStreamHandler(configChan, useConfigResultChan, logrus.New())
message := []byte("Hello cloudflared")
httpServer := httptest.NewServer(&mockHTTPHandler{message})
reverseProxyConfigs := []*pogs.ReverseProxyConfig{
{
TunnelHostname: testTunnelHostname,
OriginConfigJSONHandler: &pogs.OriginConfigJSONHandler{
OriginConfig: &pogs.HTTPOriginConfig{
URLString: httpServer.URL,
},
},
},
}
streamHandler.UpdateConfig(reverseProxyConfigs)
muxPair := NewDefaultMuxerPair(t, streamHandler)
muxPair.Serve(t)
ctx, cancel := context.WithTimeout(context.Background(), testOpenStreamTimeout)
defer cancel()
headers := append(baseHeaders, tunnelHostnameHeader)
stream, err := muxPair.EdgeMux.OpenStream(ctx, headers, nil)
assert.NoError(t, err)
assertStatusHeader(t, http.StatusOK, stream.Headers)
assertRespBody(t, message, stream)
}
func TestServeBadRequest(t *testing.T) {
configChan := make(chan *pogs.ClientConfig)
useConfigResultChan := make(chan *pogs.UseConfigurationResult)
streamHandler := NewStreamHandler(configChan, useConfigResultChan, logrus.New())
muxPair := NewDefaultMuxerPair(t, streamHandler)
muxPair.Serve(t)
ctx, cancel := context.WithTimeout(context.Background(), testOpenStreamTimeout)
defer cancel()
// No tunnel hostname header, expect to get 400 Bad Request
stream, err := muxPair.EdgeMux.OpenStream(ctx, baseHeaders, nil)
assert.NoError(t, err)
assertStatusHeader(t, http.StatusBadRequest, stream.Headers)
assertRespBody(t, statusBadRequest.text, stream)
// No mapping for the tunnel hostname, expect to get 404 Not Found
headers := append(baseHeaders, tunnelHostnameHeader)
stream, err = muxPair.EdgeMux.OpenStream(ctx, headers, nil)
assert.NoError(t, err)
assertStatusHeader(t, http.StatusNotFound, stream.Headers)
assertRespBody(t, statusNotFound.text, stream)
// Nothing listening on empty url, so proxy would fail. Expect to get 502 Bad Gateway
reverseProxyConfigs := []*pogs.ReverseProxyConfig{
{
TunnelHostname: testTunnelHostname,
OriginConfigJSONHandler: &pogs.OriginConfigJSONHandler{
OriginConfig: &pogs.HTTPOriginConfig{
URLString: "",
},
},
},
}
streamHandler.UpdateConfig(reverseProxyConfigs)
stream, err = muxPair.EdgeMux.OpenStream(ctx, headers, nil)
assert.NoError(t, err)
assertStatusHeader(t, http.StatusBadGateway, stream.Headers)
assertRespBody(t, statusBadGateway.text, stream)
// Invalid content-length, wouldn't not be able to create a request. Expect to get 400 Bad Request
headers = append(headers, h2mux.Header{
Name: "content-length",
Value: "x",
})
stream, err = muxPair.EdgeMux.OpenStream(ctx, headers, nil)
assert.NoError(t, err)
assertStatusHeader(t, http.StatusBadRequest, stream.Headers)
assertRespBody(t, statusBadRequest.text, stream)
}
func assertStatusHeader(t *testing.T, expectedStatus int, headers []h2mux.Header) {
assert.Equal(t, statusPseudoHeader, headers[0].Name)
assert.Equal(t, strconv.Itoa(expectedStatus), headers[0].Value)
}
func assertRespBody(t *testing.T, expectedRespBody []byte, stream *h2mux.MuxedStream) {
respBody := make([]byte, len(expectedRespBody))
_, err := stream.Read(respBody)
assert.NoError(t, err)
assert.Equal(t, expectedRespBody, respBody)
}
type DefaultMuxerPair struct {
OriginMuxConfig h2mux.MuxerConfig
OriginMux *h2mux.Muxer
OriginConn net.Conn
EdgeMuxConfig h2mux.MuxerConfig
EdgeMux *h2mux.Muxer
EdgeConn net.Conn
doneC chan struct{}
}
func NewDefaultMuxerPair(t assert.TestingT, h h2mux.MuxedStreamHandler) *DefaultMuxerPair {
origin, edge := net.Pipe()
p := &DefaultMuxerPair{
OriginMuxConfig: h2mux.MuxerConfig{
Timeout: testHandshakeTimeout,
Handler: h,
IsClient: true,
Name: "origin",
Logger: logrus.NewEntry(logrus.New()),
DefaultWindowSize: (1 << 8) - 1,
MaxWindowSize: (1 << 15) - 1,
StreamWriteBufferMaxLen: 1024,
},
OriginConn: origin,
EdgeMuxConfig: h2mux.MuxerConfig{
Timeout: testHandshakeTimeout,
IsClient: false,
Name: "edge",
Logger: logrus.NewEntry(logrus.New()),
DefaultWindowSize: (1 << 8) - 1,
MaxWindowSize: (1 << 15) - 1,
StreamWriteBufferMaxLen: 1024,
},
EdgeConn: edge,
doneC: make(chan struct{}),
}
assert.NoError(t, p.Handshake())
return p
}
func (p *DefaultMuxerPair) Handshake() error {
ctx, cancel := context.WithTimeout(context.Background(), testHandshakeTimeout)
defer cancel()
errGroup, _ := errgroup.WithContext(ctx)
errGroup.Go(func() (err error) {
p.EdgeMux, err = h2mux.Handshake(p.EdgeConn, p.EdgeConn, p.EdgeMuxConfig)
return errors.Wrap(err, "edge handshake failure")
})
errGroup.Go(func() (err error) {
p.OriginMux, err = h2mux.Handshake(p.OriginConn, p.OriginConn, p.OriginMuxConfig)
return errors.Wrap(err, "origin handshake failure")
})
return errGroup.Wait()
}
func (p *DefaultMuxerPair) Serve(t assert.TestingT) {
ctx := context.Background()
var wg sync.WaitGroup
wg.Add(2)
go func() {
err := p.EdgeMux.Serve(ctx)
if err != nil && err != io.EOF && err != io.ErrClosedPipe {
t.Errorf("error in edge muxer Serve(): %s", err)
}
p.OriginMux.Shutdown()
wg.Done()
}()
go func() {
err := p.OriginMux.Serve(ctx)
if err != nil && err != io.EOF && err != io.ErrClosedPipe {
t.Errorf("error in origin muxer Serve(): %s", err)
}
p.EdgeMux.Shutdown()
wg.Done()
}()
go func() {
// notify when both muxes have stopped serving
wg.Wait()
close(p.doneC)
}()
}
type mockHTTPHandler struct {
message []byte
}
func (mth *mockHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write(mth.message)
}
+179
View File
@@ -0,0 +1,179 @@
package supervisor
import (
"context"
"crypto/tls"
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"golang.org/x/sync/errgroup"
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/streamhandler"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/sirupsen/logrus"
)
type Supervisor struct {
connManager *connection.EdgeManager
streamHandler *streamhandler.StreamHandler
autoupdater *updater.AutoUpdater
supportAutoupdate bool
newConfigChan <-chan *pogs.ClientConfig
useConfigResultChan chan<- *pogs.UseConfigurationResult
state *state
logger *logrus.Entry
}
func NewSupervisor(
defaultClientConfig *pogs.ClientConfig,
userCredential []byte,
tlsConfig *tls.Config,
serviceDiscoverer connection.EdgeServiceDiscoverer,
cloudflaredConfig *connection.CloudflaredConfig,
autoupdater *updater.AutoUpdater,
supportAutoupdate bool,
logger *logrus.Logger,
) (*Supervisor, error) {
newConfigChan := make(chan *pogs.ClientConfig)
useConfigResultChan := make(chan *pogs.UseConfigurationResult)
streamHandler := streamhandler.NewStreamHandler(newConfigChan, useConfigResultChan, logger)
invalidConfigs := streamHandler.UpdateConfig(defaultClientConfig.ReverseProxyConfigs)
if len(invalidConfigs) > 0 {
for _, invalidConfig := range invalidConfigs {
logger.Errorf("Tunnel %+v is invalid, reason: %s", invalidConfig.Config, invalidConfig.Reason)
}
return nil, fmt.Errorf("At least 1 Tunnel config is invalid")
}
tunnelHostnames := make([]h2mux.TunnelHostname, len(defaultClientConfig.ReverseProxyConfigs))
for i, reverseProxyConfig := range defaultClientConfig.ReverseProxyConfigs {
tunnelHostnames[i] = reverseProxyConfig.TunnelHostname
}
defaultEdgeMgrConfigurable := &connection.EdgeManagerConfigurable{
TunnelHostnames: tunnelHostnames,
EdgeConnectionConfig: defaultClientConfig.EdgeConnectionConfig,
}
return &Supervisor{
connManager: connection.NewEdgeManager(streamHandler, defaultEdgeMgrConfigurable, userCredential, tlsConfig,
serviceDiscoverer, cloudflaredConfig, logger),
streamHandler: streamHandler,
autoupdater: autoupdater,
supportAutoupdate: supportAutoupdate,
newConfigChan: newConfigChan,
useConfigResultChan: useConfigResultChan,
state: newState(defaultClientConfig),
logger: logger.WithField("subsystem", "supervisor"),
}, nil
}
func (s *Supervisor) Run(ctx context.Context) error {
errGroup, groupCtx := errgroup.WithContext(ctx)
errGroup.Go(func() error {
return s.connManager.Run(groupCtx)
})
errGroup.Go(func() error {
return s.listenToNewConfig(groupCtx)
})
errGroup.Go(func() error {
return s.listenToShutdownSignal(groupCtx)
})
if s.supportAutoupdate {
errGroup.Go(func() error {
return s.autoupdater.Run(groupCtx)
})
}
err := errGroup.Wait()
s.logger.Warnf("Supervisor terminated, reason: %v", err)
return err
}
func (s *Supervisor) listenToShutdownSignal(serveCtx context.Context) error {
signals := make(chan os.Signal, 10)
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
defer signal.Stop(signals)
select {
case <-serveCtx.Done():
return serveCtx.Err()
case sig := <-signals:
return fmt.Errorf("received %v signal", sig)
}
}
func (s *Supervisor) listenToNewConfig(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
case newConfig := <-s.newConfigChan:
s.useConfigResultChan <- s.notifySubsystemsNewConfig(newConfig)
}
}
}
func (s *Supervisor) notifySubsystemsNewConfig(newConfig *pogs.ClientConfig) *pogs.UseConfigurationResult {
s.logger.Infof("Received configuration %v", newConfig.Version)
if s.state.hasAppliedVersion(newConfig.Version) {
s.logger.Infof("%v has been applied", newConfig.Version)
return &pogs.UseConfigurationResult{
Success: true,
}
}
s.state.updateConfig(newConfig)
var tunnelHostnames []h2mux.TunnelHostname
for _, tunnelConfig := range newConfig.ReverseProxyConfigs {
tunnelHostnames = append(tunnelHostnames, tunnelConfig.TunnelHostname)
}
// Update connManager configurable
s.connManager.UpdateConfigurable(&connection.EdgeManagerConfigurable{
TunnelHostnames: tunnelHostnames,
EdgeConnectionConfig: newConfig.EdgeConnectionConfig,
})
// Update streamHandler tunnelHostnameMapper mapping
failedConfigs := s.streamHandler.UpdateConfig(newConfig.ReverseProxyConfigs)
if s.supportAutoupdate {
s.autoupdater.Update(newConfig.SupervisorConfig.AutoUpdateFrequency)
}
return &pogs.UseConfigurationResult{
Success: len(failedConfigs) == 0,
FailedConfigs: failedConfigs,
}
}
type state struct {
sync.RWMutex
currentConfig *pogs.ClientConfig
}
func newState(currentConfig *pogs.ClientConfig) *state {
return &state{
currentConfig: currentConfig,
}
}
func (s *state) hasAppliedVersion(incomingVersion pogs.Version) bool {
s.RLock()
defer s.RUnlock()
return s.currentConfig.Version.IsNewerOrEqual(incomingVersion)
}
func (s *state) updateConfig(newConfig *pogs.ClientConfig) {
s.Lock()
defer s.Unlock()
s.currentConfig = newConfig
}
+148
View File
@@ -2,10 +2,22 @@ package tlsconfig
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net"
"runtime"
"sync"
"github.com/getsentry/raven-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"gopkg.in/urfave/cli.v2"
)
const (
OriginCAPoolFlag = "origin-ca-pool"
CaCertFlag = "cacert"
)
// CertReloader can load and reload a TLS certificate from a particular filepath.
@@ -51,3 +63,139 @@ func (cr *CertReloader) LoadCert() error {
cr.certificate = &cert
return nil
}
func LoadOriginCA(c *cli.Context, logger *logrus.Logger) (*x509.CertPool, error) {
var originCustomCAPool []byte
originCAPoolFilename := c.String(OriginCAPoolFlag)
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, logger)
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 LoadCustomOriginCA(originCAFilename string) (*x509.CertPool, error) {
// First, obtain the system certificate pool
certPool, err := x509.SystemCertPool()
if err != nil {
certPool = x509.NewCertPool()
}
// Next, append the Cloudflare CAs into the system pool
cfRootCA, err := 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)
}
if originCAFilename == "" {
return certPool, nil
}
customOriginCA, err := ioutil.ReadFile(originCAFilename)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("unable to read the file %s", originCAFilename))
}
if !certPool.AppendCertsFromPEM(customOriginCA) {
return nil, fmt.Errorf("error appending custom CA to cert pool")
}
return certPool, nil
}
func CreateTunnelConfig(c *cli.Context) (*tls.Config, error) {
var rootCAs []string
if c.String(CaCertFlag) != "" {
rootCAs = append(rootCAs, c.String(CaCertFlag))
}
userConfig := &TLSParameters{RootCAs: rootCAs}
tlsConfig, err := GetConfig(userConfig)
if err != nil {
return nil, err
}
if tlsConfig.RootCAs == nil {
rootCAPool := x509.NewCertPool()
cfRootCA, err := 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 edgeAddrs := c.StringSlice("edge"); 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 loadOriginCertPool(originCAPoolPEM []byte, logger *logrus.Logger) (*x509.CertPool, error) {
// Get the global pool
certPool, err := loadGlobalCertPool(logger)
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(logger *logrus.Logger) (*x509.CertPool, error) {
// First, obtain the system certificate pool
certPool, err := x509.SystemCertPool()
if err != nil {
if runtime.GOOS != "windows" { // See https://github.com/golang/go/issues/16736
logger.WithError(err).Warn("error obtaining the system certificates")
}
certPool = x509.NewCertPool()
}
// Next, append the Cloudflare CAs into the system pool
cfRootCA, err := 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 := 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
}
+1 -1
View File
@@ -82,7 +82,7 @@ func (u *UpstreamHTTPS) exchangeWireformat(msg []byte) ([]byte, error) {
return nil, errors.Wrap(err, "failed to create an HTTPS request")
}
req.Header.Add("Content-Type", "application/dns-udpwireformat")
req.Header.Add("Content-Type", "application/dns-message")
req.Host = u.endpoint.Hostname()
resp, err := u.client.Do(req)
@@ -0,0 +1,93 @@
package tunnelhostnamemapper
import (
"sync"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/originservice"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
// TunnelHostnameMapper maps TunnelHostname to an OriginService
type TunnelHostnameMapper struct {
sync.RWMutex
tunnelHostnameToOrigin map[h2mux.TunnelHostname]originservice.OriginService
}
func NewTunnelHostnameMapper() *TunnelHostnameMapper {
return &TunnelHostnameMapper{
tunnelHostnameToOrigin: make(map[h2mux.TunnelHostname]originservice.OriginService),
}
}
// Get an OriginService given a TunnelHostname
func (om *TunnelHostnameMapper) Get(key h2mux.TunnelHostname) (originservice.OriginService, bool) {
om.RLock()
defer om.RUnlock()
originService, ok := om.tunnelHostnameToOrigin[key]
return originService, ok
}
// Add a mapping. If there is already an OriginService with this key, shutdown the old origin service and replace it
// with the new one
func (om *TunnelHostnameMapper) Add(key h2mux.TunnelHostname, os originservice.OriginService) {
om.Lock()
defer om.Unlock()
if oldOS, ok := om.tunnelHostnameToOrigin[key]; ok {
oldOS.Shutdown()
}
om.tunnelHostnameToOrigin[key] = os
}
// Delete a mapping, and shutdown its OriginService
func (om *TunnelHostnameMapper) Delete(key h2mux.TunnelHostname) (keyFound bool) {
om.Lock()
defer om.Unlock()
if os, ok := om.tunnelHostnameToOrigin[key]; ok {
os.Shutdown()
delete(om.tunnelHostnameToOrigin, key)
return true
}
return false
}
// ToRemove finds all keys that should be removed from the TunnelHostnameMapper.
func (om *TunnelHostnameMapper) ToRemove(newConfigs []*pogs.ReverseProxyConfig) (toRemove []h2mux.TunnelHostname) {
om.Lock()
defer om.Unlock()
// Convert into a set, for O(1) lookups instead of O(n)
newConfigSet := toSet(newConfigs)
// If a config in `om` isn't in `newConfigs`, it must be removed.
for hostname := range om.tunnelHostnameToOrigin {
if _, ok := newConfigSet[hostname]; !ok {
toRemove = append(toRemove, hostname)
}
}
return
}
// ToAdd filters the given configs, keeping those that should be added to the TunnelHostnameMapper.
func (om *TunnelHostnameMapper) ToAdd(newConfigs []*pogs.ReverseProxyConfig) (toAdd []*pogs.ReverseProxyConfig) {
om.Lock()
defer om.Unlock()
// If a config in `newConfigs` isn't in `om`, it must be added.
for _, config := range newConfigs {
if _, ok := om.tunnelHostnameToOrigin[config.TunnelHostname]; !ok {
toAdd = append(toAdd, config)
}
}
return
}
func toSet(configs []*pogs.ReverseProxyConfig) map[h2mux.TunnelHostname]*pogs.ReverseProxyConfig {
m := make(map[h2mux.TunnelHostname]*pogs.ReverseProxyConfig)
for _, config := range configs {
m[config.TunnelHostname] = config
}
return m
}
@@ -0,0 +1,212 @@
package tunnelhostnamemapper
import (
"fmt"
"net/http"
"net/url"
"reflect"
"sync"
"testing"
"time"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/originservice"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/stretchr/testify/assert"
)
const (
routines = 1000
)
func TestTunnelHostnameMapperConcurrentAccess(t *testing.T) {
thm := NewTunnelHostnameMapper()
concurrentOps(t, func(i int) {
// om is empty
os, ok := thm.Get(tunnelHostname(i))
assert.False(t, ok)
assert.Nil(t, os)
})
firstURL, err := url.Parse("https://127.0.0.1:8080")
assert.NoError(t, err)
httpOS := originservice.NewHTTPService(http.DefaultTransport, firstURL, false)
concurrentOps(t, func(i int) {
thm.Add(tunnelHostname(i), httpOS)
})
concurrentOps(t, func(i int) {
os, ok := thm.Get(tunnelHostname(i))
assert.True(t, ok)
assert.Equal(t, httpOS, os)
})
secondURL, err := url.Parse("https://127.0.0.1:8080")
assert.NoError(t, err)
secondHTTPOS := originservice.NewHTTPService(http.DefaultTransport, secondURL, true)
concurrentOps(t, func(i int) {
// Add should httpOS with secondHTTPOS
thm.Add(tunnelHostname(i), secondHTTPOS)
})
concurrentOps(t, func(i int) {
os, ok := thm.Get(tunnelHostname(i))
assert.True(t, ok)
assert.Equal(t, secondHTTPOS, os)
})
}
func concurrentOps(t *testing.T, f func(i int)) {
var wg sync.WaitGroup
wg.Add(routines)
for i := 0; i < routines; i++ {
go func(i int) {
f(i)
wg.Done()
}(i)
}
wg.Wait()
}
func tunnelHostname(i int) h2mux.TunnelHostname {
return h2mux.TunnelHostname(fmt.Sprintf("%d.cftunnel.com", i))
}
func Test_toSet(t *testing.T) {
type args struct {
configs []*pogs.ReverseProxyConfig
}
tests := []struct {
name string
args args
want map[h2mux.TunnelHostname]*pogs.ReverseProxyConfig
}{
{
name: "empty slice should yield empty map",
args: args{},
want: map[h2mux.TunnelHostname]*pogs.ReverseProxyConfig{},
},
{
name: "multiple elements",
args: args{[]*pogs.ReverseProxyConfig{sampleConfig1(), sampleConfig2()}},
want: map[h2mux.TunnelHostname]*pogs.ReverseProxyConfig{
"mock.example.com": sampleConfig1(),
"mock2.example.com": sampleConfig2(),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := toSet(tt.args.configs); !reflect.DeepEqual(got, tt.want) {
t.Errorf("toSet() = %v, want %v", got, tt.want)
}
})
}
}
func TestTunnelHostnameMapper_ToAdd(t *testing.T) {
type fields struct {
tunnelHostnameToOrigin map[h2mux.TunnelHostname]originservice.OriginService
}
type args struct {
newConfigs []*pogs.ReverseProxyConfig
}
tests := []struct {
name string
fields fields
args args
wantToAdd []*pogs.ReverseProxyConfig
}{
{
name: "Mapper={}, NewConfig={}, toAdd={}",
},
{
name: "Mapper={}, NewConfig={x}, toAdd={x}",
args: args{newConfigs: []*pogs.ReverseProxyConfig{sampleConfig1()}},
wantToAdd: []*pogs.ReverseProxyConfig{sampleConfig1()},
},
{
name: "Mapper={x}, NewConfig={x,y}, toAdd={y}",
args: args{newConfigs: []*pogs.ReverseProxyConfig{sampleConfig2()}},
wantToAdd: []*pogs.ReverseProxyConfig{sampleConfig2()},
fields: fields{tunnelHostnameToOrigin: map[h2mux.TunnelHostname]originservice.OriginService{
h2mux.TunnelHostname(sampleConfig1().TunnelHostname): &originservice.HelloWorldService{},
}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
om := &TunnelHostnameMapper{
tunnelHostnameToOrigin: tt.fields.tunnelHostnameToOrigin,
}
if gotToAdd := om.ToAdd(tt.args.newConfigs); !reflect.DeepEqual(gotToAdd, tt.wantToAdd) {
t.Errorf("TunnelHostnameMapper.ToAdd() = %v, want %v", gotToAdd, tt.wantToAdd)
}
})
}
}
func TestTunnelHostnameMapper_ToRemove(t *testing.T) {
type fields struct {
tunnelHostnameToOrigin map[h2mux.TunnelHostname]originservice.OriginService
}
type args struct {
newConfigs []*pogs.ReverseProxyConfig
}
tests := []struct {
name string
fields fields
args args
wantToRemove []h2mux.TunnelHostname
}{
{
name: "Mapper={}, NewConfig={}, toRemove={}",
},
{
name: "Mapper={x}, NewConfig={}, toRemove={x}",
wantToRemove: []h2mux.TunnelHostname{sampleConfig1().TunnelHostname},
fields: fields{tunnelHostnameToOrigin: map[h2mux.TunnelHostname]originservice.OriginService{
h2mux.TunnelHostname(sampleConfig1().TunnelHostname): &originservice.HelloWorldService{},
}},
},
{
name: "Mapper={x}, NewConfig={x}, toRemove={}",
args: args{newConfigs: []*pogs.ReverseProxyConfig{sampleConfig1()}},
fields: fields{tunnelHostnameToOrigin: map[h2mux.TunnelHostname]originservice.OriginService{
h2mux.TunnelHostname(sampleConfig1().TunnelHostname): &originservice.HelloWorldService{},
}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
om := &TunnelHostnameMapper{
tunnelHostnameToOrigin: tt.fields.tunnelHostnameToOrigin,
}
if gotToRemove := om.ToRemove(tt.args.newConfigs); !reflect.DeepEqual(gotToRemove, tt.wantToRemove) {
t.Errorf("TunnelHostnameMapper.ToRemove() = %v, want %v", gotToRemove, tt.wantToRemove)
}
})
}
}
func sampleConfig1() *pogs.ReverseProxyConfig {
return &pogs.ReverseProxyConfig{
TunnelHostname: "mock.example.com",
OriginConfigJSONHandler: &pogs.OriginConfigJSONHandler{OriginConfig: &pogs.HelloWorldOriginConfig{}},
Retries: 18,
ConnectionTimeout: 5 * time.Second,
CompressionQuality: 3,
}
}
func sampleConfig2() *pogs.ReverseProxyConfig {
return &pogs.ReverseProxyConfig{
TunnelHostname: "mock2.example.com",
OriginConfigJSONHandler: &pogs.OriginConfigJSONHandler{OriginConfig: &pogs.HelloWorldOriginConfig{}},
Retries: 18,
ConnectionTimeout: 5 * time.Second,
CompressionQuality: 3,
}
}
+799
View File
@@ -0,0 +1,799 @@
package pogs
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"net"
"net/http"
"net/url"
"time"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/originservice"
"github.com/cloudflare/cloudflared/tlsconfig"
"github.com/cloudflare/cloudflared/tunnelrpc"
"github.com/pkg/errors"
capnp "zombiezen.com/go/capnproto2"
"zombiezen.com/go/capnproto2/pogs"
"zombiezen.com/go/capnproto2/rpc"
"zombiezen.com/go/capnproto2/server"
)
///
/// Structs
///
// ClientConfig is a collection of FallibleConfig that determines how cloudflared should function
type ClientConfig struct {
Version Version `json:"version"`
SupervisorConfig *SupervisorConfig `json:"supervisor_config"`
EdgeConnectionConfig *EdgeConnectionConfig `json:"edge_connection_config"`
DoHProxyConfigs []*DoHProxyConfig `json:"doh_proxy_configs" capnp:"dohProxyConfigs"`
ReverseProxyConfigs []*ReverseProxyConfig `json:"reverse_proxy_configs"`
}
// Version type models the version of a ClientConfig
type Version uint64
func InitVersion() Version {
return Version(0)
}
func (v Version) IsNewerOrEqual(comparedVersion Version) bool {
return v >= comparedVersion
}
func (v Version) String() string {
return fmt.Sprintf("Version: %d", v)
}
// FallibleConfig is an interface implemented by configs that cloudflared might not be able to apply
type FallibleConfig interface {
FailReason(err error) string
jsonType() string
}
// SupervisorConfig specifies config of components managed by Supervisor other than ConnectionManager
type SupervisorConfig struct {
AutoUpdateFrequency time.Duration `json:"auto_update_frequency"`
MetricsUpdateFrequency time.Duration `json:"metrics_update_frequency"`
GracePeriod time.Duration `json:"grace_period"`
}
// FailReason impelents FallibleConfig interface for SupervisorConfig
func (sc *SupervisorConfig) FailReason(err error) string {
return fmt.Sprintf("Cannot apply SupervisorConfig, err: %v", err)
}
func (sc *SupervisorConfig) MarshalJSON() ([]byte, error) {
marshaler := make(map[string]SupervisorConfig, 1)
marshaler[sc.jsonType()] = *sc
return json.Marshal(marshaler)
}
func (sc *SupervisorConfig) jsonType() string {
return "supervisor_config"
}
// EdgeConnectionConfig specifies what parameters and how may connections should ConnectionManager establish with edge
type EdgeConnectionConfig struct {
NumHAConnections uint8 `json:"num_ha_connections"`
HeartbeatInterval time.Duration `json:"heartbeat_interval"`
Timeout time.Duration `json:"timeout"`
MaxFailedHeartbeats uint64 `json:"max_failed_heartbeats"`
UserCredentialPath string `json:"user_credential_path"`
}
// FailReason impelents FallibleConfig interface for EdgeConnectionConfig
func (cmc *EdgeConnectionConfig) FailReason(err error) string {
return fmt.Sprintf("Cannot apply EdgeConnectionConfig, err: %v", err)
}
func (cmc *EdgeConnectionConfig) MarshalJSON() ([]byte, error) {
marshaler := make(map[string]EdgeConnectionConfig, 1)
marshaler[cmc.jsonType()] = *cmc
return json.Marshal(marshaler)
}
func (cmc *EdgeConnectionConfig) jsonType() string {
return "edge_connection_config"
}
// DoHProxyConfig is configuration for DNS over HTTPS service
type DoHProxyConfig struct {
ListenHost string `json:"listen_host"`
ListenPort uint16 `json:"listen_port"`
Upstreams []string `json:"upstreams"`
}
// FailReason impelents FallibleConfig interface for DoHProxyConfig
func (dpc *DoHProxyConfig) FailReason(err error) string {
return fmt.Sprintf("Cannot apply DoHProxyConfig, err: %v", err)
}
func (dpc *DoHProxyConfig) MarshalJSON() ([]byte, error) {
marshaler := make(map[string]DoHProxyConfig, 1)
marshaler[dpc.jsonType()] = *dpc
return json.Marshal(marshaler)
}
func (dpc *DoHProxyConfig) jsonType() string {
return "doh_proxy_config"
}
// ReverseProxyConfig how and for what hostnames can this cloudflared proxy
type ReverseProxyConfig struct {
TunnelHostname h2mux.TunnelHostname `json:"tunnel_hostname"`
OriginConfigJSONHandler *OriginConfigJSONHandler `json:"origin_config"`
Retries uint64 `json:"retries"`
ConnectionTimeout time.Duration `json:"connection_timeout"`
CompressionQuality uint64 `json:"compression_quality"`
}
func NewReverseProxyConfig(
tunnelHostname string,
originConfig OriginConfig,
retries uint64,
connectionTimeout time.Duration,
compressionQuality uint64,
) (*ReverseProxyConfig, error) {
if originConfig == nil {
return nil, fmt.Errorf("NewReverseProxyConfig: originConfigUnmarshaler was null")
}
return &ReverseProxyConfig{
TunnelHostname: h2mux.TunnelHostname(tunnelHostname),
OriginConfigJSONHandler: &OriginConfigJSONHandler{originConfig},
Retries: retries,
ConnectionTimeout: connectionTimeout,
CompressionQuality: compressionQuality,
}, nil
}
// FailReason impelents FallibleConfig interface for ReverseProxyConfig
func (rpc *ReverseProxyConfig) FailReason(err error) string {
return fmt.Sprintf("Cannot apply ReverseProxyConfig, err: %v", err)
}
func (rpc *ReverseProxyConfig) MarshalJSON() ([]byte, error) {
marshaler := make(map[string]ReverseProxyConfig, 1)
marshaler[rpc.jsonType()] = *rpc
return json.Marshal(marshaler)
}
func (rpc *ReverseProxyConfig) jsonType() string {
return "reverse_proxy_config"
}
//go-sumtype:decl OriginConfig
type OriginConfig interface {
// Service returns a OriginService used to proxy to the origin
Service() (originservice.OriginService, error)
// go-sumtype requires at least one unexported method, otherwise it will complain that interface is not sealed
jsonType() string
}
type originType int
const (
httpType originType = iota
wsType
helloWorldType
)
func (ot originType) String() string {
switch ot {
case httpType:
return "Http"
case wsType:
return "WebSocket"
case helloWorldType:
return "HelloWorld"
default:
return "unknown"
}
}
type HTTPOriginConfig struct {
URLString string `capnp:"urlString" json:"url_string" mapstructure:"url_string"`
TCPKeepAlive time.Duration `capnp:"tcpKeepAlive" json:"tcp_keep_alive" mapstructure:"tcp_keep_alive"`
DialDualStack bool `json:"dial_dual_stack" mapstructure:"dial_dual_stack"`
TLSHandshakeTimeout time.Duration `capnp:"tlsHandshakeTimeout" json:"tls_handshake_timeout" mapstructure:"tls_handshake_timeout"`
TLSVerify bool `capnp:"tlsVerify" json:"tls_verify" mapstructure:"tls_verify"`
OriginCAPool string `json:"origin_ca_pool" mapstructure:"origin_ca_pool"`
OriginServerName string `json:"origin_server_name" mapstructure:"origin_server_name"`
MaxIdleConnections uint64 `json:"max_idle_connections" mapstructure:"max_idle_connections"`
IdleConnectionTimeout time.Duration `json:"idle_connection_timeout" mapstructure:"idle_connection_timeout"`
ProxyConnectionTimeout time.Duration `json:"proxy_connection_timeout" mapstructure:"proxy_connection_timeout"`
ExpectContinueTimeout time.Duration `json:"expect_continue_timeout" mapstructure:"expect_continue_timeout"`
ChunkedEncoding bool `json:"chunked_encoding" mapstructure:"chunked_encoding"`
}
func (hc *HTTPOriginConfig) Service() (originservice.OriginService, error) {
rootCAs, err := tlsconfig.LoadCustomOriginCA(hc.OriginCAPool)
if err != nil {
return nil, err
}
dialContext := (&net.Dialer{
Timeout: hc.ProxyConnectionTimeout,
KeepAlive: hc.TCPKeepAlive,
DualStack: hc.DialDualStack,
}).DialContext
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: dialContext,
TLSClientConfig: &tls.Config{
RootCAs: rootCAs,
ServerName: hc.OriginServerName,
InsecureSkipVerify: hc.TLSVerify,
},
TLSHandshakeTimeout: hc.TLSHandshakeTimeout,
MaxIdleConns: int(hc.MaxIdleConnections),
IdleConnTimeout: hc.IdleConnectionTimeout,
ExpectContinueTimeout: hc.ExpectContinueTimeout,
}
url, err := url.Parse(hc.URLString)
if err != nil {
return nil, errors.Wrapf(err, "%s is not a valid URL", hc.URLString)
}
if url.Scheme == "unix" {
transport.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
return dialContext(ctx, "unix", url.Host)
}
}
return originservice.NewHTTPService(transport, url, hc.ChunkedEncoding), nil
}
func (_ *HTTPOriginConfig) jsonType() string {
return httpType.String()
}
type WebSocketOriginConfig struct {
URLString string `capnp:"urlString" json:"url_string" mapstructure:"url_string"`
TLSVerify bool `capnp:"tlsVerify" json:"tls_verify" mapstructure:"tls_verify"`
OriginCAPool string `json:"origin_ca_pool" mapstructure:"origin_ca_pool"`
OriginServerName string `json:"origin_server_name" mapstructure:"origin_server_name"`
}
func (wsc *WebSocketOriginConfig) Service() (originservice.OriginService, error) {
rootCAs, err := tlsconfig.LoadCustomOriginCA(wsc.OriginCAPool)
if err != nil {
return nil, err
}
tlsConfig := &tls.Config{
RootCAs: rootCAs,
ServerName: wsc.OriginServerName,
InsecureSkipVerify: wsc.TLSVerify,
}
url, err := url.Parse(wsc.URLString)
if err != nil {
return nil, errors.Wrapf(err, "%s is not a valid URL", wsc.URLString)
}
return originservice.NewWebSocketService(tlsConfig, url)
}
func (_ *WebSocketOriginConfig) jsonType() string {
return wsType.String()
}
type HelloWorldOriginConfig struct{}
func (_ *HelloWorldOriginConfig) Service() (originservice.OriginService, error) {
helloCert, err := tlsconfig.GetHelloCertificateX509()
if err != nil {
return nil, errors.Wrap(err, "Cannot get Hello World server certificate")
}
rootCAs := x509.NewCertPool()
rootCAs.AddCert(helloCert)
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
TLSClientConfig: &tls.Config{
RootCAs: rootCAs,
},
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
return originservice.NewHelloWorldService(transport)
}
func (_ *HelloWorldOriginConfig) jsonType() string {
return helloWorldType.String()
}
/*
* Boilerplate to convert between these structs and the primitive structs
* generated by capnp-go.
* Mnemonics for variable names in this section:
* - `p` is for POGS (plain old Go struct)
* - `s` (and `ss`) is for "capnp.Struct", which is the fundamental type
* underlying the capnp-go data structures.
*/
func MarshalClientConfig(s tunnelrpc.ClientConfig, p *ClientConfig) error {
s.SetVersion(uint64(p.Version))
supervisorConfig, err := s.NewSupervisorConfig()
if err != nil {
return errors.Wrap(err, "failed to get SupervisorConfig")
}
if err = MarshalSupervisorConfig(supervisorConfig, p.SupervisorConfig); err != nil {
return errors.Wrap(err, "MarshalSupervisorConfig error")
}
edgeConnectionConfig, err := s.NewEdgeConnectionConfig()
if err != nil {
return errors.Wrap(err, "failed to get EdgeConnectionConfig")
}
if err := MarshalEdgeConnectionConfig(edgeConnectionConfig, p.EdgeConnectionConfig); err != nil {
return errors.Wrap(err, "MarshalEdgeConnectionConfig error")
}
if err := marshalDoHProxyConfigs(s, p.DoHProxyConfigs); err != nil {
return errors.Wrap(err, "marshalDoHProxyConfigs error")
}
if err := marshalReverseProxyConfigs(s, p.ReverseProxyConfigs); err != nil {
return errors.Wrap(err, "marshalReverseProxyConfigs error")
}
return nil
}
func MarshalSupervisorConfig(s tunnelrpc.SupervisorConfig, p *SupervisorConfig) error {
if err := pogs.Insert(tunnelrpc.SupervisorConfig_TypeID, s.Struct, p); err != nil {
return errors.Wrap(err, "failed to insert SupervisorConfig")
}
return nil
}
func MarshalEdgeConnectionConfig(s tunnelrpc.EdgeConnectionConfig, p *EdgeConnectionConfig) error {
if err := pogs.Insert(tunnelrpc.EdgeConnectionConfig_TypeID, s.Struct, p); err != nil {
return errors.Wrap(err, "failed to insert EdgeConnectionConfig")
}
return nil
}
func marshalDoHProxyConfigs(s tunnelrpc.ClientConfig, dohProxyConfigs []*DoHProxyConfig) error {
capnpList, err := s.NewDohProxyConfigs(int32(len(dohProxyConfigs)))
if err != nil {
return err
}
for i, unmarshalledConfig := range dohProxyConfigs {
err := MarshalDoHProxyConfig(capnpList.At(i), unmarshalledConfig)
if err != nil {
return err
}
}
return nil
}
func marshalReverseProxyConfigs(s tunnelrpc.ClientConfig, reverseProxyConfigs []*ReverseProxyConfig) error {
capnpList, err := s.NewReverseProxyConfigs(int32(len(reverseProxyConfigs)))
if err != nil {
return err
}
for i, unmarshalledConfig := range reverseProxyConfigs {
err := MarshalReverseProxyConfig(capnpList.At(i), unmarshalledConfig)
if err != nil {
return err
}
}
return nil
}
func UnmarshalClientConfig(s tunnelrpc.ClientConfig) (*ClientConfig, error) {
p := new(ClientConfig)
p.Version = Version(s.Version())
supervisorConfig, err := s.SupervisorConfig()
if err != nil {
return nil, errors.Wrap(err, "failed to get SupervisorConfig")
}
p.SupervisorConfig, err = UnmarshalSupervisorConfig(supervisorConfig)
if err != nil {
return nil, errors.Wrap(err, "UnmarshalSupervisorConfig error")
}
edgeConnectionConfig, err := s.EdgeConnectionConfig()
if err != nil {
return nil, errors.Wrap(err, "failed to get ConnectionManagerConfig")
}
p.EdgeConnectionConfig, err = UnmarshalEdgeConnectionConfig(edgeConnectionConfig)
if err != nil {
return nil, errors.Wrap(err, "UnmarshalConnectionManagerConfig error")
}
p.DoHProxyConfigs, err = unmarshalDoHProxyConfigs(s)
if err != nil {
return nil, errors.Wrap(err, "unmarshalDoHProxyConfigs error")
}
p.ReverseProxyConfigs, err = unmarshalReverseProxyConfigs(s)
if err != nil {
return nil, errors.Wrap(err, "unmarshalReverseProxyConfigs error")
}
return p, nil
}
func UnmarshalSupervisorConfig(s tunnelrpc.SupervisorConfig) (*SupervisorConfig, error) {
p := new(SupervisorConfig)
err := pogs.Extract(p, tunnelrpc.SupervisorConfig_TypeID, s.Struct)
return p, err
}
func UnmarshalEdgeConnectionConfig(s tunnelrpc.EdgeConnectionConfig) (*EdgeConnectionConfig, error) {
p := new(EdgeConnectionConfig)
err := pogs.Extract(p, tunnelrpc.EdgeConnectionConfig_TypeID, s.Struct)
return p, err
}
func unmarshalDoHProxyConfigs(s tunnelrpc.ClientConfig) ([]*DoHProxyConfig, error) {
var result []*DoHProxyConfig
marshalledDoHProxyConfigs, err := s.DohProxyConfigs()
if err != nil {
return nil, err
}
for i := 0; i < marshalledDoHProxyConfigs.Len(); i++ {
ss := marshalledDoHProxyConfigs.At(i)
dohProxyConfig, err := UnmarshalDoHProxyConfig(ss)
if err != nil {
return nil, err
}
result = append(result, dohProxyConfig)
}
return result, nil
}
func unmarshalReverseProxyConfigs(s tunnelrpc.ClientConfig) ([]*ReverseProxyConfig, error) {
var result []*ReverseProxyConfig
marshalledReverseProxyConfigs, err := s.ReverseProxyConfigs()
if err != nil {
return nil, err
}
for i := 0; i < marshalledReverseProxyConfigs.Len(); i++ {
ss := marshalledReverseProxyConfigs.At(i)
reverseProxyConfig, err := UnmarshalReverseProxyConfig(ss)
if err != nil {
return nil, err
}
result = append(result, reverseProxyConfig)
}
return result, nil
}
func MarshalUseConfigurationResult(s tunnelrpc.UseConfigurationResult, p *UseConfigurationResult) error {
capnpList, err := s.NewFailedConfigs(int32(len(p.FailedConfigs)))
if err != nil {
return errors.Wrap(err, "Cannot create new FailedConfigs")
}
for i, unmarshalledFailedConfig := range p.FailedConfigs {
err := MarshalFailedConfig(capnpList.At(i), unmarshalledFailedConfig)
if err != nil {
return errors.Wrapf(err, "Cannot MarshalFailedConfig at index %d", i)
}
}
s.SetSuccess(p.Success)
return nil
}
func UnmarshalUseConfigurationResult(s tunnelrpc.UseConfigurationResult) (*UseConfigurationResult, error) {
p := new(UseConfigurationResult)
var failedConfigs []*FailedConfig
marshalledFailedConfigs, err := s.FailedConfigs()
if err != nil {
return nil, errors.Wrap(err, "Cannot get FailedConfigs")
}
for i := 0; i < marshalledFailedConfigs.Len(); i++ {
ss := marshalledFailedConfigs.At(i)
failedConfig, err := UnmarshalFailedConfig(ss)
if err != nil {
return nil, errors.Wrapf(err, "Cannot UnmarshalFailedConfig at index %d", i)
}
failedConfigs = append(failedConfigs, failedConfig)
}
p.FailedConfigs = failedConfigs
p.Success = s.Success()
return p, nil
}
func MarshalDoHProxyConfig(s tunnelrpc.DoHProxyConfig, p *DoHProxyConfig) error {
return pogs.Insert(tunnelrpc.DoHProxyConfig_TypeID, s.Struct, p)
}
func UnmarshalDoHProxyConfig(s tunnelrpc.DoHProxyConfig) (*DoHProxyConfig, error) {
p := new(DoHProxyConfig)
err := pogs.Extract(p, tunnelrpc.DoHProxyConfig_TypeID, s.Struct)
return p, err
}
func MarshalReverseProxyConfig(s tunnelrpc.ReverseProxyConfig, p *ReverseProxyConfig) error {
s.SetTunnelHostname(p.TunnelHostname.String())
switch config := p.OriginConfigJSONHandler.OriginConfig.(type) {
case *HTTPOriginConfig:
ss, err := s.Origin().NewHttp()
if err != nil {
return err
}
if err := MarshalHTTPOriginConfig(ss, config); err != nil {
return err
}
case *WebSocketOriginConfig:
ss, err := s.Origin().NewWebsocket()
if err != nil {
return err
}
if err := MarshalWebSocketOriginConfig(ss, config); err != nil {
return err
}
case *HelloWorldOriginConfig:
ss, err := s.Origin().NewHelloWorld()
if err != nil {
return err
}
if err := MarshalHelloWorldOriginConfig(ss, config); err != nil {
return err
}
default:
return fmt.Errorf("Unknown type for config: %T", config)
}
s.SetRetries(p.Retries)
s.SetConnectionTimeout(p.ConnectionTimeout.Nanoseconds())
s.SetCompressionQuality(p.CompressionQuality)
return nil
}
func UnmarshalReverseProxyConfig(s tunnelrpc.ReverseProxyConfig) (*ReverseProxyConfig, error) {
p := new(ReverseProxyConfig)
tunnelHostname, err := s.TunnelHostname()
if err != nil {
return nil, err
}
p.TunnelHostname = h2mux.TunnelHostname(tunnelHostname)
switch s.Origin().Which() {
case tunnelrpc.ReverseProxyConfig_origin_Which_http:
ss, err := s.Origin().Http()
if err != nil {
return nil, err
}
config, err := UnmarshalHTTPOriginConfig(ss)
if err != nil {
return nil, err
}
p.OriginConfigJSONHandler = &OriginConfigJSONHandler{config}
case tunnelrpc.ReverseProxyConfig_origin_Which_websocket:
ss, err := s.Origin().Websocket()
if err != nil {
return nil, err
}
config, err := UnmarshalWebSocketOriginConfig(ss)
if err != nil {
return nil, err
}
p.OriginConfigJSONHandler = &OriginConfigJSONHandler{config}
case tunnelrpc.ReverseProxyConfig_origin_Which_helloWorld:
ss, err := s.Origin().HelloWorld()
if err != nil {
return nil, err
}
config, err := UnmarshalHelloWorldOriginConfig(ss)
if err != nil {
return nil, err
}
p.OriginConfigJSONHandler = &OriginConfigJSONHandler{config}
}
p.Retries = s.Retries()
p.ConnectionTimeout = time.Duration(s.ConnectionTimeout())
p.CompressionQuality = s.CompressionQuality()
return p, nil
}
func MarshalHTTPOriginConfig(s tunnelrpc.HTTPOriginConfig, p *HTTPOriginConfig) error {
return pogs.Insert(tunnelrpc.HTTPOriginConfig_TypeID, s.Struct, p)
}
func UnmarshalHTTPOriginConfig(s tunnelrpc.HTTPOriginConfig) (*HTTPOriginConfig, error) {
p := new(HTTPOriginConfig)
err := pogs.Extract(p, tunnelrpc.HTTPOriginConfig_TypeID, s.Struct)
return p, err
}
func MarshalWebSocketOriginConfig(s tunnelrpc.WebSocketOriginConfig, p *WebSocketOriginConfig) error {
return pogs.Insert(tunnelrpc.WebSocketOriginConfig_TypeID, s.Struct, p)
}
func UnmarshalWebSocketOriginConfig(s tunnelrpc.WebSocketOriginConfig) (*WebSocketOriginConfig, error) {
p := new(WebSocketOriginConfig)
err := pogs.Extract(p, tunnelrpc.WebSocketOriginConfig_TypeID, s.Struct)
return p, err
}
func MarshalHelloWorldOriginConfig(s tunnelrpc.HelloWorldOriginConfig, p *HelloWorldOriginConfig) error {
return pogs.Insert(tunnelrpc.HelloWorldOriginConfig_TypeID, s.Struct, p)
}
func UnmarshalHelloWorldOriginConfig(s tunnelrpc.HelloWorldOriginConfig) (*HelloWorldOriginConfig, error) {
p := new(HelloWorldOriginConfig)
err := pogs.Extract(p, tunnelrpc.HelloWorldOriginConfig_TypeID, s.Struct)
return p, err
}
type ClientService interface {
UseConfiguration(ctx context.Context, config *ClientConfig) (*UseConfigurationResult, error)
}
type ClientService_PogsClient struct {
Client capnp.Client
Conn *rpc.Conn
}
func (c *ClientService_PogsClient) Close() error {
return c.Conn.Close()
}
func (c *ClientService_PogsClient) UseConfiguration(
ctx context.Context,
config *ClientConfig,
) (*UseConfigurationResult, error) {
client := tunnelrpc.ClientService{Client: c.Client}
promise := client.UseConfiguration(ctx, func(p tunnelrpc.ClientService_useConfiguration_Params) error {
clientServiceConfig, err := p.NewClientServiceConfig()
if err != nil {
return err
}
return MarshalClientConfig(clientServiceConfig, config)
})
retval, err := promise.Result().Struct()
if err != nil {
return nil, err
}
return UnmarshalUseConfigurationResult(retval)
}
func ClientService_ServerToClient(s ClientService) tunnelrpc.ClientService {
return tunnelrpc.ClientService_ServerToClient(ClientService_PogsImpl{s})
}
type ClientService_PogsImpl struct {
impl ClientService
}
func (i ClientService_PogsImpl) UseConfiguration(p tunnelrpc.ClientService_useConfiguration) error {
config, err := p.Params.ClientServiceConfig()
if err != nil {
return errors.Wrap(err, "Cannot get CloudflaredConfig parameter")
}
pogsConfig, err := UnmarshalClientConfig(config)
if err != nil {
return errors.Wrap(err, "Cannot unmarshal tunnelrpc.CloudflaredConfig to *CloudflaredConfig")
}
server.Ack(p.Options)
userConfigResult, err := i.impl.UseConfiguration(p.Ctx, pogsConfig)
if err != nil {
return err
}
result, err := p.Results.NewResult()
if err != nil {
return err
}
return MarshalUseConfigurationResult(result, userConfigResult)
}
type UseConfigurationResult struct {
Success bool `json:"success"`
FailedConfigs []*FailedConfig `json:"failed_configs"`
}
type FailedConfig struct {
Config FallibleConfig `json:"config"`
Reason string `json:"reason"`
}
func MarshalFailedConfig(s tunnelrpc.FailedConfig, p *FailedConfig) error {
switch config := p.Config.(type) {
case *SupervisorConfig:
ss, err := s.Config().NewSupervisor()
if err != nil {
return err
}
err = MarshalSupervisorConfig(ss, config)
if err != nil {
return err
}
case *EdgeConnectionConfig:
ss, err := s.Config().NewEdgeConnection()
if err != nil {
return err
}
err = MarshalEdgeConnectionConfig(ss, config)
if err != nil {
return err
}
case *DoHProxyConfig:
ss, err := s.Config().NewDoh()
if err != nil {
return err
}
err = MarshalDoHProxyConfig(ss, config)
if err != nil {
return err
}
case *ReverseProxyConfig:
ss, err := s.Config().NewReverseProxy()
if err != nil {
return err
}
err = MarshalReverseProxyConfig(ss, config)
if err != nil {
return err
}
default:
return fmt.Errorf("Unknown type for Config: %T", config)
}
s.SetReason(p.Reason)
return nil
}
func UnmarshalFailedConfig(s tunnelrpc.FailedConfig) (*FailedConfig, error) {
p := new(FailedConfig)
switch s.Config().Which() {
case tunnelrpc.FailedConfig_config_Which_supervisor:
ss, err := s.Config().Supervisor()
if err != nil {
return nil, errors.Wrap(err, "Cannot get SupervisorConfig from Config")
}
config, err := UnmarshalSupervisorConfig(ss)
if err != nil {
return nil, errors.Wrap(err, "Cannot UnmarshalSupervisorConfig")
}
p.Config = config
case tunnelrpc.FailedConfig_config_Which_edgeConnection:
ss, err := s.Config().EdgeConnection()
if err != nil {
return nil, errors.Wrap(err, "Cannot get ConnectionManager from Config")
}
config, err := UnmarshalEdgeConnectionConfig(ss)
if err != nil {
return nil, errors.Wrap(err, "Cannot UnmarshalConnectionManagerConfig")
}
p.Config = config
case tunnelrpc.FailedConfig_config_Which_doh:
ss, err := s.Config().Doh()
if err != nil {
return nil, errors.Wrap(err, "Cannot get Doh from Config")
}
config, err := UnmarshalDoHProxyConfig(ss)
if err != nil {
return nil, errors.Wrap(err, "Cannot UnmarshalDoHProxyConfig")
}
p.Config = config
case tunnelrpc.FailedConfig_config_Which_reverseProxy:
ss, err := s.Config().ReverseProxy()
if err != nil {
return nil, errors.Wrap(err, "Cannot get ReverseProxy from Config")
}
config, err := UnmarshalReverseProxyConfig(ss)
if err != nil {
return nil, errors.Wrap(err, "Cannot UnmarshalReverseProxyConfig")
}
p.Config = config
default:
return nil, fmt.Errorf("Unknown type for FailedConfig: %v", s.Config().Which())
}
reason, err := s.Reason()
if err != nil {
return nil, errors.Wrap(err, "Cannot get Reason")
}
p.Reason = reason
return p, nil
}
+460
View File
@@ -0,0 +1,460 @@
package pogs
import (
"fmt"
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/cloudflare/cloudflared/tunnelrpc"
capnp "zombiezen.com/go/capnproto2"
)
// Assert *HTTPOriginConfig implements OriginConfig
var _ OriginConfig = (*HTTPOriginConfig)(nil)
// Assert *WebSocketOriginConfig implements OriginConfig
var _ OriginConfig = (*WebSocketOriginConfig)(nil)
// Assert *HelloWorldOriginConfig implements OriginConfig
var _ OriginConfig = (*HelloWorldOriginConfig)(nil)
func TestVersion(t *testing.T) {
firstVersion := InitVersion()
secondVersion := Version(1)
assert.False(t, firstVersion.IsNewerOrEqual(secondVersion))
assert.True(t, secondVersion.IsNewerOrEqual(firstVersion))
assert.True(t, secondVersion.IsNewerOrEqual(secondVersion))
}
func TestClientConfigCapnp(t *testing.T) {
for i, testCase := range ClientConfigTestCases() {
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
capnpEntity, err := tunnelrpc.NewClientConfig(seg)
if !assert.NoError(t, err) {
t.Fatal("Couldn't initialize a new message")
}
err = MarshalClientConfig(capnpEntity, testCase)
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
continue
}
result, err := UnmarshalClientConfig(capnpEntity)
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
continue
}
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
}
}
func ClientConfigTestCases() []*ClientConfig {
addDoHProxyConfigs := func(c *ClientConfig) {
c.DoHProxyConfigs = []*DoHProxyConfig{
sampleDoHProxyConfig(),
}
}
addReverseProxyConfigs := func(c *ClientConfig) {
c.ReverseProxyConfigs = []*ReverseProxyConfig{
sampleReverseProxyConfig(),
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
}),
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
c.OriginConfigJSONHandler = &OriginConfigJSONHandler{sampleHTTPOriginConfig()}
}),
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
c.OriginConfigJSONHandler = &OriginConfigJSONHandler{sampleHTTPOriginConfigUnixPath()}
}),
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
c.OriginConfigJSONHandler = &OriginConfigJSONHandler{sampleWebSocketOriginConfig()}
}),
}
}
testCases := []*ClientConfig{
sampleClientConfig(),
sampleClientConfig(addDoHProxyConfigs),
sampleClientConfig(addReverseProxyConfigs),
sampleClientConfig(addDoHProxyConfigs, addReverseProxyConfigs),
}
return testCases
}
func TestClientConfig(t *testing.T) {
for i, testCase := range ClientConfigTestCases() {
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
capnpEntity, err := tunnelrpc.NewClientConfig(seg)
if !assert.NoError(t, err) {
t.Fatal("Couldn't initialize a new message")
}
err = MarshalClientConfig(capnpEntity, testCase)
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
continue
}
result, err := UnmarshalClientConfig(capnpEntity)
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
continue
}
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
}
}
func TestUseConfigurationResult(t *testing.T) {
testCases := []*UseConfigurationResult{
&UseConfigurationResult{
Success: true,
},
&UseConfigurationResult{
Success: false,
FailedConfigs: []*FailedConfig{
{
Config: sampleReverseProxyConfig(),
Reason: "Invalid certificate",
},
{
Config: sampleDoHProxyConfig(),
Reason: "Cannot listen on port 53",
},
},
},
}
for i, testCase := range testCases {
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
capnpEntity, err := tunnelrpc.NewUseConfigurationResult(seg)
if !assert.NoError(t, err) {
t.Fatal("Couldn't initialize a new message")
}
err = MarshalUseConfigurationResult(capnpEntity, testCase)
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
continue
}
result, err := UnmarshalUseConfigurationResult(capnpEntity)
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
continue
}
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
}
}
func TestDoHProxyConfig(t *testing.T) {
testCases := []*DoHProxyConfig{
sampleDoHProxyConfig(),
sampleDoHProxyConfig(func(c *DoHProxyConfig) {
c.Upstreams = nil
}),
}
for i, testCase := range testCases {
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
capnpEntity, err := tunnelrpc.NewDoHProxyConfig(seg)
if !assert.NoError(t, err) {
t.Fatal("Couldn't initialize a new message")
}
err = MarshalDoHProxyConfig(capnpEntity, testCase)
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
continue
}
result, err := UnmarshalDoHProxyConfig(capnpEntity)
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
continue
}
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
}
}
func TestReverseProxyConfig(t *testing.T) {
testCases := []*ReverseProxyConfig{
sampleReverseProxyConfig(),
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
c.OriginConfigJSONHandler = &OriginConfigJSONHandler{sampleHTTPOriginConfig()}
}),
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
c.OriginConfigJSONHandler = &OriginConfigJSONHandler{sampleHTTPOriginConfigUnixPath()}
}),
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
c.OriginConfigJSONHandler = &OriginConfigJSONHandler{sampleWebSocketOriginConfig()}
}),
}
for i, testCase := range testCases {
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
capnpEntity, err := tunnelrpc.NewReverseProxyConfig(seg)
if !assert.NoError(t, err) {
t.Fatal("Couldn't initialize a new message")
}
err = MarshalReverseProxyConfig(capnpEntity, testCase)
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
continue
}
result, err := UnmarshalReverseProxyConfig(capnpEntity)
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
continue
}
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
}
}
func TestHTTPOriginConfig(t *testing.T) {
testCases := []*HTTPOriginConfig{
sampleHTTPOriginConfig(),
}
for i, testCase := range testCases {
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
capnpEntity, err := tunnelrpc.NewHTTPOriginConfig(seg)
if !assert.NoError(t, err) {
t.Fatal("Couldn't initialize a new message")
}
err = MarshalHTTPOriginConfig(capnpEntity, testCase)
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
continue
}
result, err := UnmarshalHTTPOriginConfig(capnpEntity)
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
continue
}
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
}
}
func TestWebSocketOriginConfig(t *testing.T) {
testCases := []*WebSocketOriginConfig{
sampleWebSocketOriginConfig(),
}
for i, testCase := range testCases {
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
capnpEntity, err := tunnelrpc.NewWebSocketOriginConfig(seg)
if !assert.NoError(t, err) {
t.Fatal("Couldn't initialize a new message")
}
err = MarshalWebSocketOriginConfig(capnpEntity, testCase)
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
continue
}
result, err := UnmarshalWebSocketOriginConfig(capnpEntity)
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
continue
}
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
}
}
func TestOriginConfigInvalidURL(t *testing.T) {
invalidConfigs := []OriginConfig{
&HTTPOriginConfig{
// this url doesn't have a scheme
URLString: "127.0.0.1:36192",
},
&WebSocketOriginConfig{
URLString: "127.0.0.1:36192",
},
}
for _, config := range invalidConfigs {
service, err := config.Service()
assert.Error(t, err)
assert.Nil(t, service)
}
}
//////////////////////////////////////////////////////////////////////////////
// Functions to generate sample data for ease of testing
//
// There's one "sample" function per struct type. Each goes like this:
// 1. Initialize an instance of the relevant struct.
// 2. Ensure the instance has no zero-valued fields. (This catches the
// error-case where a field was added, but we forgot to add code to
// marshal/unmarshal this field in CapnProto.)
// 3. Apply one or more "override" functions (which accept a
// pointer-to-struct, so they can mutate the instance).
// sampleClientConfig initializes a new ClientConfig literal,
// applies any number of overrides to it, and returns it.
func sampleClientConfig(overrides ...func(*ClientConfig)) *ClientConfig {
sample := &ClientConfig{
Version: Version(1337),
SupervisorConfig: sampleSupervisorConfig(),
EdgeConnectionConfig: sampleEdgeConnectionConfig(),
}
sample.ensureNoZeroFields()
for _, f := range overrides {
f(sample)
}
return sample
}
func sampleSupervisorConfig() *SupervisorConfig {
sample := &SupervisorConfig{
AutoUpdateFrequency: 21 * time.Hour,
MetricsUpdateFrequency: 11 * time.Minute,
GracePeriod: 31 * time.Second,
}
sample.ensureNoZeroFields()
return sample
}
func sampleEdgeConnectionConfig() *EdgeConnectionConfig {
sample := &EdgeConnectionConfig{
NumHAConnections: 49,
HeartbeatInterval: 5 * time.Second,
Timeout: 9 * time.Second,
MaxFailedHeartbeats: 9001,
UserCredentialPath: "/Users/example/.cloudflared/cert.pem",
}
sample.ensureNoZeroFields()
return sample
}
// sampleDoHProxyConfig initializes a new DoHProxyConfig struct,
// applies any number of overrides to it, and returns it.
func sampleDoHProxyConfig(overrides ...func(*DoHProxyConfig)) *DoHProxyConfig {
sample := &DoHProxyConfig{
ListenHost: "127.0.0.1",
ListenPort: 53,
Upstreams: []string{"1.1.1.1", "1.0.0.1"},
}
sample.ensureNoZeroFields()
for _, f := range overrides {
f(sample)
}
return sample
}
// sampleReverseProxyConfig initializes a new ReverseProxyConfig struct,
// applies any number of overrides to it, and returns it.
func sampleReverseProxyConfig(overrides ...func(*ReverseProxyConfig)) *ReverseProxyConfig {
sample := &ReverseProxyConfig{
TunnelHostname: "mock-non-lb-tunnel.example.com",
OriginConfigJSONHandler: &OriginConfigJSONHandler{&HelloWorldOriginConfig{}},
Retries: 18,
ConnectionTimeout: 5 * time.Second,
CompressionQuality: 3,
}
sample.ensureNoZeroFields()
for _, f := range overrides {
f(sample)
}
return sample
}
func sampleHTTPOriginConfig(overrides ...func(*HTTPOriginConfig)) *HTTPOriginConfig {
sample := &HTTPOriginConfig{
URLString: "https.example.com",
TCPKeepAlive: 7 * time.Second,
DialDualStack: true,
TLSHandshakeTimeout: 11 * time.Second,
TLSVerify: true,
OriginCAPool: "/etc/cert.pem",
OriginServerName: "secure.example.com",
MaxIdleConnections: 19,
IdleConnectionTimeout: 17 * time.Second,
ProxyConnectionTimeout: 15 * time.Second,
ExpectContinueTimeout: 21 * time.Second,
ChunkedEncoding: true,
}
sample.ensureNoZeroFields()
for _, f := range overrides {
f(sample)
}
return sample
}
func sampleHTTPOriginConfigUnixPath(overrides ...func(*HTTPOriginConfig)) *HTTPOriginConfig {
sample := &HTTPOriginConfig{
URLString: "unix:/var/lib/file.sock",
TCPKeepAlive: 7 * time.Second,
DialDualStack: true,
TLSHandshakeTimeout: 11 * time.Second,
TLSVerify: true,
OriginCAPool: "/etc/cert.pem",
OriginServerName: "secure.example.com",
MaxIdleConnections: 19,
IdleConnectionTimeout: 17 * time.Second,
ProxyConnectionTimeout: 15 * time.Second,
ExpectContinueTimeout: 21 * time.Second,
ChunkedEncoding: true,
}
sample.ensureNoZeroFields()
for _, f := range overrides {
f(sample)
}
return sample
}
func sampleWebSocketOriginConfig(overrides ...func(*WebSocketOriginConfig)) *WebSocketOriginConfig {
sample := &WebSocketOriginConfig{
URLString: "ssh://example.com",
TLSVerify: true,
OriginCAPool: "/etc/cert.pem",
OriginServerName: "secure.example.com",
}
sample.ensureNoZeroFields()
for _, f := range overrides {
f(sample)
}
return sample
}
func (c *ClientConfig) ensureNoZeroFields() {
ensureNoZeroFieldsInSample(reflect.ValueOf(c), []string{"DoHProxyConfigs", "ReverseProxyConfigs"})
}
func (c *SupervisorConfig) ensureNoZeroFields() {
ensureNoZeroFieldsInSample(reflect.ValueOf(c), []string{})
}
func (c *EdgeConnectionConfig) ensureNoZeroFields() {
ensureNoZeroFieldsInSample(reflect.ValueOf(c), []string{})
}
func (c *DoHProxyConfig) ensureNoZeroFields() {
ensureNoZeroFieldsInSample(reflect.ValueOf(c), []string{})
}
func (c *ReverseProxyConfig) ensureNoZeroFields() {
ensureNoZeroFieldsInSample(reflect.ValueOf(c), []string{})
}
func (c *HTTPOriginConfig) ensureNoZeroFields() {
ensureNoZeroFieldsInSample(reflect.ValueOf(c), []string{})
}
func (c *WebSocketOriginConfig) ensureNoZeroFields() {
ensureNoZeroFieldsInSample(reflect.ValueOf(c), []string{})
}
// ensureNoZeroFieldsInSample checks that all fields in the sample struct,
// except those listed in `allowedZeroFieldNames`, are initialized to nonzero
// values. Note that the value has to be a pointer for reflection to work
// correctly:
// e := &ExampleStruct{ ... }
// ensureNoZeroFieldsInSample(reflect.ValueOf(e), []string{})
//
// Context:
// Our tests work by taking a sample struct and marshalling/unmarshalling it.
// This makes them easy to write, but introduces some risk: if we don't
// include a field in the sample value, it won't be covered under tests.
// This check reduces that risk by requiring fields to be either initialized
// or explicitly uninitialized.
func ensureNoZeroFieldsInSample(ptrToSampleValue reflect.Value, allowedZeroFieldNames []string) {
sampleValue := ptrToSampleValue.Elem()
structType := ptrToSampleValue.Type().Elem()
allowedZeroFieldSet := make(map[string]bool)
for _, name := range allowedZeroFieldNames {
if _, ok := structType.FieldByName(name); !ok {
panic(fmt.Sprintf("struct %v has no field %v", structType.Name(), name))
}
allowedZeroFieldSet[name] = true
}
for i := 0; i < structType.NumField(); i++ {
if allowedZeroFieldSet[structType.Field(i).Name] {
continue
}
zeroValue := reflect.Zero(structType.Field(i).Type)
if reflect.DeepEqual(zeroValue.Interface(), sampleValue.Field(i).Interface()) {
panic(fmt.Sprintf("In the sample value for struct %v, field %v was not initialized", structType.Name(), structType.Field(i).Name))
}
}
}
+68
View File
@@ -0,0 +1,68 @@
package pogs
import (
"encoding/json"
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
)
// OriginConfigJSONHandler is a wrapper to serialize OriginConfig with type information, and deserialize JSON
// into an OriginConfig.
type OriginConfigJSONHandler struct {
OriginConfig OriginConfig
}
func (ocjh *OriginConfigJSONHandler) MarshalJSON() ([]byte, error) {
marshaler := make(map[string]OriginConfig, 1)
marshaler[ocjh.OriginConfig.jsonType()] = ocjh.OriginConfig
return json.Marshal(marshaler)
}
func (ocjh *OriginConfigJSONHandler) UnmarshalJSON(b []byte) error {
var originJSON map[string]interface{}
if err := json.Unmarshal(b, &originJSON); err != nil {
return errors.Wrapf(err, "cannot unmarshal %s into originJSON", string(b))
}
if originConfig, ok := originJSON[httpType.String()]; ok {
httpOriginConfig := &HTTPOriginConfig{}
if err := mapstructure.Decode(originConfig, httpOriginConfig); err != nil {
return errors.Wrapf(err, "cannot decode %+v into HTTPOriginConfig", originConfig)
}
ocjh.OriginConfig = httpOriginConfig
return nil
}
if originConfig, ok := originJSON[wsType.String()]; ok {
wsOriginConfig := &WebSocketOriginConfig{}
if err := mapstructure.Decode(originConfig, wsOriginConfig); err != nil {
return errors.Wrapf(err, "cannot decode %+v into WebSocketOriginConfig", originConfig)
}
ocjh.OriginConfig = wsOriginConfig
return nil
}
if originConfig, ok := originJSON[helloWorldType.String()]; ok {
helloWorldOriginConfig := &HelloWorldOriginConfig{}
if err := mapstructure.Decode(originConfig, helloWorldOriginConfig); err != nil {
return errors.Wrapf(err, "cannot decode %+v into HelloWorldOriginConfig", originConfig)
}
ocjh.OriginConfig = helloWorldOriginConfig
return nil
}
return fmt.Errorf("cannot unmarshal %s into OriginConfig", string(b))
}
// FallibleConfigMarshaler is a wrapper for FallibleConfig to implement custom marshal logic
type FallibleConfigMarshaler struct {
FallibleConfig FallibleConfig
}
func (fcm *FallibleConfigMarshaler) MarshalJSON() ([]byte, error) {
marshaler := make(map[string]FallibleConfig, 1)
marshaler[fcm.FallibleConfig.jsonType()] = fcm.FallibleConfig
return json.Marshal(marshaler)
}
+285
View File
@@ -0,0 +1,285 @@
package pogs
import (
"encoding/json"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestUnmarshalOrigin(t *testing.T) {
tests := []struct {
jsonLiteral string
exceptedOriginConfig OriginConfig
}{
{
jsonLiteral: `{
"Http":{
"url_string":"https.example.com",
"tcp_keep_alive":7000000000,
"dial_dual_stack":true,
"tls_handshake_timeout":11000000000,
"tls_verify":true,
"origin_ca_pool":"/etc/cert.pem",
"origin_server_name":"secure.example.com",
"max_idle_connections":19,
"idle_connection_timeout":17000000000,
"proxy_connection_timeout":15000000000,
"expect_continue_timeout":21000000000,
"chunked_encoding":true
}
}`,
exceptedOriginConfig: sampleHTTPOriginConfig(),
},
{
jsonLiteral: `{
"WebSocket":{
"url_string":"ssh://example.com",
"tls_verify":true,
"origin_ca_pool":"/etc/cert.pem",
"origin_server_name":"secure.example.com"
}
}`,
exceptedOriginConfig: sampleWebSocketOriginConfig(),
},
{
jsonLiteral: `{
"HelloWorld": {}
}`,
exceptedOriginConfig: &HelloWorldOriginConfig{},
},
}
for _, test := range tests {
originConfigJSON := prettyToValidJSON(test.jsonLiteral)
var OriginConfigJSONHandler OriginConfigJSONHandler
err := json.Unmarshal([]byte(originConfigJSON), &OriginConfigJSONHandler)
assert.NoError(t, err)
assert.Equal(t, test.exceptedOriginConfig, OriginConfigJSONHandler.OriginConfig)
}
}
func TestUnmarshalClientConfig(t *testing.T) {
prettyClientConfigJSON := `{
"version":10,
"supervisor_config":{
"auto_update_frequency":86400000000000,
"metrics_update_frequency":300000000000,
"grace_period":30000000000
},
"edge_connection_config":{
"num_ha_connections":4,
"heartbeat_interval":5000000000,
"timeout":30000000000,
"max_failed_heartbeats":5,
"user_credential_path":"~/.cloudflared/cert.pem"
},
"doh_proxy_configs":[{
"listen_host": "localhost",
"listen_port": 53,
"upstreams": ["https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"]
}],
"reverse_proxy_configs":[{
"tunnel_hostname":"sdfjadk33.cftunnel.com",
"origin_config":{
"Http":{
"url_string":"https://127.0.0.1:8080",
"tcp_keep_alive":30000000000,
"dial_dual_stack":true,
"tls_handshake_timeout":10000000000,
"tls_verify":true,
"origin_ca_pool":"",
"origin_server_name":"",
"max_idle_connections":100,
"idle_connection_timeout":90000000000,
"proxy_connection_timeout":90000000000,
"expect_continue_timeout":90000000000,
"chunked_encoding":true
}
},
"retries":5,
"connection_timeout":30,
"compression_quality":0
}]
}`
// replace new line and tab
clientConfigJSON := prettyToValidJSON(prettyClientConfigJSON)
var clientConfig ClientConfig
err := json.Unmarshal([]byte(clientConfigJSON), &clientConfig)
assert.NoError(t, err)
assert.Equal(t, Version(10), clientConfig.Version)
supervisorConfig := SupervisorConfig{
AutoUpdateFrequency: time.Hour * 24,
MetricsUpdateFrequency: time.Second * 300,
GracePeriod: time.Second * 30,
}
assert.Equal(t, supervisorConfig, *clientConfig.SupervisorConfig)
edgeConnectionConfig := EdgeConnectionConfig{
NumHAConnections: 4,
HeartbeatInterval: time.Second * 5,
Timeout: time.Second * 30,
MaxFailedHeartbeats: 5,
UserCredentialPath: "~/.cloudflared/cert.pem",
}
assert.Equal(t, edgeConnectionConfig, *clientConfig.EdgeConnectionConfig)
dohProxyConfig := DoHProxyConfig{
ListenHost: "localhost",
ListenPort: 53,
Upstreams: []string{"https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"},
}
assert.Len(t, clientConfig.DoHProxyConfigs, 1)
assert.Equal(t, dohProxyConfig, *clientConfig.DoHProxyConfigs[0])
reverseProxyConfig := ReverseProxyConfig{
TunnelHostname: "sdfjadk33.cftunnel.com",
OriginConfigJSONHandler: &OriginConfigJSONHandler{
OriginConfig: &HTTPOriginConfig{
URLString: "https://127.0.0.1:8080",
TCPKeepAlive: time.Second * 30,
DialDualStack: true,
TLSHandshakeTimeout: time.Second * 10,
TLSVerify: true,
OriginCAPool: "",
OriginServerName: "",
MaxIdleConnections: 100,
IdleConnectionTimeout: time.Second * 90,
ProxyConnectionTimeout: time.Second * 90,
ExpectContinueTimeout: time.Second * 90,
ChunkedEncoding: true,
},
},
Retries: 5,
ConnectionTimeout: 30,
CompressionQuality: 0,
}
assert.Len(t, clientConfig.ReverseProxyConfigs, 1)
assert.Equal(t, reverseProxyConfig, *clientConfig.ReverseProxyConfigs[0])
}
func TestMarshalFallibleConfig(t *testing.T) {
tests := []struct {
fallibleConfig FallibleConfig
expctedJSONLiteral string
}{
{
fallibleConfig: sampleSupervisorConfig(),
expctedJSONLiteral: `{
"supervisor_config":{
"auto_update_frequency":75600000000000,
"metrics_update_frequency":660000000000,
"grace_period":31000000000
}
}`,
},
{
fallibleConfig: sampleEdgeConnectionConfig(),
expctedJSONLiteral: `{
"edge_connection_config":{
"num_ha_connections":49,
"heartbeat_interval":5000000000,
"timeout":9000000000,
"max_failed_heartbeats":9001,
"user_credential_path":"/Users/example/.cloudflared/cert.pem"
}
}`,
},
{
fallibleConfig: sampleDoHProxyConfig(),
expctedJSONLiteral: `{
"doh_proxy_config":{
"listen_host":"127.0.0.1",
"listen_port":53,
"upstreams":["1.1.1.1","1.0.0.1"]
}
}`,
},
{
fallibleConfig: sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
c.OriginConfigJSONHandler = &OriginConfigJSONHandler{sampleHTTPOriginConfig()}
}),
expctedJSONLiteral: `{
"reverse_proxy_config":{
"tunnel_hostname":"mock-non-lb-tunnel.example.com",
"origin_config":{
"Http":{
"url_string":"https.example.com",
"tcp_keep_alive":7000000000,
"dial_dual_stack":true,
"tls_handshake_timeout":11000000000,
"tls_verify":true,
"origin_ca_pool":"/etc/cert.pem",
"origin_server_name":"secure.example.com",
"max_idle_connections":19,
"idle_connection_timeout":17000000000,
"proxy_connection_timeout":15000000000,
"expect_continue_timeout":21000000000,
"chunked_encoding":true
}
},
"retries":18,
"connection_timeout":5000000000,
"compression_quality":3
}
}`,
},
{
fallibleConfig: sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
c.OriginConfigJSONHandler = &OriginConfigJSONHandler{sampleWebSocketOriginConfig()}
}),
expctedJSONLiteral: `{
"reverse_proxy_config":{
"tunnel_hostname":"mock-non-lb-tunnel.example.com",
"origin_config":{
"WebSocket":{
"url_string":"ssh://example.com",
"tls_verify":true,
"origin_ca_pool":"/etc/cert.pem",
"origin_server_name":"secure.example.com"
}
},
"retries":18,
"connection_timeout":5000000000,
"compression_quality":3
}
}`,
},
{
fallibleConfig: sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
c.OriginConfigJSONHandler = &OriginConfigJSONHandler{&HelloWorldOriginConfig{}}
}),
expctedJSONLiteral: `{
"reverse_proxy_config":{
"tunnel_hostname":"mock-non-lb-tunnel.example.com",
"origin_config":{
"HelloWorld":{}
},
"retries":18,
"connection_timeout":5000000000,
"compression_quality":3
}
}`,
},
}
for _, test := range tests {
b, err := json.Marshal(test.fallibleConfig)
assert.NoError(t, err)
assert.Equal(t, prettyToValidJSON(test.expctedJSONLiteral), string(b))
}
}
type prettyJSON string
func prettyToValidJSON(prettyJSON string) string {
return strings.ReplaceAll(strings.ReplaceAll(prettyJSON, "\n", ""), "\t", "")
}
+89 -122
View File
@@ -8,7 +8,7 @@ import (
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"zombiezen.com/go/capnproto2"
capnp "zombiezen.com/go/capnproto2"
"zombiezen.com/go/capnproto2/pogs"
"zombiezen.com/go/capnproto2/rpc"
"zombiezen.com/go/capnproto2/server"
@@ -73,49 +73,10 @@ func UnmarshalRegistrationOptions(s tunnelrpc.RegistrationOptions) (*Registratio
return p, err
}
type ServerHello struct {
ConnectResult *ConnectResult
CloudflaredID uuid.UUID
}
// CapnpServerHello is ServerHello respresented in Cap'n Proto build-in types
type CapnpServerHello struct {
ConnectResult *ConnectResult
CloudflaredID []byte
}
func MarshalServerHello(s tunnelrpc.CapnpServerHello, p *ServerHello) error {
cloudflaredIDBytes, err := p.CloudflaredID.MarshalBinary()
if err != nil {
return err
}
capnpServerHello := &CapnpServerHello{
ConnectResult: p.ConnectResult,
CloudflaredID: cloudflaredIDBytes,
}
return pogs.Insert(tunnelrpc.CapnpServerHello_TypeID, s.Struct, capnpServerHello)
}
func UnmarshalServerHello(s tunnelrpc.CapnpServerHello) (*ServerHello, error) {
p := new(CapnpServerHello)
err := pogs.Extract(p, tunnelrpc.CapnpServerHello_TypeID, s.Struct)
if err != nil {
return nil, err
}
cloudflaredID, err := uuid.FromBytes(p.CloudflaredID)
if err != nil {
log.Errorf("fail to unmarshal %+v", p.CloudflaredID)
return nil, err
}
return &ServerHello{
ConnectResult: p.ConnectResult,
CloudflaredID: cloudflaredID,
}, nil
}
type ConnectResult struct {
Err *ConnectError
ServerInfo ServerInfo
Err *ConnectError
ServerInfo ServerInfo
ClientConfig ClientConfig
}
func MarshalConnectResult(s tunnelrpc.ConnectResult, p *ConnectResult) error {
@@ -129,8 +90,9 @@ func UnmarshalConnectResult(s tunnelrpc.ConnectResult) (*ConnectResult, error) {
}
type ConnectError struct {
Cause string
RetryAfter time.Duration
Cause string
RetryAfter time.Duration
ShouldRetry bool
}
func MarshalConnectError(s tunnelrpc.ConnectError, p *ConnectError) error {
@@ -166,62 +128,111 @@ func UnmarshalServerInfo(s tunnelrpc.ServerInfo) (*ServerInfo, error) {
return p, err
}
type HelloParameters struct {
OriginCert []byte
Tags []Tag
NumPreviousAttempts uint8
}
func MarshalHelloParameters(s tunnelrpc.HelloParameters, p *HelloParameters) error {
return pogs.Insert(tunnelrpc.HelloParameters_TypeID, s.Struct, p)
}
func UnmarshalHelloParameters(s tunnelrpc.HelloParameters) (*HelloParameters, error) {
p := new(HelloParameters)
err := pogs.Extract(p, tunnelrpc.HelloParameters_TypeID, s.Struct)
return p, err
}
type ConnectParameters struct {
OriginCert []byte
CloudflaredID uuid.UUID
NumPreviousAttempts uint8
}
// CapnpConnectParameters is ConnectParameters represented in Cap'n Proto build-in types
type CapnpConnectParameters struct {
OriginCert []byte
CloudflaredID []byte
NumPreviousAttempts uint8
Tags []Tag
CloudflaredVersion string
Name string
Group string
}
func MarshalConnectParameters(s tunnelrpc.CapnpConnectParameters, p *ConnectParameters) error {
if err := s.SetOriginCert(p.OriginCert); err != nil {
return err
}
cloudflaredIDBytes, err := p.CloudflaredID.MarshalBinary()
if err != nil {
return err
}
capnpConnectParameters := &CapnpConnectParameters{
OriginCert: p.OriginCert,
CloudflaredID: cloudflaredIDBytes,
NumPreviousAttempts: p.NumPreviousAttempts,
if err := s.SetCloudflaredID(cloudflaredIDBytes); err != nil {
return err
}
return pogs.Insert(tunnelrpc.CapnpConnectParameters_TypeID, s.Struct, capnpConnectParameters)
s.SetNumPreviousAttempts(p.NumPreviousAttempts)
if len(p.Tags) > 0 {
tagsCapnpList, err := s.NewTags(int32(len(p.Tags)))
if err != nil {
return err
}
for i, tag := range p.Tags {
tagCapnp := tagsCapnpList.At(i)
if err := tagCapnp.SetName(tag.Name); err != nil {
return err
}
if err := tagCapnp.SetValue(tag.Value); err != nil {
return err
}
}
}
if err := s.SetCloudflaredVersion(p.CloudflaredVersion); err != nil {
return err
}
if err := s.SetName(p.Name); err != nil {
return err
}
if err := s.SetGroup(p.Group); err != nil {
return err
}
return nil
}
func UnmarshalConnectParameters(s tunnelrpc.CapnpConnectParameters) (*ConnectParameters, error) {
p := new(CapnpConnectParameters)
err := pogs.Extract(p, tunnelrpc.CapnpConnectParameters_TypeID, s.Struct)
originCert, err := s.OriginCert()
if err != nil {
return nil, err
}
cloudflaredID, err := uuid.FromBytes(p.CloudflaredID)
cloudflaredIDBytes, err := s.CloudflaredID()
if err != nil {
return nil, err
}
cloudflaredID, err := uuid.FromBytes(cloudflaredIDBytes)
if err != nil {
return nil, err
}
tagsCapnpList, err := s.Tags()
if err != nil {
return nil, err
}
var tags []Tag
for i := 0; i < tagsCapnpList.Len(); i++ {
tagCapnp := tagsCapnpList.At(i)
name, err := tagCapnp.Name()
if err != nil {
return nil, err
}
value, err := tagCapnp.Value()
if err != nil {
return nil, err
}
tags = append(tags, Tag{Name: name, Value: value})
}
cloudflaredVersion, err := s.CloudflaredVersion()
if err != nil {
return nil, err
}
name, err := s.Name()
if err != nil {
return nil, err
}
group, err := s.Group()
if err != nil {
return nil, err
}
return &ConnectParameters{
OriginCert: p.OriginCert,
OriginCert: originCert,
CloudflaredID: cloudflaredID,
NumPreviousAttempts: p.NumPreviousAttempts,
NumPreviousAttempts: s.NumPreviousAttempts(),
Tags: tags,
CloudflaredVersion: cloudflaredVersion,
Name: name,
Group: group,
}, nil
}
@@ -229,7 +240,6 @@ type TunnelServer interface {
RegisterTunnel(ctx context.Context, originCert []byte, hostname string, options *RegistrationOptions) (*TunnelRegistration, error)
GetServerInfo(ctx context.Context) (*ServerInfo, error)
UnregisterTunnel(ctx context.Context, gracePeriodNanoSec int64) error
Hello(ctx context.Context, parameters *HelloParameters) (*ServerHello, error)
Connect(ctx context.Context, paramaters *ConnectParameters) (*ConnectResult, error)
}
@@ -290,27 +300,6 @@ func (i TunnelServer_PogsImpl) UnregisterTunnel(p tunnelrpc.TunnelServer_unregis
return i.impl.UnregisterTunnel(p.Ctx, gracePeriodNanoSec)
}
func (i TunnelServer_PogsImpl) Hello(p tunnelrpc.TunnelServer_hello) error {
parameters, err := p.Params.Parameters()
if err != nil {
return err
}
pogsParameters, err := UnmarshalHelloParameters(parameters)
if err != nil {
return err
}
server.Ack(p.Options)
serverHello, err := i.impl.Hello(p.Ctx, pogsParameters)
if err != nil {
return err
}
result, err := p.Results.NewResult()
if err != nil {
return err
}
return MarshalServerHello(result, serverHello)
}
func (i TunnelServer_PogsImpl) Connect(p tunnelrpc.TunnelServer_connect) error {
paramaters, err := p.Params.Parameters()
if err != nil {
@@ -391,28 +380,6 @@ func (c TunnelServer_PogsClient) UnregisterTunnel(ctx context.Context, gracePeri
return err
}
func (c TunnelServer_PogsClient) Hello(ctx context.Context,
parameters *HelloParameters,
) (*ServerHello, error) {
client := tunnelrpc.TunnelServer{Client: c.Client}
promise := client.Hello(ctx, func(p tunnelrpc.TunnelServer_hello_Params) error {
helloParameters, err := p.NewParameters()
if err != nil {
return err
}
err = MarshalHelloParameters(helloParameters, parameters)
if err != nil {
return err
}
return nil
})
retval, err := promise.Result().Struct()
if err != nil {
return nil, err
}
return UnmarshalServerHello(retval)
}
func (c TunnelServer_PogsClient) Connect(ctx context.Context,
parameters *ConnectParameters,
) (*ConnectResult, error) {
+104
View File
@@ -0,0 +1,104 @@
package pogs
import (
"reflect"
"testing"
"time"
"github.com/cloudflare/cloudflared/tunnelrpc"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
capnp "zombiezen.com/go/capnproto2"
)
func sampleTestConnectResult() *ConnectResult {
return &ConnectResult{
Err: &ConnectError{
Cause: "it broke",
ShouldRetry: false,
RetryAfter: 2 * time.Second,
},
ServerInfo: ServerInfo{LocationName: "computer"},
ClientConfig: *sampleClientConfig(),
}
}
func TestConnectResult(t *testing.T) {
testCases := []*ConnectResult{
sampleTestConnectResult(),
}
for i, testCase := range testCases {
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
capnpEntity, err := tunnelrpc.NewConnectResult(seg)
if !assert.NoError(t, err) {
t.Fatal("Couldn't initialize a new message")
}
err = MarshalConnectResult(capnpEntity, testCase)
if !assert.NoError(t, err, "testCase #%v failed to marshal", i) {
continue
}
result, err := UnmarshalConnectResult(capnpEntity)
if !assert.NoError(t, err, "testCase #%v failed to unmarshal", i) {
continue
}
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
}
}
func TestConnectParameters(t *testing.T) {
testCases := []*ConnectParameters{
sampleConnectParameters(),
sampleConnectParameters(func(c *ConnectParameters) {
c.Name = ""
}),
sampleConnectParameters(func(c *ConnectParameters) {
c.Tags = nil
}),
}
for i, testCase := range testCases {
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
capnpEntity, err := tunnelrpc.NewCapnpConnectParameters(seg)
if !assert.NoError(t, err) {
t.Fatal("Couldn't initialize a new message")
}
err = MarshalConnectParameters(capnpEntity, testCase)
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
continue
}
result, err := UnmarshalConnectParameters(capnpEntity)
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
continue
}
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
}
}
func sampleConnectParameters(overrides ...func(*ConnectParameters)) *ConnectParameters {
cloudflaredID, err := uuid.Parse("ED7BA470-8E54-465E-825C-99712043E01C")
if err != nil {
panic(err)
}
sample := &ConnectParameters{
OriginCert: []byte("my-origin-cert"),
CloudflaredID: cloudflaredID,
NumPreviousAttempts: 19,
Tags: []Tag{
Tag{
Name: "provision-method",
Value: "new",
},
},
CloudflaredVersion: "7.0",
Name: "My Computer",
Group: "www",
}
sample.ensureNoZeroFields()
for _, f := range overrides {
f(sample)
}
return sample
}
func (c *ConnectParameters) ensureNoZeroFields() {
ensureNoZeroFieldsInSample(reflect.ValueOf(c), []string{})
}
+195 -22
View File
@@ -46,41 +46,198 @@ struct RegistrationOptions {
uuid @11 :Text;
}
struct HelloParameters {
# certificate and token to prove ownership of a zone
originCert @0 :Data;
# user defined labels for this cloudflared
tags @1 :List(Tag);
# number of previous attempts to send Hello
numPreviousAttempts @2 :UInt8;
}
struct CapnpConnectParameters {
# certificate and token to prove ownership of a zone
originCert @0 :Data;
originCert @0 :Data;
# UUID assigned to this cloudflared obtained from Hello
cloudflaredID @1 :Data;
cloudflaredID @1 :Data;
# number of previous attempts to send Connect
numPreviousAttempts @2 :UInt8;
}
struct CapnpServerHello {
connectResult @0 :ConnectResult;
# UUID assigned to this cloudflared
cloudflaredID @1 :Data;
# user defined labels for this cloudflared
tags @3 :List(Tag);
# release version of cloudflared
cloudflaredVersion @4 :Text;
# friendly name for this cloudflared instance
name @5 :Text;
# group whose behavior this cloudflared instance will adopt
group @6 :Text;
}
struct ConnectResult {
err @0 :ConnectError;
# Information about the server this connection is established with
serverInfo @1 :ServerInfo;
# How this cloudflared instance should be configured
clientConfig @2 :ClientConfig;
}
struct ConnectError {
cause @0 :Text;
# How long should this connection wait to retry in ns
retryAfter @1 :Int64;
shoudRetry @2 :Bool;
shouldRetry @2 :Bool;
}
struct ClientConfig {
# Version of this configuration. This value is opaque, but is guaranteed
# to monotonically increase in value. Any configuration supplied to
# useConfiguration() with a smaller `version` should be ignored.
version @0 :UInt64;
# supervisorConfig is configuration for supervisor, the component that manages connection manager,
# autoupdater and metrics server
supervisorConfig @1 :SupervisorConfig;
# edgeConnectionConfig is configuration for connection manager, the componenet that manages connections with the edge
edgeConnectionConfig @2 :EdgeConnectionConfig;
# Configuration for cloudflared to run as a DNS-over-HTTPS proxy.
# cloudflared CLI option: `proxy-dns`
dohProxyConfigs @3 :List(DoHProxyConfig);
# Configuration for cloudflared to run as an HTTP reverse proxy.
reverseProxyConfigs @4 :List(ReverseProxyConfig);
}
struct SupervisorConfig {
# Frequency (in ns) to check Equinox for updates.
# Zero means auto-update is disabled.
# cloudflared CLI option: `autoupdate-freq`
autoUpdateFrequency @0 :Int64;
# Frequency (in ns) to update connection-based metrics.
# cloudflared CLI option: `metrics-update-freq`
metricsUpdateFrequency @1 :Int64;
# Time (in ns) to continue serving requests after cloudflared receives its
# first SIGINT/SIGTERM. A second SIGINT/SIGTERM will force cloudflared to
# shutdown immediately. For example, this field can be used to gracefully
# transition traffic to another cloudflared instance.
# cloudflared CLI option: `grace-period`
gracePeriod @2 :Int64;
}
struct EdgeConnectionConfig {
# cloudflared CLI option: `ha-connections`
numHAConnections @0 :UInt8;
# Interval (in ns) between heartbeats with the Cloudflare edge
# cloudflared CLI option: `heartbeat-interval`
heartbeatInterval @1 :Int64;
# Maximum wait time to connect with the edge.
timeout @2 :Int64;
# Number of unacked heartbeats for cloudflared to send before
# closing the connection to the edge.
# cloudflared CLI option: `heartbeat-count`
maxFailedHeartbeats @3 :UInt64;
# Absolute path of the file containing certificate and token to connect with the edge
userCredentialPath @4 :Text;
}
struct ReverseProxyConfig {
tunnelHostname @0 :Text;
origin :union {
http @1 :HTTPOriginConfig;
websocket @2 :WebSocketOriginConfig;
helloWorld @3 :HelloWorldOriginConfig;
}
# Maximum number of retries for connection/protocol errors.
# cloudflared CLI option: `retries`
retries @4 :UInt64;
# maximum time (in ns) for cloudflared to wait to establish a connection
# to the origin. Zero means no timeout.
# cloudflared CLI option: `proxy-connect-timeout`
connectionTimeout @5 :Int64;
# (beta) Use cross-stream compression instead of HTTP compression.
# 0=off, 1=low, 2=medium, 3=high.
# For more context see the mapping here: https://github.com/cloudflare/cloudflared/blob/2019.3.2/h2mux/h2_dictionaries.go#L62
# cloudflared CLI option: `compression-quality`
compressionQuality @6 :UInt64;
}
struct WebSocketOriginConfig {
# URI of the origin service.
# cloudflared will start a websocket server that forwards data to this URI
# cloudflared CLI option: `url`
# cloudflared logic: https://github.com/cloudflare/cloudflared/blob/2019.3.2/cmd/cloudflared/tunnel/cmd.go#L304
urlString @0 :Text;
# Whether cloudflared should verify TLS connections to the origin.
# negation of cloudflared CLI option: `no-tls-verify`
tlsVerify @1 :Bool;
# originCAPool specifies the root CA that cloudflared should use when
# verifying TLS connections to the origin.
# - if tlsVerify is false, originCAPool will be ignored.
# - if tlsVerify is true and originCAPool is empty, the system CA pool
# will be loaded if possible.
# - if tlsVerify is true and originCAPool is non-empty, cloudflared will
# treat it as the filepath to the root CA.
# cloudflared CLI option: `origin-ca-pool`
originCAPool @2 :Text;
# Hostname to use when verifying TLS connections to the origin.
# cloudflared CLI option: `origin-server-name`
originServerName @3 :Text;
}
struct HTTPOriginConfig {
# HTTP(S) URL of the origin service.
# cloudflared CLI option: `url`
urlString @0 :Text;
# the TCP keep-alive period (in ns) for an active network connection.
# Zero means keep-alives are not enabled.
# cloudflared CLI option: `proxy-tcp-keepalive`
tcpKeepAlive @1 :Int64;
# whether cloudflared should use a "happy eyeballs"-compliant procedure
# to connect to origins that resolve to both IPv4 and IPv6 addresses
# negation of cloudflared CLI option: `proxy-no-happy-eyeballs`
dialDualStack @2 :Bool;
# maximum time (in ns) for cloudflared to wait for a TLS handshake
# with the origin. Zero means no timeout.
# cloudflared CLI option: `proxy-tls-timeout`
tlsHandshakeTimeout @3 :Int64;
# Whether cloudflared should verify TLS connections to the origin.
# negation of cloudflared CLI option: `no-tls-verify`
tlsVerify @4 :Bool;
# originCAPool specifies the root CA that cloudflared should use when
# verifying TLS connections to the origin.
# - if tlsVerify is false, originCAPool will be ignored.
# - if tlsVerify is true and originCAPool is empty, the system CA pool
# will be loaded if possible.
# - if tlsVerify is true and originCAPool is non-empty, cloudflared will
# treat it as the filepath to the root CA.
# cloudflared CLI option: `origin-ca-pool`
originCAPool @5 :Text;
# Hostname to use when verifying TLS connections to the origin.
# cloudflared CLI option: `origin-server-name`
originServerName @6 :Text;
# maximum number of idle (keep-alive) connections for cloudflared to
# keep open with the origin. Zero means no limit.
# cloudflared CLI option: `proxy-keepalive-connections`
maxIdleConnections @7 :UInt64;
# maximum time (in ns) for an idle (keep-alive) connection to remain
# idle before closing itself. Zero means no timeout.
# cloudflared CLI option: `proxy-keepalive-timeout`
idleConnectionTimeout @8 :Int64;
# maximum amount of time a dial will wait for a connect to complete.
proxyConnectionTimeout @9 :Int64;
# The amount of time to wait for origin's first response headers after fully
# writing the request headers if the request has an "Expect: 100-continue" header.
# Zero means no timeout and causes the body to be sent immediately, without
# waiting for the server to approve.
expectContinueTimeout @10 :Int64;
# Whether cloudflared should allow chunked transfer encoding to the
# origin. (This should be disabled for WSGI origins, for example.)
# negation of cloudflared CLI option: `no-chunked-encoding`
chunkedEncoding @11 :Bool;
}
# configuration for cloudflared to provide a DNS over HTTPS proxy server
struct DoHProxyConfig {
# The hostname for the DoH proxy server to listen on.
# cloudflared CLI option: `proxy-dns-address`
listenHost @0 :Text;
# The port for the DoH proxy server to listen on.
# cloudflared CLI option: `proxy-dns-port`
listenPort @1 :UInt16;
# Upstream endpoint URLs for the DoH proxy server.
# cloudflared CLI option: `proxy-dns-upstream`
upstreams @2 :List(Text);
}
struct HelloWorldOriginConfig {
# nothing to configure
}
struct Tag {
@@ -98,12 +255,28 @@ struct ServerInfo {
locationName @0 :Text;
}
struct UseConfigurationResult {
success @0 :Bool;
failedConfigs @1 :List(FailedConfig);
}
struct FailedConfig {
config :union {
supervisor @0 :SupervisorConfig;
edgeConnection @1 :EdgeConnectionConfig;
doh @2 :DoHProxyConfig;
reverseProxy @3 :ReverseProxyConfig;
}
reason @4 :Text;
}
interface TunnelServer {
registerTunnel @0 (originCert :Data, hostname :Text, options :RegistrationOptions) -> (result :TunnelRegistration);
getServerInfo @1 () -> (result :ServerInfo);
unregisterTunnel @2 (gracePeriodNanoSec :Int64) -> ();
# originCert is used to verify ownership of a zone and hostnames it can serve
# tags are client-defined tags to associate with the cloudflared
hello @3(parameters :HelloParameters) -> (result: CapnpServerHello);
connect @4(parameters :CapnpConnectParameters) -> (result :ConnectResult);
connect @3 (parameters :CapnpConnectParameters) -> (result :ConnectResult);
}
interface ClientService {
useConfiguration @0 (clientServiceConfig :ClientConfig) -> (result :UseConfigurationResult);
}
+2077 -529
View File
File diff suppressed because it is too large Load Diff
+12 -106
View File
@@ -180,33 +180,6 @@ func TestValidateHTTPService_HTTP2HTTP(t *testing.T) {
}
panic("Shouldn't reach here")
})))
// Integration-style test with a mock server
server, client, err := createMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, hostname, r.Host)
w.WriteHeader(200)
}))
assert.NoError(t, err)
defer server.Close()
assert.Nil(t, ValidateHTTPService(originURL, hostname, client.Transport))
// this will fail if the client follows the 302
redirectServer, redirectClient, err := createMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/followedRedirect" {
t.Fatal("shouldn't have followed the 302")
}
if r.Method == "CONNECT" {
assert.Equal(t, "127.0.0.1:443", r.Host)
} else {
assert.Equal(t, hostname, r.Host)
}
w.Header().Set("Location", "/followedRedirect")
w.WriteHeader(302)
}))
assert.NoError(t, err)
defer redirectServer.Close()
assert.Nil(t, ValidateHTTPService(originURL, hostname, redirectClient.Transport))
}
// Happy path 2: originURL is HTTPS, and HTTPS connections work
@@ -235,36 +208,6 @@ func TestValidateHTTPService_HTTPS2HTTPS(t *testing.T) {
}
panic("Shouldn't reach here")
})))
// Integration-style test with a mock server
server, client, err := createSecureMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "CONNECT" {
assert.Equal(t, "127.0.0.1:443", r.Host)
} else {
assert.Equal(t, hostname, r.Host)
}
w.WriteHeader(200)
}))
assert.NoError(t, err)
defer server.Close()
assert.Nil(t, ValidateHTTPService(originURL, hostname, client.Transport))
// this will fail if the client follows the 302
redirectServer, redirectClient, err := createSecureMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/followedRedirect" {
t.Fatal("shouldn't have followed the 302")
}
if r.Method == "CONNECT" {
assert.Equal(t, "127.0.0.1:443", r.Host)
} else {
assert.Equal(t, hostname, r.Host)
}
w.Header().Set("Location", "/followedRedirect")
w.WriteHeader(302)
}))
assert.NoError(t, err)
defer redirectServer.Close()
assert.Nil(t, ValidateHTTPService(originURL, hostname, redirectClient.Transport))
}
// Error path 1: originURL is HTTPS, but HTTP connections work
@@ -293,37 +236,6 @@ func TestValidateHTTPService_HTTPS2HTTP(t *testing.T) {
}
panic("Shouldn't reach here")
})))
// Integration-style test with a mock server
server, client, err := createMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "CONNECT" {
assert.Equal(t, "127.0.0.1:1234", r.Host)
} else {
assert.Equal(t, hostname, r.Host)
}
w.WriteHeader(200)
}))
assert.NoError(t, err)
defer server.Close()
assert.Error(t, ValidateHTTPService(originURL, hostname, client.Transport))
// this will fail if the client follows the 302
redirectServer, redirectClient, err := createMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/followedRedirect" {
t.Fatal("shouldn't have followed the 302")
}
if r.Method == "CONNECT" {
assert.Equal(t, "127.0.0.1:1234", r.Host)
} else {
assert.Equal(t, hostname, r.Host)
}
w.Header().Set("Location", "/followedRedirect")
w.WriteHeader(302)
}))
assert.NoError(t, err)
defer redirectServer.Close()
assert.Error(t, ValidateHTTPService(originURL, hostname, redirectClient.Transport))
}
// Error path 2: originURL is HTTP, but HTTPS connections work
@@ -352,21 +264,11 @@ func TestValidateHTTPService_HTTP2HTTPS(t *testing.T) {
}
panic("Shouldn't reach here")
})))
}
// Integration-style test with a mock server
server, client, err := createSecureMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "CONNECT" {
assert.Equal(t, "127.0.0.1:1234", r.Host)
} else {
assert.Equal(t, hostname, r.Host)
}
w.WriteHeader(200)
}))
assert.NoError(t, err)
defer server.Close()
assert.Error(t, ValidateHTTPService(originURL, hostname, client.Transport))
// this will fail if the client follows the 302
// Ensure the client does not follow 302 responses
func TestValidateHTTPService_NoFollowRedirects(t *testing.T) {
hostname := "example.com"
redirectServer, redirectClient, err := createSecureMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/followedRedirect" {
t.Fatal("shouldn't have followed the 302")
@@ -381,12 +283,12 @@ func TestValidateHTTPService_HTTP2HTTPS(t *testing.T) {
}))
assert.NoError(t, err)
defer redirectServer.Close()
assert.Error(t, ValidateHTTPService(originURL, hostname, redirectClient.Transport))
assert.NoError(t, ValidateHTTPService(redirectServer.URL, hostname, redirectClient.Transport))
}
// error path 3: origin URL is nonresponsive
// Ensure validation times out when origin URL is nonresponsive
func TestValidateHTTPService_NonResponsiveOrigin(t *testing.T) {
originURL := "https://127.0.0.1/"
originURL := "http://127.0.0.1/"
hostname := "example.com"
oldValidationTimeout := validationTimeout
defer func() {
@@ -394,7 +296,10 @@ func TestValidateHTTPService_NonResponsiveOrigin(t *testing.T) {
}()
validationTimeout = 500 * time.Millisecond
server, client, err := createSecureMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Use createMockServerAndClient, not createSecureMockServerAndClient.
// The latter will bail with HTTP 400 immediately on an http:// request,
// which defeats the purpose of a 'nonresponsive origin' test.
server, client, err := createMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "CONNECT" {
assert.Equal(t, "127.0.0.1:443", r.Host)
} else {
@@ -409,6 +314,7 @@ func TestValidateHTTPService_NonResponsiveOrigin(t *testing.T) {
defer server.Close()
err = ValidateHTTPService(originURL, hostname, client.Transport)
fmt.Println(err)
if err, ok := err.(net.Error); assert.True(t, ok) {
assert.True(t, err.Timeout())
}
+17 -10
View File
@@ -1,14 +1,21 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
The MIT License (MIT)
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Copyright (c) 2013 TOML authors
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+17 -10
View File
@@ -1,14 +1,21 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
The MIT License (MIT)
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Copyright (c) 2013 TOML authors
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+17 -10
View File
@@ -1,14 +1,21 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
The MIT License (MIT)
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Copyright (c) 2013 TOML authors
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+17 -10
View File
@@ -1,14 +1,21 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
The MIT License (MIT)
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Copyright (c) 2013 TOML authors
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+1 -1
View File
@@ -775,7 +775,7 @@ func lexDatetime(lx *lexer) stateFn {
return lexDatetime
}
switch r {
case '-', 'T', ':', '.', 'Z':
case '-', 'T', ':', '.', 'Z', '+':
return lexDatetime
}
+1
View File
@@ -0,0 +1 @@
shlex.test
+20
View File
@@ -0,0 +1,20 @@
Copyright (c) anmitsu <anmitsu.s@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+38
View File
@@ -0,0 +1,38 @@
# go-shlex
go-shlex is a library to make a lexical analyzer like Unix shell for
Go.
## Install
go get -u "github.com/anmitsu/go-shlex"
## Usage
```go
package main
import (
"fmt"
"log"
"github.com/anmitsu/go-shlex"
)
func main() {
cmd := `cp -Rdp "file name" 'file name2' dir\ name`
words, err := shlex.Split(cmd, true)
if err != nil {
log.Fatal(err)
}
for _, w := range words {
fmt.Println(w)
}
}
```
## Documentation
http://godoc.org/github.com/anmitsu/go-shlex
+193
View File
@@ -0,0 +1,193 @@
// Package shlex provides a simple lexical analysis like Unix shell.
package shlex
import (
"bufio"
"errors"
"io"
"strings"
"unicode"
)
var (
ErrNoClosing = errors.New("No closing quotation")
ErrNoEscaped = errors.New("No escaped character")
)
// Tokenizer is the interface that classifies a token according to
// words, whitespaces, quotations, escapes and escaped quotations.
type Tokenizer interface {
IsWord(rune) bool
IsWhitespace(rune) bool
IsQuote(rune) bool
IsEscape(rune) bool
IsEscapedQuote(rune) bool
}
// DefaultTokenizer implements a simple tokenizer like Unix shell.
type DefaultTokenizer struct{}
func (t *DefaultTokenizer) IsWord(r rune) bool {
return r == '_' || unicode.IsLetter(r) || unicode.IsNumber(r)
}
func (t *DefaultTokenizer) IsQuote(r rune) bool {
switch r {
case '\'', '"':
return true
default:
return false
}
}
func (t *DefaultTokenizer) IsWhitespace(r rune) bool {
return unicode.IsSpace(r)
}
func (t *DefaultTokenizer) IsEscape(r rune) bool {
return r == '\\'
}
func (t *DefaultTokenizer) IsEscapedQuote(r rune) bool {
return r == '"'
}
// Lexer represents a lexical analyzer.
type Lexer struct {
reader *bufio.Reader
tokenizer Tokenizer
posix bool
whitespacesplit bool
}
// NewLexer creates a new Lexer reading from io.Reader. This Lexer
// has a DefaultTokenizer according to posix and whitespacesplit
// rules.
func NewLexer(r io.Reader, posix, whitespacesplit bool) *Lexer {
return &Lexer{
reader: bufio.NewReader(r),
tokenizer: &DefaultTokenizer{},
posix: posix,
whitespacesplit: whitespacesplit,
}
}
// NewLexerString creates a new Lexer reading from a string. This
// Lexer has a DefaultTokenizer according to posix and whitespacesplit
// rules.
func NewLexerString(s string, posix, whitespacesplit bool) *Lexer {
return NewLexer(strings.NewReader(s), posix, whitespacesplit)
}
// Split splits a string according to posix or non-posix rules.
func Split(s string, posix bool) ([]string, error) {
return NewLexerString(s, posix, true).Split()
}
// SetTokenizer sets a Tokenizer.
func (l *Lexer) SetTokenizer(t Tokenizer) {
l.tokenizer = t
}
func (l *Lexer) Split() ([]string, error) {
result := make([]string, 0)
for {
token, err := l.readToken()
if token != "" {
result = append(result, token)
}
if err == io.EOF {
break
} else if err != nil {
return result, err
}
}
return result, nil
}
func (l *Lexer) readToken() (string, error) {
t := l.tokenizer
token := ""
quoted := false
state := ' '
escapedstate := ' '
scanning:
for {
next, _, err := l.reader.ReadRune()
if err != nil {
if t.IsQuote(state) {
return token, ErrNoClosing
} else if t.IsEscape(state) {
return token, ErrNoEscaped
}
return token, err
}
switch {
case t.IsWhitespace(state):
switch {
case t.IsWhitespace(next):
break scanning
case l.posix && t.IsEscape(next):
escapedstate = 'a'
state = next
case t.IsWord(next):
token += string(next)
state = 'a'
case t.IsQuote(next):
if !l.posix {
token += string(next)
}
state = next
default:
token = string(next)
if l.whitespacesplit {
state = 'a'
} else if token != "" || (l.posix && quoted) {
break scanning
}
}
case t.IsQuote(state):
quoted = true
switch {
case next == state:
if !l.posix {
token += string(next)
break scanning
} else {
state = 'a'
}
case l.posix && t.IsEscape(next) && t.IsEscapedQuote(state):
escapedstate = state
state = next
default:
token += string(next)
}
case t.IsEscape(state):
if t.IsQuote(escapedstate) && next != state && next != escapedstate {
token += string(state)
}
token += string(next)
state = escapedstate
case t.IsWord(state):
switch {
case t.IsWhitespace(next):
if token != "" || (l.posix && quoted) {
break scanning
}
case l.posix && t.IsQuote(next):
state = next
case l.posix && t.IsEscape(next):
escapedstate = 'a'
state = next
case t.IsWord(next) || t.IsQuote(next):
token += string(next)
default:
if l.whitespacesplit {
token += string(next)
} else if token != "" {
l.reader.UnreadRune()
break scanning
}
}
}
}
return token, nil
}
@@ -1,4 +1,5 @@
Apache License
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
@@ -178,7 +179,7 @@ Apache License
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
@@ -186,7 +187,7 @@ Apache License
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -199,4 +200,3 @@ Apache License
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+3
View File
@@ -0,0 +1,3 @@
AWS SDK for Go
Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Copyright 2014-2015 Stripe, Inc.
+164
View File
@@ -0,0 +1,164 @@
// Package awserr represents API error interface accessors for the SDK.
package awserr
// An Error wraps lower level errors with code, message and an original error.
// The underlying concrete error type may also satisfy other interfaces which
// can be to used to obtain more specific information about the error.
//
// Calling Error() or String() will always include the full information about
// an error based on its underlying type.
//
// Example:
//
// output, err := s3manage.Upload(svc, input, opts)
// if err != nil {
// if awsErr, ok := err.(awserr.Error); ok {
// // Get error details
// log.Println("Error:", awsErr.Code(), awsErr.Message())
//
// // Prints out full error message, including original error if there was one.
// log.Println("Error:", awsErr.Error())
//
// // Get original error
// if origErr := awsErr.OrigErr(); origErr != nil {
// // operate on original error.
// }
// } else {
// fmt.Println(err.Error())
// }
// }
//
type Error interface {
// Satisfy the generic error interface.
error
// Returns the short phrase depicting the classification of the error.
Code() string
// Returns the error details message.
Message() string
// Returns the original error if one was set. Nil is returned if not set.
OrigErr() error
}
// BatchError is a batch of errors which also wraps lower level errors with
// code, message, and original errors. Calling Error() will include all errors
// that occurred in the batch.
//
// Deprecated: Replaced with BatchedErrors. Only defined for backwards
// compatibility.
type BatchError interface {
// Satisfy the generic error interface.
error
// Returns the short phrase depicting the classification of the error.
Code() string
// Returns the error details message.
Message() string
// Returns the original error if one was set. Nil is returned if not set.
OrigErrs() []error
}
// BatchedErrors is a batch of errors which also wraps lower level errors with
// code, message, and original errors. Calling Error() will include all errors
// that occurred in the batch.
//
// Replaces BatchError
type BatchedErrors interface {
// Satisfy the base Error interface.
Error
// Returns the original error if one was set. Nil is returned if not set.
OrigErrs() []error
}
// New returns an Error object described by the code, message, and origErr.
//
// If origErr satisfies the Error interface it will not be wrapped within a new
// Error object and will instead be returned.
func New(code, message string, origErr error) Error {
var errs []error
if origErr != nil {
errs = append(errs, origErr)
}
return newBaseError(code, message, errs)
}
// NewBatchError returns an BatchedErrors with a collection of errors as an
// array of errors.
func NewBatchError(code, message string, errs []error) BatchedErrors {
return newBaseError(code, message, errs)
}
// A RequestFailure is an interface to extract request failure information from
// an Error such as the request ID of the failed request returned by a service.
// RequestFailures may not always have a requestID value if the request failed
// prior to reaching the service such as a connection error.
//
// Example:
//
// output, err := s3manage.Upload(svc, input, opts)
// if err != nil {
// if reqerr, ok := err.(RequestFailure); ok {
// log.Println("Request failed", reqerr.Code(), reqerr.Message(), reqerr.RequestID())
// } else {
// log.Println("Error:", err.Error())
// }
// }
//
// Combined with awserr.Error:
//
// output, err := s3manage.Upload(svc, input, opts)
// if err != nil {
// if awsErr, ok := err.(awserr.Error); ok {
// // Generic AWS Error with Code, Message, and original error (if any)
// fmt.Println(awsErr.Code(), awsErr.Message(), awsErr.OrigErr())
//
// if reqErr, ok := err.(awserr.RequestFailure); ok {
// // A service error occurred
// fmt.Println(reqErr.StatusCode(), reqErr.RequestID())
// }
// } else {
// fmt.Println(err.Error())
// }
// }
//
type RequestFailure interface {
Error
// The status code of the HTTP response.
StatusCode() int
// The request ID returned by the service for a request failure. This will
// be empty if no request ID is available such as the request failed due
// to a connection error.
RequestID() string
}
// NewRequestFailure returns a wrapped error with additional information for
// request status code, and service requestID.
//
// Should be used to wrap all request which involve service requests. Even if
// the request failed without a service response, but had an HTTP status code
// that may be meaningful.
func NewRequestFailure(err Error, statusCode int, reqID string) RequestFailure {
return newRequestError(err, statusCode, reqID)
}
// UnmarshalError provides the interface for the SDK failing to unmarshal data.
type UnmarshalError interface {
awsError
Bytes() []byte
}
// NewUnmarshalError returns an initialized UnmarshalError error wrapper adding
// the bytes that fail to unmarshal to the error.
func NewUnmarshalError(err error, msg string, bytes []byte) UnmarshalError {
return &unmarshalError{
awsError: New("UnmarshalError", msg, err),
bytes: bytes,
}
}
+221
View File
@@ -0,0 +1,221 @@
package awserr
import (
"encoding/hex"
"fmt"
)
// SprintError returns a string of the formatted error code.
//
// Both extra and origErr are optional. If they are included their lines
// will be added, but if they are not included their lines will be ignored.
func SprintError(code, message, extra string, origErr error) string {
msg := fmt.Sprintf("%s: %s", code, message)
if extra != "" {
msg = fmt.Sprintf("%s\n\t%s", msg, extra)
}
if origErr != nil {
msg = fmt.Sprintf("%s\ncaused by: %s", msg, origErr.Error())
}
return msg
}
// A baseError wraps the code and message which defines an error. It also
// can be used to wrap an original error object.
//
// Should be used as the root for errors satisfying the awserr.Error. Also
// for any error which does not fit into a specific error wrapper type.
type baseError struct {
// Classification of error
code string
// Detailed information about error
message string
// Optional original error this error is based off of. Allows building
// chained errors.
errs []error
}
// newBaseError returns an error object for the code, message, and errors.
//
// code is a short no whitespace phrase depicting the classification of
// the error that is being created.
//
// message is the free flow string containing detailed information about the
// error.
//
// origErrs is the error objects which will be nested under the new errors to
// be returned.
func newBaseError(code, message string, origErrs []error) *baseError {
b := &baseError{
code: code,
message: message,
errs: origErrs,
}
return b
}
// Error returns the string representation of the error.
//
// See ErrorWithExtra for formatting.
//
// Satisfies the error interface.
func (b baseError) Error() string {
size := len(b.errs)
if size > 0 {
return SprintError(b.code, b.message, "", errorList(b.errs))
}
return SprintError(b.code, b.message, "", nil)
}
// String returns the string representation of the error.
// Alias for Error to satisfy the stringer interface.
func (b baseError) String() string {
return b.Error()
}
// Code returns the short phrase depicting the classification of the error.
func (b baseError) Code() string {
return b.code
}
// Message returns the error details message.
func (b baseError) Message() string {
return b.message
}
// OrigErr returns the original error if one was set. Nil is returned if no
// error was set. This only returns the first element in the list. If the full
// list is needed, use BatchedErrors.
func (b baseError) OrigErr() error {
switch len(b.errs) {
case 0:
return nil
case 1:
return b.errs[0]
default:
if err, ok := b.errs[0].(Error); ok {
return NewBatchError(err.Code(), err.Message(), b.errs[1:])
}
return NewBatchError("BatchedErrors",
"multiple errors occurred", b.errs)
}
}
// OrigErrs returns the original errors if one was set. An empty slice is
// returned if no error was set.
func (b baseError) OrigErrs() []error {
return b.errs
}
// So that the Error interface type can be included as an anonymous field
// in the requestError struct and not conflict with the error.Error() method.
type awsError Error
// A requestError wraps a request or service error.
//
// Composed of baseError for code, message, and original error.
type requestError struct {
awsError
statusCode int
requestID string
bytes []byte
}
// newRequestError returns a wrapped error with additional information for
// request status code, and service requestID.
//
// Should be used to wrap all request which involve service requests. Even if
// the request failed without a service response, but had an HTTP status code
// that may be meaningful.
//
// Also wraps original errors via the baseError.
func newRequestError(err Error, statusCode int, requestID string) *requestError {
return &requestError{
awsError: err,
statusCode: statusCode,
requestID: requestID,
}
}
// Error returns the string representation of the error.
// Satisfies the error interface.
func (r requestError) Error() string {
extra := fmt.Sprintf("status code: %d, request id: %s",
r.statusCode, r.requestID)
return SprintError(r.Code(), r.Message(), extra, r.OrigErr())
}
// String returns the string representation of the error.
// Alias for Error to satisfy the stringer interface.
func (r requestError) String() string {
return r.Error()
}
// StatusCode returns the wrapped status code for the error
func (r requestError) StatusCode() int {
return r.statusCode
}
// RequestID returns the wrapped requestID
func (r requestError) RequestID() string {
return r.requestID
}
// OrigErrs returns the original errors if one was set. An empty slice is
// returned if no error was set.
func (r requestError) OrigErrs() []error {
if b, ok := r.awsError.(BatchedErrors); ok {
return b.OrigErrs()
}
return []error{r.OrigErr()}
}
type unmarshalError struct {
awsError
bytes []byte
}
// Error returns the string representation of the error.
// Satisfies the error interface.
func (e unmarshalError) Error() string {
extra := hex.Dump(e.bytes)
return SprintError(e.Code(), e.Message(), extra, e.OrigErr())
}
// String returns the string representation of the error.
// Alias for Error to satisfy the stringer interface.
func (e unmarshalError) String() string {
return e.Error()
}
// Bytes returns the bytes that failed to unmarshal.
func (e unmarshalError) Bytes() []byte {
return e.bytes
}
// An error list that satisfies the golang interface
type errorList []error
// Error returns the string representation of the error.
//
// Satisfies the error interface.
func (e errorList) Error() string {
msg := ""
// How do we want to handle the array size being zero
if size := len(e); size > 0 {
for i := 0; i < size; i++ {
msg += e[i].Error()
// We check the next index to see if it is within the slice.
// If it is, then we append a newline. We do this, because unit tests
// could be broken with the additional '\n'
if i+1 < size {
msg += "\n"
}
}
}
return msg
}
+108
View File
@@ -0,0 +1,108 @@
package awsutil
import (
"io"
"reflect"
"time"
)
// Copy deeply copies a src structure to dst. Useful for copying request and
// response structures.
//
// Can copy between structs of different type, but will only copy fields which
// are assignable, and exist in both structs. Fields which are not assignable,
// or do not exist in both structs are ignored.
func Copy(dst, src interface{}) {
dstval := reflect.ValueOf(dst)
if !dstval.IsValid() {
panic("Copy dst cannot be nil")
}
rcopy(dstval, reflect.ValueOf(src), true)
}
// CopyOf returns a copy of src while also allocating the memory for dst.
// src must be a pointer type or this operation will fail.
func CopyOf(src interface{}) (dst interface{}) {
dsti := reflect.New(reflect.TypeOf(src).Elem())
dst = dsti.Interface()
rcopy(dsti, reflect.ValueOf(src), true)
return
}
// rcopy performs a recursive copy of values from the source to destination.
//
// root is used to skip certain aspects of the copy which are not valid
// for the root node of a object.
func rcopy(dst, src reflect.Value, root bool) {
if !src.IsValid() {
return
}
switch src.Kind() {
case reflect.Ptr:
if _, ok := src.Interface().(io.Reader); ok {
if dst.Kind() == reflect.Ptr && dst.Elem().CanSet() {
dst.Elem().Set(src)
} else if dst.CanSet() {
dst.Set(src)
}
} else {
e := src.Type().Elem()
if dst.CanSet() && !src.IsNil() {
if _, ok := src.Interface().(*time.Time); !ok {
dst.Set(reflect.New(e))
} else {
tempValue := reflect.New(e)
tempValue.Elem().Set(src.Elem())
// Sets time.Time's unexported values
dst.Set(tempValue)
}
}
if src.Elem().IsValid() {
// Keep the current root state since the depth hasn't changed
rcopy(dst.Elem(), src.Elem(), root)
}
}
case reflect.Struct:
t := dst.Type()
for i := 0; i < t.NumField(); i++ {
name := t.Field(i).Name
srcVal := src.FieldByName(name)
dstVal := dst.FieldByName(name)
if srcVal.IsValid() && dstVal.CanSet() {
rcopy(dstVal, srcVal, false)
}
}
case reflect.Slice:
if src.IsNil() {
break
}
s := reflect.MakeSlice(src.Type(), src.Len(), src.Cap())
dst.Set(s)
for i := 0; i < src.Len(); i++ {
rcopy(dst.Index(i), src.Index(i), false)
}
case reflect.Map:
if src.IsNil() {
break
}
s := reflect.MakeMap(src.Type())
dst.Set(s)
for _, k := range src.MapKeys() {
v := src.MapIndex(k)
v2 := reflect.New(v.Type()).Elem()
rcopy(v2, v, false)
dst.SetMapIndex(k, v2)
}
default:
// Assign the value if possible. If its not assignable, the value would
// need to be converted and the impact of that may be unexpected, or is
// not compatible with the dst type.
if src.Type().AssignableTo(dst.Type()) {
dst.Set(src)
}
}
}
+27
View File
@@ -0,0 +1,27 @@
package awsutil
import (
"reflect"
)
// DeepEqual returns if the two values are deeply equal like reflect.DeepEqual.
// In addition to this, this method will also dereference the input values if
// possible so the DeepEqual performed will not fail if one parameter is a
// pointer and the other is not.
//
// DeepEqual will not perform indirection of nested values of the input parameters.
func DeepEqual(a, b interface{}) bool {
ra := reflect.Indirect(reflect.ValueOf(a))
rb := reflect.Indirect(reflect.ValueOf(b))
if raValid, rbValid := ra.IsValid(), rb.IsValid(); !raValid && !rbValid {
// If the elements are both nil, and of the same type they are equal
// If they are of different types they are not equal
return reflect.TypeOf(a) == reflect.TypeOf(b)
} else if raValid != rbValid {
// Both values must be valid to be equal
return false
}
return reflect.DeepEqual(ra.Interface(), rb.Interface())
}
+221
View File
@@ -0,0 +1,221 @@
package awsutil
import (
"reflect"
"regexp"
"strconv"
"strings"
"github.com/jmespath/go-jmespath"
)
var indexRe = regexp.MustCompile(`(.+)\[(-?\d+)?\]$`)
// rValuesAtPath returns a slice of values found in value v. The values
// in v are explored recursively so all nested values are collected.
func rValuesAtPath(v interface{}, path string, createPath, caseSensitive, nilTerm bool) []reflect.Value {
pathparts := strings.Split(path, "||")
if len(pathparts) > 1 {
for _, pathpart := range pathparts {
vals := rValuesAtPath(v, pathpart, createPath, caseSensitive, nilTerm)
if len(vals) > 0 {
return vals
}
}
return nil
}
values := []reflect.Value{reflect.Indirect(reflect.ValueOf(v))}
components := strings.Split(path, ".")
for len(values) > 0 && len(components) > 0 {
var index *int64
var indexStar bool
c := strings.TrimSpace(components[0])
if c == "" { // no actual component, illegal syntax
return nil
} else if caseSensitive && c != "*" && strings.ToLower(c[0:1]) == c[0:1] {
// TODO normalize case for user
return nil // don't support unexported fields
}
// parse this component
if m := indexRe.FindStringSubmatch(c); m != nil {
c = m[1]
if m[2] == "" {
index = nil
indexStar = true
} else {
i, _ := strconv.ParseInt(m[2], 10, 32)
index = &i
indexStar = false
}
}
nextvals := []reflect.Value{}
for _, value := range values {
// pull component name out of struct member
if value.Kind() != reflect.Struct {
continue
}
if c == "*" { // pull all members
for i := 0; i < value.NumField(); i++ {
if f := reflect.Indirect(value.Field(i)); f.IsValid() {
nextvals = append(nextvals, f)
}
}
continue
}
value = value.FieldByNameFunc(func(name string) bool {
if c == name {
return true
} else if !caseSensitive && strings.ToLower(name) == strings.ToLower(c) {
return true
}
return false
})
if nilTerm && value.Kind() == reflect.Ptr && len(components[1:]) == 0 {
if !value.IsNil() {
value.Set(reflect.Zero(value.Type()))
}
return []reflect.Value{value}
}
if createPath && value.Kind() == reflect.Ptr && value.IsNil() {
// TODO if the value is the terminus it should not be created
// if the value to be set to its position is nil.
value.Set(reflect.New(value.Type().Elem()))
value = value.Elem()
} else {
value = reflect.Indirect(value)
}
if value.Kind() == reflect.Slice || value.Kind() == reflect.Map {
if !createPath && value.IsNil() {
value = reflect.ValueOf(nil)
}
}
if value.IsValid() {
nextvals = append(nextvals, value)
}
}
values = nextvals
if indexStar || index != nil {
nextvals = []reflect.Value{}
for _, valItem := range values {
value := reflect.Indirect(valItem)
if value.Kind() != reflect.Slice {
continue
}
if indexStar { // grab all indices
for i := 0; i < value.Len(); i++ {
idx := reflect.Indirect(value.Index(i))
if idx.IsValid() {
nextvals = append(nextvals, idx)
}
}
continue
}
// pull out index
i := int(*index)
if i >= value.Len() { // check out of bounds
if createPath {
// TODO resize slice
} else {
continue
}
} else if i < 0 { // support negative indexing
i = value.Len() + i
}
value = reflect.Indirect(value.Index(i))
if value.Kind() == reflect.Slice || value.Kind() == reflect.Map {
if !createPath && value.IsNil() {
value = reflect.ValueOf(nil)
}
}
if value.IsValid() {
nextvals = append(nextvals, value)
}
}
values = nextvals
}
components = components[1:]
}
return values
}
// ValuesAtPath returns a list of values at the case insensitive lexical
// path inside of a structure.
func ValuesAtPath(i interface{}, path string) ([]interface{}, error) {
result, err := jmespath.Search(path, i)
if err != nil {
return nil, err
}
v := reflect.ValueOf(result)
if !v.IsValid() || (v.Kind() == reflect.Ptr && v.IsNil()) {
return nil, nil
}
if s, ok := result.([]interface{}); ok {
return s, err
}
if v.Kind() == reflect.Map && v.Len() == 0 {
return nil, nil
}
if v.Kind() == reflect.Slice {
out := make([]interface{}, v.Len())
for i := 0; i < v.Len(); i++ {
out[i] = v.Index(i).Interface()
}
return out, nil
}
return []interface{}{result}, nil
}
// SetValueAtPath sets a value at the case insensitive lexical path inside
// of a structure.
func SetValueAtPath(i interface{}, path string, v interface{}) {
rvals := rValuesAtPath(i, path, true, false, v == nil)
for _, rval := range rvals {
if rval.Kind() == reflect.Ptr && rval.IsNil() {
continue
}
setValue(rval, v)
}
}
func setValue(dstVal reflect.Value, src interface{}) {
if dstVal.Kind() == reflect.Ptr {
dstVal = reflect.Indirect(dstVal)
}
srcVal := reflect.ValueOf(src)
if !srcVal.IsValid() { // src is literal nil
if dstVal.CanAddr() {
// Convert to pointer so that pointer's value can be nil'ed
// dstVal = dstVal.Addr()
}
dstVal.Set(reflect.Zero(dstVal.Type()))
} else if srcVal.Kind() == reflect.Ptr {
if srcVal.IsNil() {
srcVal = reflect.Zero(dstVal.Type())
} else {
srcVal = reflect.ValueOf(src).Elem()
}
dstVal.Set(srcVal)
} else {
dstVal.Set(srcVal)
}
}
+113
View File
@@ -0,0 +1,113 @@
package awsutil
import (
"bytes"
"fmt"
"io"
"reflect"
"strings"
)
// Prettify returns the string representation of a value.
func Prettify(i interface{}) string {
var buf bytes.Buffer
prettify(reflect.ValueOf(i), 0, &buf)
return buf.String()
}
// prettify will recursively walk value v to build a textual
// representation of the value.
func prettify(v reflect.Value, indent int, buf *bytes.Buffer) {
for v.Kind() == reflect.Ptr {
v = v.Elem()
}
switch v.Kind() {
case reflect.Struct:
strtype := v.Type().String()
if strtype == "time.Time" {
fmt.Fprintf(buf, "%s", v.Interface())
break
} else if strings.HasPrefix(strtype, "io.") {
buf.WriteString("<buffer>")
break
}
buf.WriteString("{\n")
names := []string{}
for i := 0; i < v.Type().NumField(); i++ {
name := v.Type().Field(i).Name
f := v.Field(i)
if name[0:1] == strings.ToLower(name[0:1]) {
continue // ignore unexported fields
}
if (f.Kind() == reflect.Ptr || f.Kind() == reflect.Slice || f.Kind() == reflect.Map) && f.IsNil() {
continue // ignore unset fields
}
names = append(names, name)
}
for i, n := range names {
val := v.FieldByName(n)
buf.WriteString(strings.Repeat(" ", indent+2))
buf.WriteString(n + ": ")
prettify(val, indent+2, buf)
if i < len(names)-1 {
buf.WriteString(",\n")
}
}
buf.WriteString("\n" + strings.Repeat(" ", indent) + "}")
case reflect.Slice:
strtype := v.Type().String()
if strtype == "[]uint8" {
fmt.Fprintf(buf, "<binary> len %d", v.Len())
break
}
nl, id, id2 := "", "", ""
if v.Len() > 3 {
nl, id, id2 = "\n", strings.Repeat(" ", indent), strings.Repeat(" ", indent+2)
}
buf.WriteString("[" + nl)
for i := 0; i < v.Len(); i++ {
buf.WriteString(id2)
prettify(v.Index(i), indent+2, buf)
if i < v.Len()-1 {
buf.WriteString("," + nl)
}
}
buf.WriteString(nl + id + "]")
case reflect.Map:
buf.WriteString("{\n")
for i, k := range v.MapKeys() {
buf.WriteString(strings.Repeat(" ", indent+2))
buf.WriteString(k.String() + ": ")
prettify(v.MapIndex(k), indent+2, buf)
if i < v.Len()-1 {
buf.WriteString(",\n")
}
}
buf.WriteString("\n" + strings.Repeat(" ", indent) + "}")
default:
if !v.IsValid() {
fmt.Fprint(buf, "<invalid value>")
return
}
format := "%v"
switch v.Interface().(type) {
case string:
format = "%q"
case io.ReadSeeker, io.Reader:
format = "buffer(%p)"
}
fmt.Fprintf(buf, format, v.Interface())
}
}
+88
View File
@@ -0,0 +1,88 @@
package awsutil
import (
"bytes"
"fmt"
"reflect"
"strings"
)
// StringValue returns the string representation of a value.
func StringValue(i interface{}) string {
var buf bytes.Buffer
stringValue(reflect.ValueOf(i), 0, &buf)
return buf.String()
}
func stringValue(v reflect.Value, indent int, buf *bytes.Buffer) {
for v.Kind() == reflect.Ptr {
v = v.Elem()
}
switch v.Kind() {
case reflect.Struct:
buf.WriteString("{\n")
for i := 0; i < v.Type().NumField(); i++ {
ft := v.Type().Field(i)
fv := v.Field(i)
if ft.Name[0:1] == strings.ToLower(ft.Name[0:1]) {
continue // ignore unexported fields
}
if (fv.Kind() == reflect.Ptr || fv.Kind() == reflect.Slice) && fv.IsNil() {
continue // ignore unset fields
}
buf.WriteString(strings.Repeat(" ", indent+2))
buf.WriteString(ft.Name + ": ")
if tag := ft.Tag.Get("sensitive"); tag == "true" {
buf.WriteString("<sensitive>")
} else {
stringValue(fv, indent+2, buf)
}
buf.WriteString(",\n")
}
buf.WriteString("\n" + strings.Repeat(" ", indent) + "}")
case reflect.Slice:
nl, id, id2 := "", "", ""
if v.Len() > 3 {
nl, id, id2 = "\n", strings.Repeat(" ", indent), strings.Repeat(" ", indent+2)
}
buf.WriteString("[" + nl)
for i := 0; i < v.Len(); i++ {
buf.WriteString(id2)
stringValue(v.Index(i), indent+2, buf)
if i < v.Len()-1 {
buf.WriteString("," + nl)
}
}
buf.WriteString(nl + id + "]")
case reflect.Map:
buf.WriteString("{\n")
for i, k := range v.MapKeys() {
buf.WriteString(strings.Repeat(" ", indent+2))
buf.WriteString(k.String() + ": ")
stringValue(v.MapIndex(k), indent+2, buf)
if i < v.Len()-1 {
buf.WriteString(",\n")
}
}
buf.WriteString("\n" + strings.Repeat(" ", indent) + "}")
default:
format := "%v"
switch v.Interface().(type) {
case string:
format = "%q"
}
fmt.Fprintf(buf, format, v.Interface())
}
}
+96
View File
@@ -0,0 +1,96 @@
package client
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/client/metadata"
"github.com/aws/aws-sdk-go/aws/request"
)
// A Config provides configuration to a service client instance.
type Config struct {
Config *aws.Config
Handlers request.Handlers
Endpoint string
SigningRegion string
SigningName string
// States that the signing name did not come from a modeled source but
// was derived based on other data. Used by service client constructors
// to determine if the signin name can be overridden based on metadata the
// service has.
SigningNameDerived bool
}
// ConfigProvider provides a generic way for a service client to receive
// the ClientConfig without circular dependencies.
type ConfigProvider interface {
ClientConfig(serviceName string, cfgs ...*aws.Config) Config
}
// ConfigNoResolveEndpointProvider same as ConfigProvider except it will not
// resolve the endpoint automatically. The service client's endpoint must be
// provided via the aws.Config.Endpoint field.
type ConfigNoResolveEndpointProvider interface {
ClientConfigNoResolveEndpoint(cfgs ...*aws.Config) Config
}
// A Client implements the base client request and response handling
// used by all service clients.
type Client struct {
request.Retryer
metadata.ClientInfo
Config aws.Config
Handlers request.Handlers
}
// New will return a pointer to a new initialized service client.
func New(cfg aws.Config, info metadata.ClientInfo, handlers request.Handlers, options ...func(*Client)) *Client {
svc := &Client{
Config: cfg,
ClientInfo: info,
Handlers: handlers.Copy(),
}
switch retryer, ok := cfg.Retryer.(request.Retryer); {
case ok:
svc.Retryer = retryer
case cfg.Retryer != nil && cfg.Logger != nil:
s := fmt.Sprintf("WARNING: %T does not implement request.Retryer; using DefaultRetryer instead", cfg.Retryer)
cfg.Logger.Log(s)
fallthrough
default:
maxRetries := aws.IntValue(cfg.MaxRetries)
if cfg.MaxRetries == nil || maxRetries == aws.UseServiceDefaultRetries {
maxRetries = 3
}
svc.Retryer = DefaultRetryer{NumMaxRetries: maxRetries}
}
svc.AddDebugHandlers()
for _, option := range options {
option(svc)
}
return svc
}
// NewRequest returns a new Request pointer for the service API
// operation and parameters.
func (c *Client) NewRequest(operation *request.Operation, params interface{}, data interface{}) *request.Request {
return request.New(c.Config, c.ClientInfo, c.Handlers, c.Retryer, operation, params, data)
}
// AddDebugHandlers injects debug logging handlers into the service to log request
// debug information.
func (c *Client) AddDebugHandlers() {
if !c.Config.LogLevel.AtLeast(aws.LogDebug) {
return
}
c.Handlers.Send.PushFrontNamed(LogHTTPRequestHandler)
c.Handlers.Send.PushBackNamed(LogHTTPResponseHandler)
}
+103
View File
@@ -0,0 +1,103 @@
package client
import (
"strconv"
"time"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/internal/sdkrand"
)
// DefaultRetryer implements basic retry logic using exponential backoff for
// most services. If you want to implement custom retry logic, implement the
// request.Retryer interface or create a structure type that composes this
// struct and override the specific methods. For example, to override only
// the MaxRetries method:
//
// type retryer struct {
// client.DefaultRetryer
// }
//
// // This implementation always has 100 max retries
// func (d retryer) MaxRetries() int { return 100 }
type DefaultRetryer struct {
NumMaxRetries int
}
// MaxRetries returns the number of maximum returns the service will use to make
// an individual API request.
func (d DefaultRetryer) MaxRetries() int {
return d.NumMaxRetries
}
// RetryRules returns the delay duration before retrying this request again
func (d DefaultRetryer) RetryRules(r *request.Request) time.Duration {
// Set the upper limit of delay in retrying at ~five minutes
minTime := 30
isThrottle := r.IsErrorThrottle()
if isThrottle {
if delay, ok := getRetryDelay(r); ok {
return delay
}
minTime = 500
}
retryCount := r.RetryCount
if isThrottle && retryCount > 8 {
retryCount = 8
} else if retryCount > 13 {
retryCount = 13
}
delay := (1 << uint(retryCount)) * (sdkrand.SeededRand.Intn(minTime) + minTime)
return time.Duration(delay) * time.Millisecond
}
// ShouldRetry returns true if the request should be retried.
func (d DefaultRetryer) ShouldRetry(r *request.Request) bool {
// If one of the other handlers already set the retry state
// we don't want to override it based on the service's state
if r.Retryable != nil {
return *r.Retryable
}
if r.HTTPResponse.StatusCode >= 500 && r.HTTPResponse.StatusCode != 501 {
return true
}
return r.IsErrorRetryable() || r.IsErrorThrottle()
}
// This will look in the Retry-After header, RFC 7231, for how long
// it will wait before attempting another request
func getRetryDelay(r *request.Request) (time.Duration, bool) {
if !canUseRetryAfterHeader(r) {
return 0, false
}
delayStr := r.HTTPResponse.Header.Get("Retry-After")
if len(delayStr) == 0 {
return 0, false
}
delay, err := strconv.Atoi(delayStr)
if err != nil {
return 0, false
}
return time.Duration(delay) * time.Second, true
}
// Will look at the status code to see if the retry header pertains to
// the status code.
func canUseRetryAfterHeader(r *request.Request) bool {
switch r.HTTPResponse.StatusCode {
case 429:
case 503:
default:
return false
}
return true
}

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